diff --git a/src/admin/public/style.css b/src/admin/public/style.css
index 52e968c..a5f3d22 100644
--- a/src/admin/public/style.css
+++ b/src/admin/public/style.css
@@ -753,3 +753,66 @@ table.compact th, table.compact td {
th, td { padding: 0.4rem; }
.catalog-layout { grid-template-columns: 1fr; }
}
+
+.locale-actions {
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.save-status {
+ font-size: 14px;
+ color: #666;
+}
+
+.locale-section {
+ margin-bottom: 30px;
+}
+
+.locale-section h3 {
+ text-transform: capitalize;
+ margin-bottom: 10px;
+ color: #333;
+}
+
+.locale-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 15px;
+}
+
+.locale-table th,
+.locale-table td {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ text-align: left;
+}
+
+.locale-table th {
+ background: #f5f5f5;
+ font-weight: 600;
+}
+
+.locale-table .key-cell {
+ width: 200px;
+}
+
+.locale-table .key-cell code {
+ font-size: 12px;
+ color: #666;
+}
+
+.locale-input {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.locale-input:focus {
+ border-color: #4CAF50;
+ outline: none;
+ box-shadow: 0 0 3px rgba(76, 175, 80, 0.3);
+}
diff --git a/src/admin/routes/locales.js b/src/admin/routes/locales.js
new file mode 100644
index 0000000..b14fff8
--- /dev/null
+++ b/src/admin/routes/locales.js
@@ -0,0 +1,171 @@
+import express, { Router } from 'express';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import logger from '../../utils/logger.js';
+import { layout } from '../views/layout.js';
+import { AVAILABLE_LANGUAGES } from '../../i18n/index.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const LOCALES_DIR = path.join(__dirname, '..', '..', 'i18n', 'locales');
+
+const router = Router();
+
+router.get('/', (req, res) => {
+ try {
+ const files = fs.readdirSync(LOCALES_DIR).filter(f => f.endsWith('.json'));
+ const locales = {};
+
+ for (const file of files) {
+ const lang = file.replace('.json', '');
+ const content = fs.readFileSync(path.join(LOCALES_DIR, file), 'utf-8');
+ locales[lang] = JSON.parse(content);
+ }
+
+ const html = renderLocalesPage(locales);
+ res.send(html);
+ } catch (error) {
+ logger.error({ err: error }, 'Error loading locales');
+ res.status(500).send('Error loading locales');
+ }
+});
+
+router.post('/save', express.json(), (req, res) => {
+ try {
+ const { lang, key, value } = req.body;
+
+ if (!lang || !key || value === undefined) {
+ return res.status(400).json({ error: 'Missing required fields' });
+ }
+
+ if (!AVAILABLE_LANGUAGES.includes(lang)) {
+ return res.status(400).json({ error: 'Invalid language' });
+ }
+
+ const filePath = path.join(LOCALES_DIR, `${lang}.json`);
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const locale = JSON.parse(content);
+
+ const keys = key.split('.');
+ let obj = locale;
+ for (let i = 0; i < keys.length - 1; i++) {
+ if (!obj[keys[i]]) obj[keys[i]] = {};
+ obj = obj[keys[i]];
+ }
+ obj[keys[keys.length - 1]] = value;
+
+ fs.writeFileSync(filePath, JSON.stringify(locale, null, 2) + '\n', 'utf-8');
+
+ res.json({ success: true });
+ } catch (error) {
+ logger.error({ err: error }, 'Error saving locale');
+ res.status(500).json({ error: 'Error saving locale' });
+ }
+});
+
+function renderLocalesPage(locales) {
+ const sections = Object.keys(locales.en || {});
+
+ let sectionsHtml = '';
+ for (const section of sections) {
+ const keys = Object.keys(locales.en[section] || {});
+ let rowsHtml = '';
+ for (const key of keys) {
+ const fullKey = `${section}.${key}`;
+ const enVal = locales.en?.[section]?.[key] || '';
+ const deVal = locales.de?.[section]?.[key] || '';
+ const esVal = locales.es?.[section]?.[key] || '';
+
+ rowsHtml += `
+
+ ${fullKey} |
+ |
+ |
+ |
+
`;
+ }
+
+ sectionsHtml += `
+
+
${section}
+
+
+
+ | Ключ |
+ English |
+ Deutsch |
+ Español |
+
+
+ ${rowsHtml}
+
+
`;
+ }
+
+ const content = `
+
+
+
+
+ ${sectionsHtml}
+
+ `;
+
+ return layout('Локализация', content, 'locales');
+}
+
+function escapeAttr(str) {
+ return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
+}
+
+export default router;
diff --git a/src/admin/server.js b/src/admin/server.js
index 9e97ded..6123a24 100644
--- a/src/admin/server.js
+++ b/src/admin/server.js
@@ -17,6 +17,7 @@ import categoriesRouter from './routes/categories.js';
import paymentWalletsRouter from './routes/paymentWallets.js';
import locationsRouter from './routes/locations.js';
import seedRouter from './routes/seed.js';
+import localesRouter from './routes/locales.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
@@ -52,6 +53,7 @@ app.use('/categories', categoriesRouter);
app.use('/locations', locationsRouter);
app.use('/payment-wallets', paymentWalletsRouter);
app.use('/seed', seedRouter);
+app.use('/locales', localesRouter);
export function startAdminPanel() {
const port = parseInt(process.env.ADMIN_PORT || '3001', 10);
diff --git a/src/admin/views/layout.js b/src/admin/views/layout.js
index 3f5f6c0..fc8af6c 100644
--- a/src/admin/views/layout.js
+++ b/src/admin/views/layout.js
@@ -9,6 +9,7 @@ export function layout(title, content, activeTab = '') {
{ href: '/settings', label: 'Settings', id: 'settings' },
{ href: '/payment-wallets', label: 'Payment Wallets', id: 'payment-wallets' },
{ href: '/seed', label: 'Seed & Reset', id: 'seed' },
+ { href: '/locales', label: 'Локализация', id: 'locales' },
];
const navHtml = nav.map(n =>
diff --git a/src/config/config.js b/src/config/config.js
index 57163fe..7d79f25 100644
--- a/src/config/config.js
+++ b/src/config/config.js
@@ -31,6 +31,7 @@ export default {
ADMIN_IDS,
SUPER_ADMIN_IDS,
SUPPORT_LINK: process.env.SUPPORT_LINK,
+ DEFAULT_LANGUAGE: process.env.DEFAULT_LANGUAGE || 'en',
CATALOG_PATH: process.env.CATALOG_PATH,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
diff --git a/src/handlers/userHandlers/userDeletionHandler.js b/src/handlers/userHandlers/userDeletionHandler.js
index fcfd3fc..6047e77 100644
--- a/src/handlers/userHandlers/userDeletionHandler.js
+++ b/src/handlers/userHandlers/userDeletionHandler.js
@@ -5,23 +5,30 @@ import UserService from "../../services/userService.js";
import userStates from "../../context/userStates.js";
import logger from '../../utils/logger.js';
import { editOrSendCallback } from '../../utils/messageUtils.js';
+import { tForUser } from '../../i18n/index.js';
export default class UserDeletionHandler {
static async handleDeleteAccount(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
+ const text = `${t('deletion.confirm_title')}\n\n${t('deletion.confirm_body')}`;
+
const keyboard = {
inline_keyboard: [
[
- {text: '✅ Confirm Delete', callback_data: `confirm_delete_account`},
- {text: '❌ Cancel', callback_data: `back_to_profile`}
+ {text: t('deletion.confirm_button'), callback_data: `confirm_delete_account`},
+ {text: t('deletion.cancel_button'), callback_data: `back_to_profile`}
]
]
};
await bot.editMessageText(
- `⚠️ Are you sure you want to delete your account?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
+ text,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -31,24 +38,27 @@ export default class UserDeletionHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleDeleteUser');
- await editOrSendCallback(callbackQuery, 'Error processing delete request. Please try again.');
+ await editOrSendCallback(callbackQuery, t('deletion.error_processing'));
}
}
static async handleConfirmDelete(callbackQuery) {
const telegramId = callbackQuery.from.id;
const chatId = callbackQuery.message.chat.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
await UserService.updateUserStatus(telegramId, 1);
await bot.editMessageText(
- '⚠️Your account has been successful deleted',
+ t('deletion.deleted'),
{ chat_id: chatId, message_id: callbackQuery.message.message_id, }
);
} catch (error) {
logger.error({ err: error }, 'Error in handleConfirmDelete');
- await editOrSendCallback(callbackQuery, 'Error deleting user. Please try again.');
+ await editOrSendCallback(callbackQuery, t('deletion.error_deleting'));
}
}
}
\ No newline at end of file
diff --git a/src/handlers/userHandlers/userHandler.js b/src/handlers/userHandlers/userHandler.js
index 7e6ad72..d01c2f4 100644
--- a/src/handlers/userHandlers/userHandler.js
+++ b/src/handlers/userHandlers/userHandler.js
@@ -5,15 +5,19 @@ import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js";
import logger from "../../utils/logger.js";
+import { tForUser, LANGUAGE_NAMES, AVAILABLE_LANGUAGES } from '../../i18n/index.js';
export default class UserHandler {
static async canUseBot(msg) {
const telegramId = msg.from.id;
const user = await UserService.getUserByTelegramId(telegramId);
+ msg.__user = user; // Cache user for downstream handlers
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
const keyboard = {
inline_keyboard: [
- [{text: "Contact support", url: config.SUPPORT_LINK}]
+ [{text: t('bot.contact_support'), url: config.SUPPORT_LINK}]
]
};
@@ -21,10 +25,10 @@ export default class UserHandler {
case 0:
return true;
case 1:
- await bot.sendMessage(telegramId, '⚠️Your account has been deleted by administrator', {reply_markup: keyboard});
+ await bot.sendMessage(telegramId, t('bot.account_deleted'), {reply_markup: keyboard});
return false;
case 2:
- await bot.sendMessage(telegramId, '⚠️Your account has been blocked by administrator', {reply_markup: keyboard});
+ await bot.sendMessage(telegramId, t('bot.account_blocked'), {reply_markup: keyboard});
return false;
default:
return true;
@@ -34,58 +38,58 @@ export default class UserHandler {
static async showProfile(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
-
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
await UserService.recalculateUserBalanceByTelegramId(telegramId);
const userStats = await UserService.getDetailedUserByTelegramId(telegramId);
-
+
if (!userStats) {
- await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
+ await bot.sendMessage(chatId, t('profile.not_found'));
return;
}
-
- // Получаем балансы активных и архивных кошельков
+
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(userStats.id);
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(userStats.id);
-
- // Доступный баланс (bonus_balance + total_balance)
const availableBalance = userStats.bonus_balance + (userStats.total_balance || 0);
-
+
const locationText = userStats.country && userStats.city && userStats.district
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
- : 'Not set';
-
+ : t('profile.location_not_set');
+
const text = `
- 👤 *Your Profile*
-
- 📱 Telegram ID: \`${telegramId}\`
- 📍 Location: ${locationText}
-
- 📊 Statistics:
- ├ Total Purchases: ${userStats.purchase_count || 0}
- ├ Total Spent: $${userStats.total_spent || 0}
- ├ Active Wallets: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
- ├ Archived Wallets: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
- ├ Bonus Balance: $${userStats.bonus_balance || 0}
- └ Available Balance: $${availableBalance.toFixed(2)}
-
- 📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
- `;
-
+${t('profile.title')}
+
+${t('profile.telegram_id')}: \`${telegramId}\`
+${t('profile.location')}: ${locationText}
+
+${t('profile.stats')}
+├ ${t('profile.total_purchases')}: ${userStats.purchase_count || 0}
+├ ${t('profile.total_spent')}: $${userStats.total_spent || 0}
+├ ${t('profile.active_wallets')}: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
+├ ${t('profile.archived_wallets')}: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
+├ ${t('profile.bonus_balance')}: $${userStats.bonus_balance || 0}
+└ ${t('profile.available_balance')}: $${availableBalance.toFixed(2)}
+
+${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateString()}
+`;
+
const keyboard = {
inline_keyboard: [
- [{text: '📍 Set Location', callback_data: 'set_location'}],
- [{text: '❌ Delete Account', callback_data: 'delete_account'}]
+ [{text: t('profile.set_location'), callback_data: 'set_location'}],
+ [{text: t('profile.delete_account'), callback_data: 'delete_account'}]
]
};
-
+
await bot.sendMessage(chatId, text, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
logger.error({ err: error }, 'Error in showProfile');
- await bot.sendMessage(chatId, 'Error loading profile. Please try again.');
+ await bot.sendMessage(chatId, t('profile.error_loading'));
}
}
@@ -95,33 +99,92 @@ export default class UserHandler {
const username = msg.chat.username;
try {
- // Create user profile
await UserService.createUser({
telegram_id: telegramId,
username: username
});
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language;
+
+ if (!user?.language_set) {
+ const keyboard = {
+ inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
+ text: LANGUAGE_NAMES[code],
+ callback_data: `set_language_${code}`
+ }])
+ };
+ await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
+ return;
+ }
+
+ const t = tForUser(lang);
+
const keyboard = {
reply_markup: {
keyboard: [
- ['📦 Products', '👤 Profile'],
- ['🛍 Purchases', '💰 Wallets']
+ [t('keyboard.products'), t('keyboard.profile')],
+ [t('keyboard.purchases'), t('keyboard.wallets')]
],
resize_keyboard: true
}
};
- await bot.sendMessage(
- chatId,
- 'Welcome to the shop! Choose an option:',
- keyboard
- );
+ await bot.sendMessage(chatId, t('bot.welcome'), keyboard);
} catch (error) {
logger.error({ err: error }, 'Error in handleStart');
- await bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
+ const fallbackT = tForUser('en');
+ await bot.sendMessage(chatId, fallbackT('bot.error_generic'));
}
}
+ static async handleSetLanguage(callbackQuery) {
+ const chatId = callbackQuery.message.chat.id;
+ const telegramId = callbackQuery.from.id;
+ const lang = callbackQuery.data.replace('set_language_', '');
+
+ if (!AVAILABLE_LANGUAGES.includes(lang)) {
+ await bot.answerCallbackQuery(callbackQuery.id);
+ return;
+ }
+
+ try {
+ await UserService.setUserLanguage(telegramId, lang);
+ const t = tForUser(lang);
+
+ await bot.answerCallbackQuery(callbackQuery.id);
+
+ const keyboard = {
+ reply_markup: {
+ keyboard: [
+ [t('keyboard.products'), t('keyboard.profile')],
+ [t('keyboard.purchases'), t('keyboard.wallets')]
+ ],
+ resize_keyboard: true
+ }
+ };
+
+ await bot.deleteMessage(chatId, callbackQuery.message.message_id);
+ await bot.sendMessage(chatId, t('bot.language_changed', { language: LANGUAGE_NAMES[lang] }), keyboard);
+ } catch (error) {
+ logger.error({ err: error }, 'Error in handleSetLanguage');
+ await bot.answerCallbackQuery(callbackQuery.id);
+ }
+ }
+
+ static async handleLanguageCommand(msg) {
+ const chatId = msg.chat.id;
+
+ const keyboard = {
+ inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{
+ text: LANGUAGE_NAMES[code],
+ callback_data: `set_language_${code}`
+ }])
+ };
+
+ await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard });
+ }
+
static async handleBackToProfile(callbackQuery) {
await this.showProfile({
chat: {id: callbackQuery.message.chat.id},
diff --git a/src/handlers/userHandlers/userLocationHandler.js b/src/handlers/userHandlers/userLocationHandler.js
index abdf270..b32b8a5 100644
--- a/src/handlers/userHandlers/userLocationHandler.js
+++ b/src/handlers/userHandlers/userLocationHandler.js
@@ -4,24 +4,29 @@ import bot from "../../context/bot.js";
import UserService from "../../services/userService.js";
import logger from '../../utils/logger.js';
import { editOrSendCallback } from '../../utils/messageUtils.js';
+import { tForUser } from '../../i18n/index.js';
export default class UserLocationHandler {
static async handleSetLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
const countries = await LocationService.getCountries();
if (countries.length === 0) {
await bot.editMessageText(
- 'No locations available yet.',
+ t('location.no_locations'),
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
- {text: '« Back to Profile', callback_data: 'back_to_profile'}
+ {text: t('location.back_to_profile'), callback_data: 'back_to_profile'}
]]
}
}
@@ -35,12 +40,12 @@ export default class UserLocationHandler {
text: loc.country,
callback_data: `set_country_${loc.country}`
}]),
- [{text: '« Back to Profile', callback_data: 'back_to_profile'}]
+ [{text: t('location.back_to_profile'), callback_data: 'back_to_profile'}]
]
};
await bot.editMessageText(
- '🌍 Select your country:',
+ t('location.select_country'),
{
chat_id: chatId,
message_id: messageId,
@@ -49,7 +54,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetLocation');
- await editOrSendCallback(callbackQuery, 'Error loading countries. Please try again.');
+ await editOrSendCallback(callbackQuery, t('location.error_loading_countries'));
}
}
@@ -57,6 +62,10 @@ export default class UserLocationHandler {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('set_country_', '');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
const cities = await LocationService.getCitiesByCountry(country);
@@ -67,12 +76,12 @@ export default class UserLocationHandler {
text: loc.city,
callback_data: `set_city_${country}_${loc.city}`
}]),
- [{text: '« Back to Countries', callback_data: 'set_location'}]
+ [{text: t('location.back_to_countries'), callback_data: 'set_location'}]
]
};
await bot.editMessageText(
- `🏙 Select city in ${country}:`,
+ t('location.select_city', { country }),
{
chat_id: chatId,
message_id: messageId,
@@ -81,7 +90,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetCountry');
- await editOrSendCallback(callbackQuery, 'Error loading cities. Please try again.');
+ await editOrSendCallback(callbackQuery, t('location.error_loading_cities'));
}
}
@@ -89,6 +98,10 @@ export default class UserLocationHandler {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('set_city_', '').split('_');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
const districts = await LocationService.getDistrictsByCountryAndCity(country, city);
@@ -99,12 +112,12 @@ export default class UserLocationHandler {
text: loc.district,
callback_data: `set_district_${country}_${city}_${loc.district}`
}]),
- [{text: '« Back to Cities', callback_data: `set_country_${country}`}]
+ [{text: t('location.back_to_countries'), callback_data: `set_country_${country}`}]
]
};
await bot.editMessageText(
- `📍 Select district in ${city}:`,
+ t('location.select_district', { city }),
{
chat_id: chatId,
message_id: messageId,
@@ -113,7 +126,7 @@ export default class UserLocationHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSetCity');
- await editOrSendCallback(callbackQuery, 'Error loading districts. Please try again.');
+ await editOrSendCallback(callbackQuery, t('location.error_loading_districts'));
}
}
@@ -122,6 +135,9 @@ export default class UserLocationHandler {
const messageId = callbackQuery.message.message_id;
const telegramId = callbackQuery.from.id;
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
try {
await db.runAsync('BEGIN TRANSACTION');
@@ -129,13 +145,13 @@ export default class UserLocationHandler {
await db.runAsync('COMMIT');
await bot.editMessageText(
- `✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
+ `${t('location.location_updated')}\n\n${t('location.country')}: ${country}\n${t('location.city')}: ${city}\n${t('location.district')}: ${district}`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
- {text: '« Back to Profile', callback_data: 'back_to_profile'}
+ {text: t('location.back_to_profile'), callback_data: 'back_to_profile'}
]]
}
}
@@ -143,7 +159,7 @@ export default class UserLocationHandler {
} catch (error) {
await db.runAsync('ROLLBACK');
logger.error({ err: error }, 'Error in handleSetDistrict');
- await editOrSendCallback(callbackQuery, 'Error updating location. Please try again.');
+ await editOrSendCallback(callbackQuery, t('location.error_updating'));
}
}
}
\ No newline at end of file
diff --git a/src/handlers/userHandlers/userProductHandler.js b/src/handlers/userHandlers/userProductHandler.js
index 867cf54..24fc5e5 100644
--- a/src/handlers/userHandlers/userProductHandler.js
+++ b/src/handlers/userHandlers/userProductHandler.js
@@ -9,6 +9,7 @@ import UserService from "../../services/userService.js";
import PurchaseService from '../../services/purchaseService.js';
import Validators from '../../utils/validators.js';
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
+import { tForUser } from '../../i18n/index.js';
import fs from 'fs';
import path from 'path';
@@ -44,10 +45,14 @@ export default class UserProductHandler {
const messageId = msg?.message_id;
try {
+ const user = await UserService.getUserByTelegramId(msg.from.id);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const countries = await LocationService.getCountries()
if (countries.length === 0) {
- const message = 'No products available at the moment.';
+ const message = t('products.no_products');
if (messageId) {
await bot.editMessageText(message, {
chat_id: chatId,
@@ -66,7 +71,7 @@ export default class UserProductHandler {
}])
};
- const message = '🌍 Select your country:';
+ const message = t('products.select_country');
try {
if (messageId) {
@@ -81,7 +86,10 @@ export default class UserProductHandler {
}
} catch (error) {
logger.error({ err: error }, 'Error in showProducts');
- await bot.sendMessage(chatId, 'Error loading products. Please try again.');
+ const user = await UserService.getUserByTelegramId(msg.from.id).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading'));
}
}
@@ -91,6 +99,11 @@ export default class UserProductHandler {
const country = decodeURIComponent(callbackQuery.data.replace('shop_country_', ''));
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const cities = await LocationService.getCitiesByCountry(country);
const keyboard = {
@@ -99,12 +112,12 @@ export default class UserProductHandler {
text: loc.city,
callback_data: `shop_city_${encodeURIComponent(country)}|${encodeURIComponent(loc.city)}`
}]),
- [{text: '« Back to Countries', callback_data: 'shop_start'}]
+ [{text: t('products.back_to_countries'), callback_data: 'shop_start'}]
]
};
await bot.editMessageText(
- `🏙 Select city in ${country}:`,
+ t('products.select_city', { country }),
{
chat_id: chatId,
message_id: messageId,
@@ -113,7 +126,11 @@ export default class UserProductHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleCountrySelection');
- await editOrSendCallback(callbackQuery, 'Error loading cities. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await editOrSendCallback(callbackQuery, t('products.error_loading_cities'));
}
}
@@ -124,6 +141,11 @@ export default class UserProductHandler {
const [country, city] = payload.split('|').map(decodeURIComponent);
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const locations = await LocationService.getLocationsByCountryAndCity(country, city);
const keyboard = {
@@ -132,12 +154,12 @@ export default class UserProductHandler {
text: loc.district || loc.city,
callback_data: `shop_loc_${loc.id}`
}]),
- [{text: '« Back to Cities', callback_data: `shop_country_${encodeURIComponent(country)}`}]
+ [{text: t('products.back_to_cities'), callback_data: `shop_country_${encodeURIComponent(country)}`}]
]
};
await bot.editMessageText(
- `📍 Select district in ${city}:`,
+ t('products.select_district', { city }),
{
chat_id: chatId,
message_id: messageId,
@@ -146,7 +168,11 @@ export default class UserProductHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleCitySelection');
- await editOrSendCallback(callbackQuery, 'Error loading districts. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await editOrSendCallback(callbackQuery, t('products.error_loading_districts'));
}
}
@@ -154,19 +180,24 @@ export default class UserProductHandler {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const locationId = parseInt(callbackQuery.data.replace('shop_loc_', ''), 10);
-
+
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const location = await LocationService.getLocationById(locationId);
if (!location) {
await bot.editMessageText(
- 'Location not found. Returning to previous menu.',
+ t('products.not_found'),
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
- { text: '« Back', callback_data: `shop_city_${encodeURIComponent(location?.country || '')}|${encodeURIComponent(location?.city || '')}` }
+ { text: t('products.back'), callback_data: `shop_city_${encodeURIComponent(location?.country || '')}|${encodeURIComponent(location?.city || '')}` }
]]
}
}
@@ -187,12 +218,12 @@ export default class UserProductHandler {
text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}`
}]),
- [{ text: '« Back', callback_data: `shop_city_${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}` }]
+ [{ text: t('products.back'), callback_data: `shop_city_${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}` }]
]
};
await bot.editMessageText(
- '📦 Select category:',
+ t('products.select_category'),
{
chat_id: chatId,
message_id: messageId,
@@ -201,7 +232,11 @@ export default class UserProductHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleDistrictSelection');
- await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading_categories'));
}
}
@@ -209,8 +244,13 @@ export default class UserProductHandler {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
-
+
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
// Удаляем текущее сообщение
await bot.deleteMessage(chatId, messageId);
@@ -232,11 +272,11 @@ export default class UserProductHandler {
if (products.length === 0) {
await bot.sendMessage(
chatId,
- 'No products available in this category.',
+ t('products.no_products_category'),
{
reply_markup: {
inline_keyboard: [[
- { text: '« Back', callback_data: `shop_district_${state.location}` }
+ { text: t('products.back'), callback_data: `shop_district_${state.location}` }
]]
}
}
@@ -256,13 +296,13 @@ export default class UserProductHandler {
// Добавляем кнопку "Назад"
keyboard.inline_keyboard.push([
- { text: '« Back', callback_data: `shop_district_${state.location}` }
+ { text: t('products.back'), callback_data: `shop_district_${state.location}` }
]);
// Отправляем сообщение с товарами
await bot.sendMessage(
chatId,
- 'Select a product:',
+ t('products.select_product'),
{
reply_markup: keyboard
}
@@ -277,7 +317,11 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleCategorySelection');
- await bot.sendMessage(chatId, 'Error loading products. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading'));
}
}
@@ -287,6 +331,11 @@ export default class UserProductHandler {
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
// Delete the photo message if it exists
if (photoMessageId) {
try {
@@ -301,14 +350,14 @@ export default class UserProductHandler {
if (products.length === 0) {
await bot.editMessageText(
- 'No products available in this subcategory.',
+ t('products.no_products_subcategory'),
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{
- text: '« Back to Subcategories',
+ text: t('products.back_to_subcategories'),
callback_data: `shop_category_${locationId}_${categoryId}`
}
]]
@@ -324,12 +373,12 @@ export default class UserProductHandler {
text: `${prod.name} - $${prod.price}`,
callback_data: `shop_product_${prod.id}`
}]),
- [{text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}`}]
+ [{text: t('products.back_to_subcategories'), callback_data: `shop_category_${locationId}_${categoryId}`}]
]
};
await bot.editMessageText(
- `📦 Products in ${subcategory.name}:`,
+ t('products.products_in', { name: subcategory.name }),
{
chat_id: chatId,
message_id: messageId,
@@ -338,7 +387,11 @@ export default class UserProductHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleSubcategorySelection');
- await bot.sendMessage(chatId, 'Error loading products. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading'));
}
}
@@ -346,8 +399,13 @@ export default class UserProductHandler {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('shop_product_', '');
-
+
try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const product = await ProductService.getDetailedProductById(productId);
if (!product) {
@@ -372,11 +430,11 @@ export default class UserProductHandler {
const message = `
📦 ${product.name}
- 💰 Price: $${product.price}
- 📝 Description: ${product.description}
- 📦 Available: ${product.quantity_in_stock} pcs
+ ${t('products.product_price')}: $${product.price}
+ ${t('products.product_description')}: ${product.description}
+ ${t('products.product_available')}: ${product.quantity_in_stock} pcs
- Category: ${product.category_name}
+ ${t('products.product_category')}: ${product.category_name}
`;
// Отправляем фото, если оно существует
@@ -387,7 +445,7 @@ export default class UserProductHandler {
const keyboard = {
inline_keyboard: [
- [{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
+ [{ text: t('products.buy_now'), callback_data: `buy_product_${productId}` }],
[
{
text: '➖',
@@ -401,7 +459,7 @@ export default class UserProductHandler {
callback_game: product.quantity_in_stock <= 1 ? {} : null // Отключено, если остаток 1 или меньше
}
],
- [{ text: `« Back ${product.category_name}`, callback_data: `shop_category_${product.location_id}_${product.category_id}` }] // Возврат к категории
+ [{ text: `${t('products.back')} ${product.category_name}`, callback_data: `shop_category_${product.location_id}_${product.category_id}` }] // Возврат к категории
]
};
@@ -422,7 +480,11 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleProductSelection');
- await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading_product'));
}
}
@@ -557,6 +619,9 @@ export default class UserProductHandler {
if (!user) {
throw new Error('User not found');
}
+
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
const product = await ProductService.getProductById(productId);
if (!product) {
@@ -572,11 +637,11 @@ export default class UserProductHandler {
// Проверка баланса пользователя
if (userBalance <= 0) {
await editOrSendCallback(callbackQuery,
- `❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
+ t('purchase.insufficient_balance', { balance: userBalance, total: totalPrice }),
{
reply_markup: {
inline_keyboard: [[
- { text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
+ { text: t('purchase.top_up_balance'), callback_data: 'top_up_wallet' }
]]
}
}
@@ -586,11 +651,11 @@ export default class UserProductHandler {
if (userBalance < totalPrice) {
await editOrSendCallback(callbackQuery,
- `❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
+ t('purchase.insufficient_balance', { balance: userBalance, total: totalPrice }),
{
reply_markup: {
inline_keyboard: [[
- { text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
+ { text: t('purchase.top_up_balance'), callback_data: 'top_up_wallet' }
]]
}
}
@@ -608,11 +673,11 @@ export default class UserProductHandler {
if (cryptoWallets.length === 0) {
await editOrSendCallback(callbackQuery,
- 'You need to add a crypto wallet first to make purchases.',
+ t('purchase.need_wallet'),
{
reply_markup: {
inline_keyboard: [[
- { text: '➕ Add Wallet', callback_data: 'add_wallet' }
+ { text: t('purchase.add_wallet'), callback_data: 'add_wallet' }
]]
}
}
@@ -622,17 +687,17 @@ export default class UserProductHandler {
const keyboard = {
inline_keyboard: [
- [{ text: `Pay`, callback_data: `pay_with_main_${productId}_${quantity}` }],
- [{ text: '« Cancel', callback_data: `shop_product_${productId}` }] // Кнопка "Back"
+ [{ text: t('purchase.pay'), callback_data: `pay_with_main_${productId}_${quantity}` }],
+ [{ text: t('purchase.cancel'), callback_data: `shop_product_${productId}` }] // Кнопка "Back"
]
};
// Отправка сообщения с кнопками
const purchaseMessage = await bot.editMessageText(
- `🛒 Purchase Summary:\n\n` +
- `Product: ${product.name}\n` +
- `Quantity: ${quantity}\n` +
- `Total: $${totalPrice}\n`,
+ `${t('purchase.summary')}\n\n` +
+ `${t('purchase.product')}: ${product.name}\n` +
+ `${t('purchase.quantity')}: ${quantity}\n` +
+ `${t('purchase.total')}: $${totalPrice}\n`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -648,7 +713,10 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleBuyProduct');
- await editOrSendCallback(callbackQuery, 'Error processing purchase. Please try again.');
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await editOrSendCallback(callbackQuery, t('purchase.error_processing'));
}
}
@@ -658,23 +726,26 @@ export default class UserProductHandler {
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
const state = await userStates.get(chatId);
- if (!Validators.isValidWalletType(walletType)) {
- await editOrSendCallback(callbackQuery, 'Invalid wallet type.');
- return;
- }
- if (!Validators.isValidNumericId(Number(productId))) {
- await editOrSendCallback(callbackQuery, 'Invalid product.');
- return;
- }
- const qty = Number(quantity);
- if (!Number.isFinite(qty) || qty <= 0) {
- await editOrSendCallback(callbackQuery, 'Invalid quantity.');
- return;
- }
-
try {
- await UserService.recalculateUserBalanceByTelegramId(telegramId);
const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
+ if (!Validators.isValidWalletType(walletType)) {
+ await editOrSendCallback(callbackQuery, t('purchase.invalid_wallet'));
+ return;
+ }
+ if (!Validators.isValidNumericId(Number(productId))) {
+ await editOrSendCallback(callbackQuery, t('purchase.invalid_product'));
+ return;
+ }
+ const qty = Number(quantity);
+ if (!Number.isFinite(qty) || qty <= 0) {
+ await editOrSendCallback(callbackQuery, t('purchase.invalid_quantity'));
+ return;
+ }
+
+ await UserService.recalculateUserBalanceByTelegramId(telegramId);
if (!user) {
throw new Error('User not found');
@@ -690,7 +761,7 @@ export default class UserProductHandler {
if (totalPrice > balance) {
await userStates.delete(chatId);
- await bot.editMessageText(`Not enough money`, {
+ await bot.editMessageText(t('purchase.not_enough_money'), {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
});
@@ -699,7 +770,7 @@ export default class UserProductHandler {
// Проверка наличия товара
if (product.quantity_in_stock < quantity) {
- await editOrSendCallback(callbackQuery, `❌ Not enough items in stock. Only ${product.quantity_in_stock} available.`);
+ await editOrSendCallback(callbackQuery, t('purchase.not_enough_stock', { count: product.quantity_in_stock }));
return;
}
@@ -729,23 +800,23 @@ export default class UserProductHandler {
}
const message = `
- 📦 Purchase Details:
- Name: ${product.name}
- Quantity: ${quantity}
- Total: $${totalPrice}
- Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
- Category: ${category?.name || 'N/A'}
+ ${t('purchase.details')}
+ ${t('purchase.product')}: ${product.name}
+ ${t('purchase.quantity')}: ${quantity}
+ ${t('purchase.total')}: $${totalPrice}
+ ${t('purchase.location')}: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
+ ${t('purchase.category')}: ${category?.name || 'N/A'}
- 🔒 Private Information:
+ ${t('purchase.private_info')}
${product.private_data || 'N/A'}
- Hidden Location: ${product.hidden_description || 'N/A'}
- Coordinates: ${product.hidden_coordinates || 'N/A'}
+ ${t('purchase.hidden_location')}: ${product.hidden_description || 'N/A'}
+ ${t('purchase.coordinates')}: ${product.hidden_coordinates || 'N/A'}
`;
const keyboard = {
inline_keyboard: [
- [{ text: 'View new purchase', callback_data: `view_purchase_${purchaseId}` }], // Переход к покупке
- [{ text: "Contact support", url: config.SUPPORT_LINK }] // Сохранение кнопки "Contact support"
+ [{ text: t('purchase.view_purchase'), callback_data: `view_purchase_${purchaseId}` }], // Переход к покупке
+ [{ text: t('bot.contact_support'), url: config.SUPPORT_LINK }] // Сохранение кнопки "Contact support"
]
};
@@ -760,7 +831,10 @@ export default class UserProductHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handlePay');
- await editOrSendCallback(callbackQuery, 'Error processing purchase. Please try again.');
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await editOrSendCallback(callbackQuery, t('purchase.error_processing'));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/handlers/userHandlers/userPurchaseHandler.js b/src/handlers/userHandlers/userPurchaseHandler.js
index 7a6aa88..0fecb3f 100644
--- a/src/handlers/userHandlers/userPurchaseHandler.js
+++ b/src/handlers/userHandlers/userPurchaseHandler.js
@@ -16,6 +16,7 @@ import WalletService from "../../services/walletService.js";
import userStates from "../../context/userStates.js";
import Validators from '../../utils/validators.js';
import { editOrSendCallback, deleteAndSend } from '../../utils/messageUtils.js';
+import { tForUser } from '../../i18n/index.js';
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
const FALLBACK_PHOTO = path.join(process.cwd(), 'corrupt-photo.jpg');
@@ -42,70 +43,62 @@ async function sendProductPhoto(chatId, photoUrl, caption) {
}
export default class UserPurchaseHandler {
- static async viewPurchasePage(userId, page) {
+ static async viewPurchasePage(userId, page, t) {
try {
- const limit = 10; // Количество покупок на странице
+ const limit = 10;
const offset = page * limit;
- // Получаем покупки пользователя с учетом пагинации
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
-
- // Получаем общее количество покупок пользователя
const totalPurchases = await PurchaseService.getTotalPurchasesByUserId(userId);
-
- // Вычисляем общее количество страниц
const totalPages = Math.ceil(totalPurchases / limit);
- // Если покупок нет, возвращаем сообщение о пустом архиве
if (totalPurchases === 0) {
return {
- text: 'Your purchase history is empty.',
+ text: t('purchase.history_empty'),
markup: {
inline_keyboard: [
- [{ text: '🛍 Browse Products', callback_data: 'shop_start' }]
+ [{ text: t('purchase.browse_products'), callback_data: 'shop_start' }]
]
}
};
}
- // Если покупок нет на текущей странице, но это не первая страница, переходим на предыдущую страницу
if (purchases.length === 0 && page > 0) {
- return await this.viewPurchasePage(userId, page - 1);
+ return await this.viewPurchasePage(userId, page - 1, t);
}
const keyboard = {
inline_keyboard: [
...purchases.map(item => [{
- // Добавляем иконку статуса покупки
text: `${item.status === 'received' ? '✅' : '❌'} ${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
callback_data: `view_purchase_${item.id}`
}]),
[
{
- text: page > 0 ? `« Back (Page ${page})` : '« Back',
- callback_data: page > 0 ? `list_purchases_${page - 1}` : 'no_action', // Если на первой странице, то "no_action"
- hide: page === 0 // Скрываем кнопку "Назад", если на первой странице
+ text: page > 0 ? t('purchase.page_back', { page }) : '« Back',
+ callback_data: page > 0 ? `list_purchases_${page - 1}` : 'no_action',
+ hide: page === 0
},
{
- text: `Page ${page + 1} of ${totalPages}`,
+ text: t('purchase.page_info', { current: page + 1, total: totalPages }),
callback_data: 'current_page'
},
{
- text: page < totalPages - 1 ? `Next » (Page ${page + 2})` : 'Next »',
- callback_data: page < totalPages - 1 ? `list_purchases_${page + 1}` : 'no_action', // Если на последней странице, то "no_action"
- hide: page === totalPages - 1 // Скрываем кнопку "Вперед", если на последней странице
+ text: page < totalPages - 1 ? t('purchase.page_next', { page: page + 2 }) : 'Next »',
+ callback_data: page < totalPages - 1 ? `list_purchases_${page + 1}` : 'no_action',
+ hide: page === totalPages - 1
}
]
]
};
return {
- text: `📦 Select purchase to view detailed information (Page ${page + 1} of ${totalPages}):`,
+ text: t('purchase.select_purchase', { page: page + 1, total: totalPages }),
markup: keyboard
};
} catch (error) {
logger.error({ err: error }, 'Error in viewPurchasePage');
- return { text: 'Error loading purchase history. Please try again.' };
+ return { text: t('purchase.error_loading') };
}
}
@@ -116,13 +109,14 @@ export default class UserPurchaseHandler {
try {
const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
if (!user) {
- await bot.sendMessage(chatId, 'User not found.');
+ await bot.sendMessage(chatId, t('profile.not_found'));
return;
}
- // Удаляем сообщение с Hidden Photo, если оно существует
const state = await userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
@@ -132,7 +126,7 @@ export default class UserPurchaseHandler {
}
}
- const { text, markup } = await this.viewPurchasePage(user.id, page);
+ const { text, markup } = await this.viewPurchasePage(user.id, page, t);
await bot.editMessageText(text, {
chat_id: chatId,
@@ -141,11 +135,11 @@ export default class UserPurchaseHandler {
parse_mode: 'Markdown'
});
- // Удаляем состояние пользователя
await userStates.delete(chatId);
} catch (e) {
logger.error({ err: e }, 'Error in handlePurchaseListPage');
- await editOrSendCallback(callbackQuery, 'Error loading purchase history. Please try again.');
+ const t = tForUser('en');
+ await editOrSendCallback(callbackQuery, t('purchase.error_loading'));
}
}
@@ -155,18 +149,21 @@ export default class UserPurchaseHandler {
try {
const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
if (!user) {
- await bot.sendMessage(chatId, 'User not found.');
+ await bot.sendMessage(chatId, t('profile.not_found'));
return;
}
- const { text, markup } = await this.viewPurchasePage(user.id, 0);
+ const { text, markup } = await this.viewPurchasePage(user.id, 0, t);
await bot.sendMessage(chatId, text, { reply_markup: markup, parse_mode: 'Markdown' });
} catch (error) {
logger.error({ err: error }, 'Error in showPurchases');
- await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
+ const t = tForUser('en');
+ await bot.sendMessage(chatId, t('purchase.error_loading'));
}
}
@@ -175,26 +172,26 @@ export default class UserPurchaseHandler {
const purchaseId = callbackQuery.data.replace('view_purchase_', '');
try {
- // Получаем данные покупки
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
- await editOrSendCallback(callbackQuery, "No such purchase");
+ await editOrSendCallback(callbackQuery, t('purchase.no_such_purchase'));
return;
}
const product = await ProductService.getProductById(purchase.product_id);
if (!product) {
- await editOrSendCallback(callbackQuery, "No such product");
+ await editOrSendCallback(callbackQuery, t('purchase.no_such_product'));
return;
}
- // Получаем данные локации по location_id
const location = await LocationService.getLocationById(product.location_id);
-
- // Получаем данные категории по category_id
const category = await CategoryService.getCategoryById(product.category_id);
- // Удаляем старое сообщение с Hidden Photo, если оно существует
const state = await userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
@@ -204,44 +201,36 @@ export default class UserPurchaseHandler {
}
}
- // Отправляем Hidden Photo
let hiddenPhotoMessage;
if (product.hidden_photo_url) {
hiddenPhotoMessage = await sendProductPhoto(chatId, product.hidden_photo_url, 'Hidden photo');
}
- // Формируем сообщение с деталями покупки
const message = `
- 📦 Purchase Details:
- Name: ${product.name || 'N/A'}
- Quantity: ${purchase.quantity}
- Total: $${purchase.total_price}
- Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
- Category: ${category?.name || 'N/A'}
+ ${t('purchase.details')}
+ ${t('purchase.product')}: ${product.name || 'N/A'}
+ ${t('purchase.quantity')}: ${purchase.quantity}
+ ${t('purchase.total')}: $${purchase.total_price}
+ ${t('purchase.location')}: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
+ ${t('purchase.category')}: ${category?.name || 'N/A'}
- 🔒 Private Information:
+ ${t('purchase.private_info')}
${product.private_data || 'N/A'}
- Hidden Location: ${product.hidden_description || 'N/A'}
- Coordinates: ${product.hidden_coordinates || 'N/A'}
+ ${t('purchase.hidden_location')}: ${product.hidden_description || 'N/A'}
+ ${t('purchase.coordinates')}: ${product.hidden_coordinates || 'N/A'}
`;
- // Создаем клавиатуру с кнопками
const keyboard = {
inline_keyboard: [
- // Проверяем статус покупки перед добавлением кнопки "I've got it!"
- ...(purchase.status !== 'received' ? [[{ text: "I've got it!", callback_data: `confirm_received_${purchaseId}` }]] : []),
- [{ text: "« Back to Purchase List", callback_data: `list_purchases_0` }], // Кнопка "Назад к списку покупок"
- [{ text: "Contact support", url: config.SUPPORT_LINK }]
+ ...(purchase.status !== 'received' ? [[{ text: t('purchase.confirm_received'), callback_data: `confirm_received_${purchaseId}` }]] : []),
+ [{ text: t('purchase.back_to_list'), callback_data: `list_purchases_0` }],
+ [{ text: t('bot.contact_support'), url: config.SUPPORT_LINK }]
]
};
- // Отправляем сообщение с деталями покупки
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
-
- // Удаляем предыдущее сообщение
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
- // Сохраняем ID сообщения с Hidden Photo в состояние пользователя
await userStates.set(chatId, {
action: 'viewing_purchase',
purchaseId,
@@ -249,7 +238,8 @@ export default class UserPurchaseHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in viewPurchase');
- await editOrSendCallback(callbackQuery, 'Error loading purchase details. Please try again.');
+ const t = tForUser('en');
+ await editOrSendCallback(callbackQuery, t('purchase.error_loading_details'));
}
}
@@ -259,49 +249,45 @@ export default class UserPurchaseHandler {
const purchaseId = callbackQuery.data.replace('confirm_received_', '');
try {
- // Получаем данные покупки
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const purchase = await PurchaseService.getPurchaseById(purchaseId);
if (!purchase) {
- await editOrSendCallback(callbackQuery, "Purchase not found.");
+ await editOrSendCallback(callbackQuery, t('purchase.no_such_purchase'));
return;
}
- // Получаем данные пользователя по user_id из покупки
- const user = await UserService.getUserByUserId(purchase.user_id);
- if (!user) {
- await editOrSendCallback(callbackQuery, 'User not found.');
+ const purchaseUser = await UserService.getUserByUserId(purchase.user_id);
+ if (!purchaseUser) {
+ await editOrSendCallback(callbackQuery, t('profile.not_found'));
return;
}
- // Обновляем статус покупки в базе данных
await PurchaseService.updatePurchaseStatus(purchaseId, 'received');
- // Добавляем запись в таблицу transactions
await db.runAsync(
`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount, created_at)
VALUES (?, ?, ?, ?, ?)`,
[
- user.id, // ID пользователя
- purchase.wallet_type, // Источник списания (например, "bonus_50, crypto_30")
- purchase.tx_hash || 'no_hash', // Хеш транзакции (если не указан, то "no_hash")
- purchase.total_price, // Сумма транзакции
- new Date().toISOString() // Дата создания транзакции
+ purchaseUser.id,
+ purchase.wallet_type,
+ purchase.tx_hash || 'no_hash',
+ purchase.total_price,
+ new Date().toISOString()
]
);
- // Отправляем уведомление администраторам
- const adminIds = config.ADMIN_IDS; // Используем массив ADMIN_IDS
+ const adminIds = config.ADMIN_IDS;
for (const adminId of adminIds) {
- await bot.sendMessage(adminId, `User ${callbackQuery.from.username} has confirmed receiving purchase #${purchaseId}.`);
+ await bot.sendMessage(adminId, t('purchase.admin_notification', { username: callbackQuery.from.username, purchaseId }));
}
- // Уведомляем пользователя
- await bot.sendMessage(chatId, "Thank you! Your purchase has been marked as received.");
-
- // Удаляем сообщение с карточкой товара
+ await bot.sendMessage(chatId, t('purchase.purchase_received'));
await bot.deleteMessage(chatId, messageId);
- // Удаляем Hidden Photo, если оно существует
const state = await userStates.get(chatId);
if (state?.hiddenPhotoMessageId) {
try {
@@ -311,14 +297,12 @@ export default class UserPurchaseHandler {
}
}
- // Удаляем состояние пользователя
await userStates.delete(chatId);
-
- // Открываем список покупок для пользователя
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
} catch (error) {
logger.error({ err: error }, 'Error in handleConfirmReceived');
- await editOrSendCallback(callbackQuery, 'Error confirming receipt. Please try again.');
+ const t = tForUser('en');
+ await editOrSendCallback(callbackQuery, t('purchase.error_confirming'));
}
}
}
diff --git a/src/handlers/userHandlers/wallet/archiveHandler.js b/src/handlers/userHandlers/wallet/archiveHandler.js
index e876329..1118283 100644
--- a/src/handlers/userHandlers/wallet/archiveHandler.js
+++ b/src/handlers/userHandlers/wallet/archiveHandler.js
@@ -4,15 +4,18 @@ import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class ArchiveHandler {
static async handleViewArchivedWallets(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
- try {
- const user = await UserService.getUserByTelegramId(telegramId.toString());
+ const user = await UserService.getUserByTelegramId(telegramId.toString());
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ try {
const archivedWallets = await db.allAsync(`
SELECT wallet_type, address FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%' ORDER BY wallet_type
@@ -24,9 +27,9 @@ export default class ArchiveHandler {
});
if (validArchivedWallets.length === 0) {
- await bot.editMessageText('No archived wallets found.', {
+ await bot.editMessageText(t('wallet.no_archived_wallets'), {
chat_id: chatId, message_id: callbackQuery.message.message_id,
- reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
return;
}
@@ -46,7 +49,7 @@ export default class ArchiveHandler {
);
const balances = await walletUtilsInstance.getAllBalances();
- let message = '📁 *Archived Wallets:*\n\n';
+ let message = `${t('wallet.archived_wallets_title')}\n\n`;
let totalUsdValue = 0;
for (const baseType of Object.keys(groupedWallets).sort()) {
@@ -62,28 +65,28 @@ export default class ArchiveHandler {
typeUsdTotal += usdValue;
const date = new Date(wallet.timestamp);
- message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
- message += `├ Value: $${usdValue.toFixed(2)}\n`;
- message += `├ Address: \`${wallet.address}\`\n`;
- message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
+ message += `├ ${t('wallet.balance')}: ${balance.toFixed(8)} ${baseType}\n`;
+ message += `├ ${t('wallet.value')}: $${usdValue.toFixed(2)}\n`;
+ message += `├ ${t('wallet.address')}: \`${wallet.address}\`\n`;
+ message += `└ ${t('wallet.archived_date')}: ${date.toLocaleDateString()}\n\n`;
}
- message += `📊 *Total ${baseType}*:\n`;
- message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
- message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
+ message += `${t('wallet.total_type', { type: baseType })}:\n`;
+ message += `├ ${t('wallet.amount')}: ${typeTotal.toFixed(8)} ${baseType}\n`;
+ message += `└ ${t('wallet.value')}: $${typeUsdTotal.toFixed(2)}\n\n`;
totalUsdValue += typeUsdTotal;
}
- message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
+ message += `${t('wallet.total_archived_value')} $${totalUsdValue.toFixed(2)}`;
await bot.editMessageText(message, {
chat_id: chatId, message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
- reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
} catch (error) {
logger.error({ err: error }, 'Error in handleViewArchivedWallets');
- await editOrSendCallback(callbackQuery, 'Error loading archived wallets. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_loading_archived'));
}
}
}
\ No newline at end of file
diff --git a/src/handlers/userHandlers/wallet/balanceHandler.js b/src/handlers/userHandlers/wallet/balanceHandler.js
index f2a8824..2bab636 100644
--- a/src/handlers/userHandlers/wallet/balanceHandler.js
+++ b/src/handlers/userHandlers/wallet/balanceHandler.js
@@ -5,16 +5,20 @@ import WalletService from '../../../services/walletService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class BalanceHandler {
static async showBalance(msg) {
const chatId = msg.chat.id;
const telegramId = msg.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId.toString());
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId.toString());
if (!user) {
- await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
+ await bot.sendMessage(chatId, t('wallet.profile_not_found'));
return;
}
@@ -27,7 +31,7 @@ export default class BalanceHandler {
ORDER BY wallet_type
`, [updatedUser.id]);
- let message = '💰 *Your Active Wallets:*\n\n';
+ let message = `${t('wallet.your_active_wallets')}\n\n`;
if (cryptoWallets.length > 0) {
const walletUtilsInstance = new WalletUtils(
@@ -46,46 +50,46 @@ export default class BalanceHandler {
const wallet = cryptoWallets.find(w => w.wallet_type === type.split(' ')[0]);
if (wallet) {
message += `🔐 *${type}*\n`;
- message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
- message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
- message += `└ Address: \`${wallet.address}\`\n\n`;
+ message += `├ ${t('wallet.balance')}: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
+ message += `├ ${t('wallet.value')}: $${balance.usdValue.toFixed(2)}\n`;
+ message += `└ ${t('wallet.address')}: \`${wallet.address}\`\n\n`;
totalUsdValue += balance.usdValue;
}
}
- message += `📊 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`;
- message += `🎁 *Bonus Balance:* $${updatedUser.bonus_balance.toFixed(2)}\n`;
+ message += `${t('wallet.total_crypto_balance')} $${totalUsdValue.toFixed(2)}\n`;
+ message += `${t('wallet.bonus_balance_label')} $${updatedUser.bonus_balance.toFixed(2)}\n`;
const availableBalance = updatedUser.bonus_balance + (updatedUser.total_balance || 0);
- message += `💰 *Available Balance:* $${availableBalance.toFixed(2)}\n`;
+ message += `${t('wallet.available_balance_label')} $${availableBalance.toFixed(2)}\n`;
} else {
- message = 'You don\'t have any active wallets yet.';
+ message = t('wallet.no_active_wallets');
}
const archivedCount = await WalletService.getArchivedWalletsCount(updatedUser);
const keyboard = {
inline_keyboard: [
[
- { text: '➕ Add Crypto Wallet', callback_data: 'add_wallet' },
- { text: '💸 Top Up', callback_data: 'top_up_wallet' }
+ { text: t('wallet.add_crypto_wallet'), callback_data: 'add_wallet' },
+ { text: t('wallet.top_up'), callback_data: 'top_up_wallet' }
],
- [{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }]
+ [{ text: t('wallet.refresh_balance'), callback_data: 'refresh_balance' }]
]
};
if (archivedCount > 0) {
keyboard.inline_keyboard.splice(2, 0, [
- { text: `📁 Archived Wallets (${archivedCount})`, callback_data: 'view_archived_wallets' }
+ { text: t('wallet.archived_wallets_count', { count: archivedCount }), callback_data: 'view_archived_wallets' }
]);
}
keyboard.inline_keyboard.splice(3, 0, [
- { text: '📊 Transaction History', callback_data: 'view_transaction_history_0' }
+ { text: t('wallet.transaction_history'), callback_data: 'view_transaction_history_0' }
]);
await bot.sendMessage(chatId, message, { reply_markup: keyboard, parse_mode: 'Markdown' });
} catch (error) {
logger.error({ err: error }, 'Error in showBalance');
- await bot.sendMessage(chatId, 'Error loading balance. Please try again.');
+ await bot.sendMessage(chatId, t('wallet.error_loading_balance'));
}
}
diff --git a/src/handlers/userHandlers/wallet/createHandler.js b/src/handlers/userHandlers/wallet/createHandler.js
index 106b308..f03efd1 100644
--- a/src/handlers/userHandlers/wallet/createHandler.js
+++ b/src/handlers/userHandlers/wallet/createHandler.js
@@ -6,10 +6,16 @@ import UserService from '../../../services/userService.js';
import logger from '../../../utils/logger.js';
import WalletHelpers from './helpers.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class CreateHandler {
static async handleAddWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const cryptoOptions = [['BTC', 'ETH', 'LTC'], ['USDT', 'USDC']];
const keyboard = {
@@ -20,11 +26,11 @@ export default class CreateHandler {
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
}))
),
- [{ text: '« Back', callback_data: 'back_to_balance' }]
+ [{ text: t('wallet.back'), callback_data: 'back_to_balance' }]
]
};
- await bot.editMessageText('🔐 Select cryptocurrency to generate wallet:', {
+ await bot.editMessageText(t('wallet.select_crypto'), {
chat_id: chatId, message_id: callbackQuery.message.message_id,
reply_markup: keyboard
});
@@ -35,14 +41,17 @@ export default class CreateHandler {
const telegramId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
if (!Validators.isValidWalletType(walletType)) {
- await editOrSendCallback(callbackQuery, 'Invalid wallet type.');
+ await editOrSendCallback(callbackQuery, t('wallet.invalid_wallet_type'));
return;
}
try {
- const user = await UserService.getUserByTelegramId(telegramId);
- if (!user) throw new Error('User not found');
+ if (!user) throw new Error(t('wallet.user_not_found'));
await db.runAsync('BEGIN TRANSACTION');
@@ -63,21 +72,21 @@ export default class CreateHandler {
const walletResult = await WalletService.createWallet(user.id, walletType);
if (!walletResult?.address) throw new Error('Failed to generate wallet address');
- const network = WalletHelpers.getNetworkName(walletType);
+ const network = WalletHelpers.getNetworkName(walletType, t);
- let message = `✅ New wallet generated successfully!\n\n`;
- message += `Type: ${walletType}\nNetwork: ${network}\n`;
- message += `Address: \`${walletResult.address}\`\n\n`;
+ let message = `${t('wallet.wallet_generated')}\n\n`;
+ message += `${t('wallet.wallet_type')}: ${walletType}\n${t('wallet.network')}: ${network}\n`;
+ message += `${t('wallet.address')}: \`${walletResult.address}\`\n\n`;
if (existingWallet) {
- message += `ℹ️ Your previous wallet has been archived.\n`;
+ message += `${t('wallet.previous_archived')}\n`;
}
- message += `\n⚠️ Important: Your recovery phrase has been securely stored.`;
+ message += `\n${t('wallet.recovery_stored')}`;
await bot.editMessageText(message, {
chat_id: chatId, message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
- reply_markup: { inline_keyboard: [[{ text: '« Back to Balance', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back_to_balance'), callback_data: 'back_to_balance' }]] }
});
await db.runAsync('COMMIT');
@@ -87,9 +96,9 @@ export default class CreateHandler {
}
} catch (error) {
logger.error({ err: error }, 'Error generating wallet');
- await bot.editMessageText('❌ Error generating wallet. Please try again.', {
+ await bot.editMessageText(t('wallet.error_generating'), {
chat_id: chatId, message_id: callbackQuery.message.message_id,
- reply_markup: { inline_keyboard: [[{ text: '« Back to Balance', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back_to_balance'), callback_data: 'back_to_balance' }]] }
});
}
}
diff --git a/src/handlers/userHandlers/wallet/depositHandler.js b/src/handlers/userHandlers/wallet/depositHandler.js
index 6e15ecf..b6ecf6b 100644
--- a/src/handlers/userHandlers/wallet/depositHandler.js
+++ b/src/handlers/userHandlers/wallet/depositHandler.js
@@ -4,6 +4,7 @@ import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
const DEPOSIT_AMOUNTS = [25, 50, 100, 250, 500];
@@ -28,10 +29,13 @@ export default class DepositHandler {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
- await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
+ await editOrSendCallback(callbackQuery, t('wallet.profile_not_found'));
return;
}
@@ -42,14 +46,14 @@ export default class DepositHandler {
if (cryptoWallets.length === 0) {
await bot.editMessageText(
- '❌ You don\'t have any wallets yet. Create one first.',
+ t('wallet.no_wallets_prefix'),
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [
- [{ text: '➕ Add Wallet', callback_data: 'add_wallet' }],
- [{ text: '« Back', callback_data: 'back_to_balance' }]
+ [{ text: t('purchase.add_wallet'), callback_data: 'add_wallet' }],
+ [{ text: t('wallet.back'), callback_data: 'back_to_balance' }]
]
}
}
@@ -65,10 +69,10 @@ export default class DepositHandler {
}];
});
- walletButtons.push([{ text: '« Back', callback_data: 'back_to_balance' }]);
+ walletButtons.push([{ text: t('wallet.back'), callback_data: 'back_to_balance' }]);
await bot.editMessageText(
- '💳 *Deposit via ChangeNOW*\n\nSelect the wallet you want to top up:',
+ t('wallet.deposit_changenow_select'),
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -78,23 +82,28 @@ export default class DepositHandler {
);
} catch (error) {
logger.error({ err: error }, 'Error in handleDepositSelectWallet');
- await editOrSendCallback(callbackQuery, 'Error loading wallets. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_loading'));
}
}
static async handleDepositSelectAmount(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
+ const telegramId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('deposit_wallet_', '');
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
const amountButtons = DEPOSIT_AMOUNTS.map(amount => ([{
text: `$${amount}`,
callback_data: `deposit_amount_${walletType}_${amount}`
}]));
- amountButtons.push([{ text: '« Back', callback_data: 'top_up_wallet' }]);
+ amountButtons.push([{ text: t('wallet.back'), callback_data: 'top_up_wallet' }]);
await bot.editMessageText(
- `💳 *Deposit ${walletType}*\n\nSelect the amount (USD) you want to deposit:`,
+ t('wallet.deposit_select_amount', { type: walletType }),
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
@@ -111,10 +120,13 @@ export default class DepositHandler {
const walletType = parts[0];
const amount = parts[1];
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
- await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
+ await editOrSendCallback(callbackQuery, t('wallet.profile_not_found'));
return;
}
@@ -124,7 +136,7 @@ export default class DepositHandler {
);
if (!wallet) {
- await editOrSendCallback(callbackQuery, 'Wallet not found. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.wallet_not_found'));
return;
}
@@ -132,31 +144,31 @@ export default class DepositHandler {
const refId = config.CHANGENOW_REF;
const changenowUrl = `https://changenow.io/exchange?from=eur&to=${changenowTo}&fiatMode=true&amount=${amount}${refId ? `&ref_id=${refId}` : ''}`;
- let message = `💳 *Deposit ${walletType} — €${amount}*\n\n`;
- message += `📋 *Step\\-by\\-step instructions:*\n\n`;
- message += `1️⃣ Tap *Copy Address* below to copy your wallet address\n`;
- message += `2️⃣ Tap *Open ChangeNOW* \\— the amount €${amount} and currency ${walletType} are already set\n`;
- message += `3️⃣ On ChangeNOW\\, paste the copied address as the receiving wallet\n`;
- message += `4️⃣ Enter your email and create a password when prompted \\— this is *required by law* for card payments \\(KYC verification\\)\\. Your data is protected by ChangeNOW\\'s security\n`;
- message += `5️⃣ Pay with your bank card \\(Visa\\/Mastercard\\)\n`;
- message += `6️⃣ Crypto will arrive in your wallet within 5\\-30 minutes\n\n`;
- message += `🔐 *Your ${walletType} wallet address:*\n`;
+ let message = `${t('wallet.deposit_title', { type: walletType, amount })}\n\n`;
+ message += `${t('wallet.deposit_instructions_title')}\n\n`;
+ message += `${t('wallet.deposit_step1')}\n`;
+ message += `${t('wallet.deposit_step2', { amount, type: walletType })}\n`;
+ message += `${t('wallet.deposit_step3')}\n`;
+ message += `${t('wallet.deposit_step4')}\n`;
+ message += `${t('wallet.deposit_step5')}\n`;
+ message += `${t('wallet.deposit_step6')}\n\n`;
+ message += `${t('wallet.deposit_your_address', { type: walletType })}\n`;
message += `\`${wallet.address}\`\n\n`;
- message += `⚠️ *Important:*\n`;
- message += `• Double\\-check the wallet address before confirming\n`;
- message += `• Email \\+ password on ChangeNOW is a standard verification step for card payments \\— don\\'t worry\\, it\\'s safe\n`;
- message += `• If crypto doesn\\'t arrive within 30 min \\— check the transaction status in your ChangeNOW email confirmation`;
+ message += `${t('wallet.deposit_important_title')}\n`;
+ message += `${t('wallet.deposit_important1')}\n`;
+ message += `${t('wallet.deposit_important2')}\n`;
+ message += `${t('wallet.deposit_important3')}`;
const keyboard = {
inline_keyboard: [
- [{ text: `🌐 Open ChangeNOW — €${amount} → ${walletType}`, url: changenowUrl }],
+ [{ text: t('wallet.deposit_open_changenow', { amount, type: walletType }), url: changenowUrl }],
[
- { text: '📋 Copy Address', callback_data: `deposit_copy_${walletType}` },
- { text: '🔄 Change Amount', callback_data: `deposit_wallet_${walletType}` }
+ { text: t('wallet.deposit_copy_address'), callback_data: `deposit_copy_${walletType}` },
+ { text: t('wallet.deposit_change_amount'), callback_data: `deposit_wallet_${walletType}` }
],
[
- { text: '💸 Choose Different Wallet', callback_data: 'top_up_wallet' },
- { text: '« Back to Balance', callback_data: 'back_to_balance' }
+ { text: t('wallet.deposit_choose_different'), callback_data: 'top_up_wallet' },
+ { text: t('wallet.back_to_balance'), callback_data: 'back_to_balance' }
]
]
};
@@ -169,7 +181,7 @@ export default class DepositHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleDepositInstruction');
- await editOrSendCallback(callbackQuery, 'Error creating deposit instructions. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_deposit_instructions'));
}
}
@@ -178,10 +190,13 @@ export default class DepositHandler {
const telegramId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('deposit_copy_', '');
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
- await bot.answerCallbackQuery(callbackQuery.id, { text: 'Profile not found.' });
+ await bot.answerCallbackQuery(callbackQuery.id, { text: t('wallet.profile_not_found_short') });
return;
}
@@ -191,25 +206,25 @@ export default class DepositHandler {
);
if (!wallet) {
- await bot.answerCallbackQuery(callbackQuery.id, { text: 'Wallet not found.' });
+ await bot.answerCallbackQuery(callbackQuery.id, { text: t('wallet.wallet_not_found_short') });
return;
}
- await bot.sendMessage(chatId, `${walletType} wallet address:\n\n\`${wallet.address}\``, {
+ await bot.sendMessage(chatId, `${t('wallet.deposit_wallet_address', { type: walletType })}\n\n\`${wallet.address}\``, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
- [{ text: '« Back to Deposit', callback_data: `deposit_wallet_${walletType}` }]
+ [{ text: t('wallet.back_to_deposit'), callback_data: `deposit_wallet_${walletType}` }]
]
}
});
await bot.answerCallbackQuery(callbackQuery.id, {
- text: `📋 ${walletType} address sent! Copy it from the message below.`
+ text: t('wallet.deposit_address_sent', { type: walletType })
});
} catch (error) {
logger.error({ err: error }, 'Error in handleDepositCopyAddress');
- await bot.answerCallbackQuery(callbackQuery.id, { text: 'Error copying address.' });
+ await bot.answerCallbackQuery(callbackQuery.id, { text: t('wallet.error_copying_address') });
}
}
}
\ No newline at end of file
diff --git a/src/handlers/userHandlers/wallet/helpers.js b/src/handlers/userHandlers/wallet/helpers.js
index b6b348c..cb81208 100644
--- a/src/handlers/userHandlers/wallet/helpers.js
+++ b/src/handlers/userHandlers/wallet/helpers.js
@@ -1,13 +1,13 @@
import WalletUtils from '../../../utils/walletUtils.js';
export default class WalletHelpers {
- static getNetworkName(walletType) {
- if (walletType.includes('USDT')) return 'Ethereum Network (ERC-20)';
- if (walletType.includes('USDC')) return 'Ethereum Network (ERC-20)';
- if (walletType === 'BTC') return 'Bitcoin Network';
- if (walletType === 'LTC') return 'Litecoin Network';
- if (walletType === 'ETH') return 'Ethereum Network';
- return 'Unknown Network';
+ static getNetworkName(walletType, t) {
+ if (walletType.includes('USDT')) return t ? t('wallet.network_erc20') : 'Ethereum Network (ERC-20)';
+ if (walletType.includes('USDC')) return t ? t('wallet.network_erc20') : 'Ethereum Network (ERC-20)';
+ if (walletType === 'BTC') return t ? t('wallet.network_btc') : 'Bitcoin Network';
+ if (walletType === 'LTC') return t ? t('wallet.network_ltc') : 'Litecoin Network';
+ if (walletType === 'ETH') return t ? t('wallet.network_eth') : 'Ethereum Network';
+ return t ? t('wallet.network_unknown') : 'Unknown Network';
}
static getWalletAddress(wallets, walletType) {
diff --git a/src/handlers/userHandlers/wallet/historyHandler.js b/src/handlers/userHandlers/wallet/historyHandler.js
index 24c6bed..4af75f2 100644
--- a/src/handlers/userHandlers/wallet/historyHandler.js
+++ b/src/handlers/userHandlers/wallet/historyHandler.js
@@ -3,16 +3,20 @@ import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class HistoryHandler {
static async handleTransactionHistory(callbackQuery, page = 0) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId.toString());
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId.toString());
if (!user) {
- await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
+ await editOrSendCallback(callbackQuery, t('wallet.profile_not_found'));
return;
}
@@ -27,23 +31,23 @@ export default class HistoryHandler {
let message = '';
if (transactions.length > 0) {
- message = '📊 *Transaction History:*\n\n';
+ message = `${t('wallet.transaction_history_title')}\n\n`;
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
- message += `💰 Amount: ${tx.amount}\n`;
- message += `🔗 TX Hash: \`${tx.tx_hash}\`\n`;
- message += `🕒 Date: ${date}\n`;
- message += `💼 Wallet Type: ${tx.wallet_type}\n\n`;
+ message += `${t('wallet.tx_amount')}: ${tx.amount}\n`;
+ message += `${t('wallet.tx_hash')}: \`${tx.tx_hash}\`\n`;
+ message += `${t('wallet.tx_date')}: ${date}\n`;
+ message += `${t('wallet.tx_wallet_type')}: ${tx.wallet_type}\n\n`;
});
} else {
- message = '📊 *Transaction History:*\n\nNo transactions found.';
+ message = `${t('wallet.transaction_history_title')}\n\n${t('wallet.no_transactions')}`;
}
- const keyboard = { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] };
+ const keyboard = { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] };
if (page > 0) {
keyboard.inline_keyboard.unshift([
- { text: '⬅️ Previous', callback_data: `view_transaction_history_${page - 1}` }
+ { text: t('wallet.previous_page'), callback_data: `view_transaction_history_${page - 1}` }
]);
}
@@ -54,7 +58,7 @@ export default class HistoryHandler {
if (nextTransactions.length > 0) {
keyboard.inline_keyboard.push([
- { text: '➡️ Next', callback_data: `view_transaction_history_${page + 1}` }
+ { text: t('wallet.next_page'), callback_data: `view_transaction_history_${page + 1}` }
]);
}
@@ -64,7 +68,7 @@ export default class HistoryHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleTransactionHistory');
- await editOrSendCallback(callbackQuery, 'Error loading transaction history. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_loading_history'));
}
}
@@ -72,8 +76,11 @@ export default class HistoryHandler {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId);
const transactions = await db.allAsync(`
SELECT type, amount, tx_hash, created_at, wallet_type
FROM transactions WHERE user_id = ?
@@ -81,30 +88,30 @@ export default class HistoryHandler {
`, [user.id]);
if (transactions.length === 0) {
- await bot.editMessageText('No transactions found.', {
+ await bot.editMessageText(t('wallet.no_transactions'), {
chat_id: chatId, message_id: callbackQuery.message.message_id,
- reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
return;
}
- let message = '📊 *Recent Transactions:*\n\n';
+ let message = `${t('wallet.recent_transactions')}\n\n`;
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
const symbol = tx.type === 'deposit' ? '➕' : '➖';
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
- message += `🔗 TX: \`${tx.tx_hash}\`\n`;
+ message += `${t('wallet.tx_hash')}: \`${tx.tx_hash}\`\n`;
message += `🕒 ${date}\n\n`;
});
await bot.editMessageText(message, {
chat_id: chatId, message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
- reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
} catch (error) {
logger.error({ err: error }, 'Error in handleWalletHistory');
- await editOrSendCallback(callbackQuery, 'Error loading transaction history. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_loading_history'));
}
}
}
\ No newline at end of file
diff --git a/src/handlers/userHandlers/wallet/refreshHandler.js b/src/handlers/userHandlers/wallet/refreshHandler.js
index 78f976a..028d537 100644
--- a/src/handlers/userHandlers/wallet/refreshHandler.js
+++ b/src/handlers/userHandlers/wallet/refreshHandler.js
@@ -4,18 +4,22 @@ import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class RefreshHandler {
static async handleRefreshBalance(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
- try {
- await bot.answerCallbackQuery(callbackQuery.id, { text: '🔄 Refreshing balances...' });
+ const user = await UserService.getUserByTelegramId(callbackQuery.from.id.toString());
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
+ try {
+ await bot.answerCallbackQuery(callbackQuery.id, { text: t('wallet.refreshing_balances') });
- const user = await UserService.getUserByTelegramId(callbackQuery.from.id.toString());
if (!user) {
- await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
+ await editOrSendCallback(callbackQuery, t('wallet.profile_not_found'));
return;
}
@@ -69,9 +73,9 @@ export default class RefreshHandler {
await bot.deleteMessage(chatId, messageId);
} catch (error) {
logger.error({ err: error }, 'Error in handleRefreshBalance');
- await bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Error refreshing balances.' });
- await editOrSendCallback(callbackQuery, '❌ Error refreshing balances. Please try again.', {
- reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ await bot.answerCallbackQuery(callbackQuery.id, { text: t('wallet.error_refreshing_balances') });
+ await editOrSendCallback(callbackQuery, t('wallet.error_refreshing_balances_retry'), {
+ reply_markup: { inline_keyboard: [[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
}
}
diff --git a/src/handlers/userHandlers/wallet/topUpHandler.js b/src/handlers/userHandlers/wallet/topUpHandler.js
index d25c2ed..c7e5275 100644
--- a/src/handlers/userHandlers/wallet/topUpHandler.js
+++ b/src/handlers/userHandlers/wallet/topUpHandler.js
@@ -4,16 +4,20 @@ import UserService from '../../../services/userService.js';
import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js';
+import { tForUser } from '../../../i18n/index.js';
export default class TopUpHandler {
static async handleTopUpWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
try {
- const user = await UserService.getUserByTelegramId(telegramId);
if (!user) {
- await editOrSendCallback(callbackQuery, 'Profile not found. Please use /start to create one.');
+ await editOrSendCallback(callbackQuery, t('wallet.profile_not_found'));
return;
}
@@ -24,9 +28,9 @@ export default class TopUpHandler {
`, [user.id]);
if (cryptoWallets.length === 0) {
- await bot.editMessageText('You don\'t have any wallets yet. Create one first.', {
+ await bot.editMessageText(t('wallet.no_wallets'), {
chat_id: chatId, message_id: callbackQuery.message.message_id,
- reply_markup: { inline_keyboard: [[{ text: '➕ Add Wallet', callback_data: 'add_wallet' }], [{ text: '« Back', callback_data: 'back_to_balance' }]] }
+ reply_markup: { inline_keyboard: [[{ text: t('purchase.add_wallet'), callback_data: 'add_wallet' }], [{ text: t('wallet.back'), callback_data: 'back_to_balance' }]] }
});
return;
}
@@ -43,28 +47,28 @@ export default class TopUpHandler {
const balances = await walletUtilsInstance.getAllBalancesFromDB();
- let message = '💰 *Your Wallets*\n\n';
+ let message = `${t('wallet.your_wallets')}\n\n`;
for (const wallet of cryptoWallets) {
const balanceData = balances[wallet.wallet_type];
const amount = balanceData ? balanceData.amount.toFixed(8) : '0.00000000';
const usdValue = balanceData ? balanceData.usdValue.toFixed(2) : '0.00';
message += `🔐 *${wallet.wallet_type}*\n`;
- message += `├ Balance: ${amount} ${wallet.wallet_type}\n`;
- message += `├ Value: $${usdValue}\n`;
- message += `└ Address: \`${wallet.address}\`\n\n`;
+ message += `├ ${t('wallet.balance')}: ${amount} ${wallet.wallet_type}\n`;
+ message += `├ ${t('wallet.value')}: $${usdValue}\n`;
+ message += `└ ${t('wallet.address')}: \`${wallet.address}\`\n\n`;
}
const walletButtons = cryptoWallets.map(w => ([{
- text: `💳 Deposit ${w.wallet_type}`,
+ text: t('wallet.deposit', { type: w.wallet_type }),
callback_data: `deposit_wallet_${w.wallet_type}`
}]));
const keyboard = {
inline_keyboard: [
- [{ text: '💳 Deposit via ChangeNOW', callback_data: 'deposit_select_wallet' }],
+ [{ text: t('wallet.deposit_via_changenow'), callback_data: 'deposit_select_wallet' }],
...walletButtons,
- [{ text: '« Back', callback_data: 'back_to_balance' }]
+ [{ text: t('wallet.back'), callback_data: 'back_to_balance' }]
]
};
@@ -75,7 +79,7 @@ export default class TopUpHandler {
});
} catch (error) {
logger.error({ err: error }, 'Error in handleTopUpWallet');
- await editOrSendCallback(callbackQuery, 'Error loading wallets. Please try again.');
+ await editOrSendCallback(callbackQuery, t('wallet.error_loading'));
}
}
}
\ No newline at end of file
diff --git a/src/i18n/index.js b/src/i18n/index.js
new file mode 100644
index 0000000..cf80ab6
--- /dev/null
+++ b/src/i18n/index.js
@@ -0,0 +1,66 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const AVAILABLE_LANGUAGES = ['en', 'es', 'de'];
+
+const LANGUAGE_NAMES = {
+ en: '🇬🇧 English',
+ es: '🇪🇸 Español',
+ de: '🇩🇪 Deutsch'
+};
+
+const DEFAULT_LOCALE = 'en';
+let currentLocale = DEFAULT_LOCALE;
+
+const locales = {};
+for (const lang of AVAILABLE_LANGUAGES) {
+ const filePath = path.join(__dirname, 'locales', `${lang}.json`);
+ try {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ locales[lang] = JSON.parse(content);
+ } catch (err) {
+ console.error(`[i18n] Failed to load locale "${lang}" from ${filePath}: ${err.message}. Using empty object as fallback.`);
+ locales[lang] = {};
+ }
+}
+
+function getNestedValue(obj, keyPath) {
+ return keyPath.split('.').reduce((o, k) => o?.[k], obj);
+}
+
+function t(key, params = {}) {
+ return tForLang(currentLocale, key, params);
+}
+
+function tForLang(lang, key, params = {}) {
+ let value = getNestedValue(locales[lang], key)
+ || getNestedValue(locales[DEFAULT_LOCALE], key)
+ || key;
+
+ if (typeof value === 'string') {
+ for (const [paramKey, paramValue] of Object.entries(params)) {
+ value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), paramValue);
+ }
+ }
+
+ return value;
+}
+
+function tForUser(lang) {
+ return (key, params = {}) => tForLang(lang || currentLocale, key, params);
+}
+
+function setLocale(lang) {
+ if (AVAILABLE_LANGUAGES.includes(lang)) {
+ currentLocale = lang;
+ }
+}
+
+function getLocale() {
+ return currentLocale;
+}
+
+export { t, tForLang, tForUser, setLocale, getLocale, AVAILABLE_LANGUAGES, LANGUAGE_NAMES };
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
new file mode 100644
index 0000000..14e0958
--- /dev/null
+++ b/src/i18n/locales/de.json
@@ -0,0 +1,219 @@
+{
+ "bot": {
+ "welcome": "Willkommen im Shop! Wähle eine Option:",
+ "language_select": "🌍 Bitte wähle deine Sprache:",
+ "language_changed": "✅ Sprache geändert zu {{language}}!",
+ "error_generic": "Fehler bei der Verarbeitung. Bitte versuche es erneut.",
+ "account_blocked": "⚠️ Dein Konto wurde vom Administrator gesperrt",
+ "account_deleted": "⚠️ Dein Konto wurde vom Administrator gelöscht",
+ "contact_support": "Support kontaktieren"
+ },
+ "profile": {
+ "title": "👤 *Dein Profil*",
+ "not_found": "Profil nicht gefunden. Bitte verwende /start um eines zu erstellen.",
+ "telegram_id": "📱 Telegram ID",
+ "location": "📍 Standort",
+ "location_not_set": "Nicht festgelegt",
+ "stats": "📊 Statistiken:",
+ "total_purchases": "Gesammte Käufe",
+ "total_spent": "Gesamtausgaben",
+ "active_wallets": "Aktive Wallets",
+ "archived_wallets": "Archivierte Wallets",
+ "bonus_balance": "Bonus-Guthaben",
+ "available_balance": "Verfügbares Guthaben",
+ "member_since": "📅 Mitglied seit",
+ "set_location": "📍 Standort festlegen",
+ "delete_account": "❌ Konto löschen",
+ "error_loading": "Fehler beim Laden des Profils. Bitte versuche es erneut."
+ },
+ "products": {
+ "select_country": "🌍 Wähle dein Land:",
+ "select_city": "🏙 Wähle eine Stadt in {{country}}:",
+ "select_district": "📍 Wähle einen Bezirk in {{city}}:",
+ "select_category": "📦 Wähle eine Kategorie:",
+ "select_product": "Wähle ein Produkt:",
+ "no_products": "Aktuell keine Produkte verfügbar.",
+ "no_products_category": "Keine Produkte in dieser Kategorie.",
+ "no_products_subcategory": "Keine Produkte in dieser Unterkategorie.",
+ "back_to_countries": "« Zurück zu den Ländern",
+ "back_to_cities": "« Zurück zu den Städten",
+ "back_to_subcategories": "« Zurück zu den Unterkategorien",
+ "back": "« Zurück",
+ "product_price": "💰 Preis",
+ "product_description": "📝 Beschreibung",
+ "product_available": "📦 Verfügbar",
+ "product_category": "Kategorie",
+ "buy_now": "🛒 Jetzt kaufen",
+ "increase": "➕",
+ "decrease": "➖",
+ "products_in": "📦 Produkte in {{name}}:",
+ "error_loading": "Fehler beim Laden der Produkte. Bitte versuche es erneut.",
+ "error_loading_cities": "Fehler beim Laden der Städte. Bitte versuche es erneut.",
+ "error_loading_districts": "Fehler beim Laden der Bezirke. Bitte versuche es erneut.",
+ "error_loading_categories": "Fehler beim Laden der Kategorien. Bitte versuche es erneut.",
+ "error_loading_product": "Fehler beim Laden der Produktdetails. Bitte versuche es erneut.",
+ "not_found": "Standort nicht gefunden. Zurück zum vorherigen Menü."
+ },
+ "purchase": {
+ "summary": "🛒 Kaufübersicht:",
+ "product": "Produkt",
+ "quantity": "Menge",
+ "total": "Gesamt",
+ "pay": "Bezahlen",
+ "cancel": "« Abbrechen",
+ "insufficient_balance": "❌ Nicht genug Guthaben. Dein aktuelles Guthaben: ${{balance}}. Du benötigst: ${{total}}.",
+ "need_wallet": "Du musst zuerst ein Krypto-Wallet hinzufügen, um Käufe zu tätigen.",
+ "add_wallet": "➕ Wallet hinzufügen",
+ "top_up_balance": "💰 Guthaben aufladen",
+ "not_enough_stock": "❌ Nicht genug auf Lager. Nur {{count}} verfügbar.",
+ "not_enough_money": "Nicht genug Guthaben",
+ "details": "📦 Kaufdetails:",
+ "location": "Standort",
+ "category": "Kategorie",
+ "private_info": "🔒 Private Informationen:",
+ "hidden_location": "Versteckter Standort",
+ "coordinates": "Koordinaten",
+ "view_purchase": "Kauf ansehen",
+ "confirm_received": "Erhalten!",
+ "back_to_list": "« Zurück zur Kaufübersicht",
+ "history_empty": "Dein Kaufverlauf ist leer.",
+ "browse_products": "🛍 Produkte durchsuchen",
+ "select_purchase": "📦 Wähle einen Kauf für Details (Seite {{page}} von {{total}}):",
+ "page_back": "« Zurück (Seite {{page}})",
+ "page_next": "Weiter » (Seite {{page}})",
+ "page_info": "Seite {{current}} von {{total}}",
+ "no_such_purchase": "Kauf nicht gefunden",
+ "no_such_product": "Produkt nicht gefunden",
+ "purchase_received": "Danke! Dein Kauf wurde als erhalten markiert.",
+ "admin_notification": "Benutzer {{username}} hat den Erhalt von Kauf #{{purchaseId}} bestätigt.",
+ "error_loading": "Fehler beim Laden des Kaufverlaufs. Bitte versuche es erneut.",
+ "error_loading_details": "Fehler beim Laden der Kaufdetails. Bitte versuche es erneut.",
+ "error_confirming": "Fehler bei der Empfangsbestätigung. Bitte versuche es erneut.",
+ "error_processing": "Fehler bei der Kaufabwicklung. Bitte versuche es erneut.",
+ "invalid_wallet": "Ungültiger Wallet-Typ.",
+ "invalid_product": "Ungültiges Produkt.",
+ "invalid_quantity": "Ungültige Menge."
+ },
+ "wallet": {
+ "your_wallets": "💰 *Deine Wallets*",
+ "your_active_wallets": "💰 *Deine aktiven Wallets:*",
+ "balance": "Guthaben",
+ "value": "Wert",
+ "address": "Adresse",
+ "network": "Netzwerk",
+ "deposit": "💳 {{type}} aufladen",
+ "deposit_via_changenow": "💳 Über ChangeNOW aufladen",
+ "select_crypto": "🔐 Wähle eine Kryptowährung zum Wallet erstellen:",
+ "wallet_generated": "✅ Neues Wallet erfolgreich erstellt!",
+ "wallet_type": "Typ",
+ "previous_archived": "ℹ️ Dein vorheriges Wallet wurde archiviert.",
+ "recovery_stored": "⚠️ Wichtig: Deine Wiederherstellungsphrase wurde sicher gespeichert.",
+ "error_generating": "❌ Fehler beim Wallet erstellen. Bitte versuche es erneut.",
+ "no_wallets": "Du hast noch keine Wallets. Erstelle zuerst eines.",
+ "no_wallets_prefix": "❌ Du hast noch keine Wallets. Erstelle zuerst eines.",
+ "no_active_wallets": "Du hast keine aktiven Wallets.",
+ "back_to_balance": "« Zurück zum Guthaben",
+ "back": "« Zurück",
+ "invalid_wallet_type": "Ungültiger Wallet-Typ.",
+ "user_not_found": "Benutzer nicht gefunden.",
+ "profile_not_found": "Profil nicht gefunden. Bitte verwende /start.",
+ "profile_not_found_short": "Profil nicht gefunden.",
+ "error_loading": "Fehler beim Laden der Wallets. Bitte versuche es erneut.",
+ "error_loading_balance": "Fehler beim Laden des Guthabens. Bitte versuche es erneut.",
+ "error_loading_archived": "Fehler beim Laden der archivierten Wallets. Bitte versuche es erneut.",
+ "error_loading_history": "Fehler beim Laden der Transaktionshistorie. Bitte versuche es erneut.",
+ "error_refreshing_balances": "❌ Fehler beim Aktualisieren der Guthaben.",
+ "error_refreshing_balances_retry": "❌ Fehler beim Aktualisieren der Guthaben. Bitte versuche es erneut.",
+ "error_deposit_instructions": "Fehler beim Erstellen der Einzahlungsanleitung. Bitte versuche es erneut.",
+ "error_copying_address": "Fehler beim Kopieren der Adresse.",
+ "add_crypto_wallet": "➕ Krypto-Wallet hinzufügen",
+ "top_up": "💸 Aufladen",
+ "refresh_balance": "🔄 Guthaben aktualisieren",
+ "refreshing_balances": "🔄 Guthaben werden aktualisiert...",
+ "archived_wallets_count": "📁 Archivierte Wallets ({{count}})",
+ "archived_wallets_title": "📁 *Archivierte Wallets:*",
+ "no_archived_wallets": "Keine archivierten Wallets gefunden.",
+ "archived_date": "Archiviert",
+ "total_crypto_balance": "📊 *Gesamtes Krypto-Guthaben:*",
+ "bonus_balance_label": "🎁 *Bonus-Guthaben:*",
+ "available_balance_label": "💰 *Verfügbares Guthaben:*",
+ "total_type": "📊 *Gesamt {{type}}:*",
+ "amount": "Betrag",
+ "total_archived_value": "💰 *Gesamtwert der archivierten Wallets:*",
+ "transaction_history": "📊 Transaktionshistorie",
+ "transaction_history_title": "📊 *Transaktionshistorie:*",
+ "recent_transactions": "📊 *Letzte Transaktionen:*",
+ "no_transactions": "Keine Transaktionen gefunden.",
+ "tx_amount": "💰 Betrag",
+ "tx_hash": "🔗 TX-Hash",
+ "tx_date": "🕒 Datum",
+ "tx_wallet_type": "💼 Wallet-Typ",
+ "previous_page": "⬅️ Zurück",
+ "next_page": "➡️ Weiter",
+ "wallet_not_found": "Wallet nicht gefunden. Bitte versuche es erneut.",
+ "wallet_not_found_short": "Wallet nicht gefunden.",
+ "deposit_changenow_select": "💳 *Einzahlung über ChangeNOW*\n\nWähle das Wallet zum Aufladen:",
+ "deposit_select_amount": "💳 *{{type}} aufladen*\n\nWähle den Betrag (USD) zum Aufladen:",
+ "deposit_title": "💳 *{{type}} aufladen — €{{amount}}*",
+ "deposit_instructions_title": "📋 *Schritt-für-Schritt-Anleitung:*",
+ "deposit_step1": "1️⃣ Tippe auf *Adresse kopieren* unten, um deine Wallet-Adresse zu kopieren",
+ "deposit_step2": "2️⃣ Tippe auf *ChangeNOW öffnen* \\— Betrag €{{amount}} und Währung {{type}} sind bereits eingestellt",
+ "deposit_step3": "3️⃣ Füge auf ChangeNOW die kopierte Adresse als Empfangswallet ein",
+ "deposit_step4": "4️⃣ Gib deine E-Mail ein und erstelle ein Passwort \\— dies ist *gesetzlich vorgeschrieben* für Kartenzahlungen \\(KYC-Verifizierung\\)\\. Deine Daten sind durch ChangeNOWs Sicherheit geschützt",
+ "deposit_step5": "5️⃣ Bezahle mit deiner Bankkarte \\(Visa\\/Mastercard\\)",
+ "deposit_step6": "6️⃣ Die Kryptowährung wird innerhalb von 5\\-30 Minuten in deinem Wallet eintreffen",
+ "deposit_your_address": "🔐 *Deine {{type}} Wallet-Adresse:*",
+ "deposit_important_title": "⚠️ *Wichtig:*",
+ "deposit_important1": "• Überprüfe die Wallet-Adresse doppelt vor der Bestätigung",
+ "deposit_important2": "• E-Mail \\+ Passwort auf ChangeNOW ist ein Standard-Verifizierungsschritt für Kartenzahlungen \\— keine Sorge\\, es ist sicher",
+ "deposit_important3": "• Wenn die Kryptowährung nicht innerhalb von 30 Minuten eintrifft \\— prüfe den Transaktionsstatus in deiner ChangeNOW E-Mail-Bestätigung",
+ "deposit_open_changenow": "🌐 ChangeNOW öffnen — €{{amount}} → {{type}}",
+ "deposit_copy_address": "📋 Adresse kopieren",
+ "deposit_change_amount": "🔄 Betrag ändern",
+ "deposit_choose_different": "💸 Anderes Wallet wählen",
+ "deposit_wallet_address": "{{type}} Wallet-Adresse:",
+ "back_to_deposit": "« Zurück zur Einzahlung",
+ "deposit_address_sent": "📋 {{type}}-Adresse gesendet! Kopiere sie aus der Nachricht unten.",
+ "network_erc20": "Ethereum-Netzwerk (ERC-20)",
+ "network_btc": "Bitcoin-Netzwerk",
+ "network_ltc": "Litecoin-Netzwerk",
+ "network_eth": "Ethereum-Netzwerk",
+ "network_unknown": "Unbekanntes Netzwerk"
+ },
+ "location": {
+ "select_country": "🌍 Wähle dein Land:",
+ "select_city": "🏙 Wähle eine Stadt in {{country}}:",
+ "select_district": "📍 Wähle einen Bezirk in {{city}}:",
+ "no_locations": "Noch keine Standorte verfügbar.",
+ "back_to_profile": "« Zurück zum Profil",
+ "back_to_countries": "« Zurück zu den Ländern",
+ "location_updated": "✅ Standort erfolgreich aktualisiert!",
+ "country": "Land",
+ "city": "Stadt",
+ "district": "Bezirk",
+ "error_loading_countries": "Fehler beim Laden der Länder. Bitte versuche es erneut.",
+ "error_loading_cities": "Fehler beim Laden der Städte. Bitte versuche es erneut.",
+ "error_loading_districts": "Fehler beim Laden der Bezirke. Bitte versuche es erneut.",
+ "error_updating": "Fehler beim Aktualisieren des Standorts. Bitte versuche es erneut."
+ },
+ "deletion": {
+ "confirm_title": "⚠️ Bist du sicher, dass du dein Konto löschen möchtest?",
+ "confirm_body": "Diese Aktion:\n- Löscht alle Benutzerdaten\n- Entfernt alle Wallets\n- Löscht den Kaufverlauf\n\nDiese Aktion kann nicht rückgängig gemacht werden!",
+ "confirm_button": "✅ Löschung bestätigen",
+ "cancel_button": "❌ Abbrechen",
+ "deleted": "⚠️ Dein Konto wurde erfolgreich gelöscht",
+ "error_processing": "Fehler bei der Löschanfrage. Bitte versuche es erneut.",
+ "error_deleting": "Fehler beim Löschen des Benutzers. Bitte versuche es erneut."
+ },
+ "keyboard": {
+ "products": "📦 Produkte",
+ "profile": "👤 Profil",
+ "purchases": "🛍 Käufe",
+ "wallets": "💰 Wallets",
+ "manage_products": "📦 Produkte verwalten",
+ "manage_users": "👥 Benutzer verwalten",
+ "manage_locations": "📍 Standorte verwalten",
+ "database_backup": "💾 Datenbank-Backup",
+ "manage_wallets": "💰 Wallets verwalten"
+ }
+}
\ No newline at end of file
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
new file mode 100644
index 0000000..dee00bb
--- /dev/null
+++ b/src/i18n/locales/en.json
@@ -0,0 +1,219 @@
+{
+ "bot": {
+ "welcome": "Welcome to the shop! Choose an option:",
+ "language_select": "🌍 Please select your language:",
+ "language_changed": "✅ Language changed to {{language}}!",
+ "error_generic": "Error processing request. Please try again.",
+ "account_blocked": "⚠️ Your account has been blocked by administrator",
+ "account_deleted": "⚠️ Your account has been deleted by administrator",
+ "contact_support": "Contact support"
+ },
+ "profile": {
+ "title": "👤 *Your Profile*",
+ "not_found": "Profile not found. Please use /start to create one.",
+ "telegram_id": "📱 Telegram ID",
+ "location": "📍 Location",
+ "location_not_set": "Not set",
+ "stats": "📊 Statistics:",
+ "total_purchases": "Total Purchases",
+ "total_spent": "Total Spent",
+ "active_wallets": "Active Wallets",
+ "archived_wallets": "Archived Wallets",
+ "bonus_balance": "Bonus Balance",
+ "available_balance": "Available Balance",
+ "member_since": "📅 Member since",
+ "set_location": "📍 Set Location",
+ "delete_account": "❌ Delete Account",
+ "error_loading": "Error loading profile. Please try again."
+ },
+ "products": {
+ "select_country": "🌍 Select your country:",
+ "select_city": "🏙 Select city in {{country}}:",
+ "select_district": "📍 Select district in {{city}}:",
+ "select_category": "📦 Select category:",
+ "select_product": "Select a product:",
+ "no_products": "No products available at the moment.",
+ "no_products_category": "No products available in this category.",
+ "no_products_subcategory": "No products available in this subcategory.",
+ "back_to_countries": "« Back to Countries",
+ "back_to_cities": "« Back to Cities",
+ "back_to_subcategories": "« Back to Subcategories",
+ "back": "« Back",
+ "product_price": "💰 Price",
+ "product_description": "📝 Description",
+ "product_available": "📦 Available",
+ "product_category": "Category",
+ "buy_now": "🛒 Buy Now",
+ "increase": "➕",
+ "decrease": "➖",
+ "products_in": "📦 Products in {{name}}:",
+ "error_loading": "Error loading products. Please try again.",
+ "error_loading_cities": "Error loading cities. Please try again.",
+ "error_loading_districts": "Error loading districts. Please try again.",
+ "error_loading_categories": "Error loading categories. Please try again.",
+ "error_loading_product": "Error loading product details. Please try again.",
+ "not_found": "Location not found. Returning to previous menu."
+ },
+ "purchase": {
+ "summary": "🛒 Purchase Summary:",
+ "product": "Product",
+ "quantity": "Quantity",
+ "total": "Total",
+ "pay": "Pay",
+ "cancel": "« Cancel",
+ "insufficient_balance": "❌ Insufficient balance. Your current balance is ${{balance}}. You need ${{total}} to complete this purchase.",
+ "need_wallet": "You need to add a crypto wallet first to make purchases.",
+ "add_wallet": "➕ Add Wallet",
+ "top_up_balance": "💰 Top Up Balance",
+ "not_enough_stock": "❌ Not enough items in stock. Only {{count}} available.",
+ "not_enough_money": "Not enough money",
+ "details": "📦 Purchase Details:",
+ "location": "Location",
+ "category": "Category",
+ "private_info": "🔒 Private Information:",
+ "hidden_location": "Hidden Location",
+ "coordinates": "Coordinates",
+ "view_purchase": "View new purchase",
+ "confirm_received": "I've got it!",
+ "back_to_list": "« Back to Purchase List",
+ "history_empty": "Your purchase history is empty.",
+ "browse_products": "🛍 Browse Products",
+ "select_purchase": "📦 Select purchase to view detailed information (Page {{page}} of {{total}}):",
+ "page_back": "« Back (Page {{page}})",
+ "page_next": "Next » (Page {{page}})",
+ "page_info": "Page {{current}} of {{total}}",
+ "no_such_purchase": "No such purchase",
+ "no_such_product": "No such product",
+ "purchase_received": "Thank you! Your purchase has been marked as received.",
+ "admin_notification": "User {{username}} has confirmed receiving purchase #{{purchaseId}}.",
+ "error_loading": "Error loading purchase history. Please try again.",
+ "error_loading_details": "Error loading purchase details. Please try again.",
+ "error_confirming": "Error confirming receipt. Please try again.",
+ "error_processing": "Error processing purchase. Please try again.",
+ "invalid_wallet": "Invalid wallet type.",
+ "invalid_product": "Invalid product.",
+ "invalid_quantity": "Invalid quantity."
+ },
+ "wallet": {
+ "your_wallets": "💰 *Your Wallets*",
+ "your_active_wallets": "💰 *Your Active Wallets:*",
+ "balance": "Balance",
+ "value": "Value",
+ "address": "Address",
+ "network": "Network",
+ "deposit": "💳 Deposit {{type}}",
+ "deposit_via_changenow": "💳 Deposit via ChangeNOW",
+ "select_crypto": "🔐 Select cryptocurrency to generate wallet:",
+ "wallet_generated": "✅ New wallet generated successfully!",
+ "wallet_type": "Type",
+ "previous_archived": "ℹ️ Your previous wallet has been archived.",
+ "recovery_stored": "⚠️ Important: Your recovery phrase has been securely stored.",
+ "error_generating": "❌ Error generating wallet. Please try again.",
+ "no_wallets": "You don't have any wallets yet. Create one first.",
+ "no_wallets_prefix": "❌ You don't have any wallets yet. Create one first.",
+ "no_active_wallets": "You don't have any active wallets yet.",
+ "back_to_balance": "« Back to Balance",
+ "back": "« Back",
+ "invalid_wallet_type": "Invalid wallet type.",
+ "user_not_found": "User not found.",
+ "profile_not_found": "Profile not found. Please use /start to create one.",
+ "profile_not_found_short": "Profile not found.",
+ "error_loading": "Error loading wallets. Please try again.",
+ "error_loading_balance": "Error loading balance. Please try again.",
+ "error_loading_archived": "Error loading archived wallets. Please try again.",
+ "error_loading_history": "Error loading transaction history. Please try again.",
+ "error_refreshing_balances": "❌ Error refreshing balances.",
+ "error_refreshing_balances_retry": "❌ Error refreshing balances. Please try again.",
+ "error_deposit_instructions": "Error creating deposit instructions. Please try again.",
+ "error_copying_address": "Error copying address.",
+ "add_crypto_wallet": "➕ Add Crypto Wallet",
+ "top_up": "💸 Top Up",
+ "refresh_balance": "🔄 Refresh Balance",
+ "refreshing_balances": "🔄 Refreshing balances...",
+ "archived_wallets_count": "📁 Archived Wallets ({{count}})",
+ "archived_wallets_title": "📁 *Archived Wallets:*",
+ "no_archived_wallets": "No archived wallets found.",
+ "archived_date": "Archived",
+ "total_crypto_balance": "📊 *Total Crypto Balance:*",
+ "bonus_balance_label": "🎁 *Bonus Balance:*",
+ "available_balance_label": "💰 *Available Balance:*",
+ "total_type": "📊 *Total {{type}}:*",
+ "amount": "Amount",
+ "total_archived_value": "💰 *Total Value of Archived Wallets:*",
+ "transaction_history": "📊 Transaction History",
+ "transaction_history_title": "📊 *Transaction History:*",
+ "recent_transactions": "📊 *Recent Transactions:*",
+ "no_transactions": "No transactions found.",
+ "tx_amount": "💰 Amount",
+ "tx_hash": "🔗 TX Hash",
+ "tx_date": "🕒 Date",
+ "tx_wallet_type": "💼 Wallet Type",
+ "previous_page": "⬅️ Previous",
+ "next_page": "➡️ Next",
+ "wallet_not_found": "Wallet not found. Please try again.",
+ "wallet_not_found_short": "Wallet not found.",
+ "deposit_changenow_select": "💳 *Deposit via ChangeNOW*\n\nSelect the wallet you want to top up:",
+ "deposit_select_amount": "💳 *Deposit {{type}}*\n\nSelect the amount (USD) you want to deposit:",
+ "deposit_title": "💳 *Deposit {{type}} — €{{amount}}*",
+ "deposit_instructions_title": "📋 *Step\\-by\\-step instructions:*",
+ "deposit_step1": "1️⃣ Tap *Copy Address* below to copy your wallet address",
+ "deposit_step2": "2️⃣ Tap *Open ChangeNOW* \\— the amount €{{amount}} and currency {{type}} are already set",
+ "deposit_step3": "3️⃣ On ChangeNOW\\, paste the copied address as the receiving wallet",
+ "deposit_step4": "4️⃣ Enter your email and create a password when prompted \\— this is *required by law* for card payments \\(KYC verification\\)\\. Your data is protected by ChangeNOW\\'s security",
+ "deposit_step5": "5️⃣ Pay with your bank card \\(Visa\\/Mastercard\\)",
+ "deposit_step6": "6️⃣ Crypto will arrive in your wallet within 5\\-30 minutes",
+ "deposit_your_address": "🔐 *Your {{type}} wallet address:*",
+ "deposit_important_title": "⚠️ *Important:*",
+ "deposit_important1": "• Double\\-check the wallet address before confirming",
+ "deposit_important2": "• Email \\+ password on ChangeNOW is a standard verification step for card payments \\— don\\'t worry\\, it\\'s safe",
+ "deposit_important3": "• If crypto doesn\\'t arrive within 30 min \\— check the transaction status in your ChangeNOW email confirmation",
+ "deposit_open_changenow": "🌐 Open ChangeNOW — €{{amount}} → {{type}}",
+ "deposit_copy_address": "📋 Copy Address",
+ "deposit_change_amount": "🔄 Change Amount",
+ "deposit_choose_different": "💸 Choose Different Wallet",
+ "deposit_wallet_address": "{{type}} wallet address:",
+ "back_to_deposit": "« Back to Deposit",
+ "deposit_address_sent": "📋 {{type}} address sent! Copy it from the message below.",
+ "network_erc20": "Ethereum Network (ERC-20)",
+ "network_btc": "Bitcoin Network",
+ "network_ltc": "Litecoin Network",
+ "network_eth": "Ethereum Network",
+ "network_unknown": "Unknown Network"
+ },
+ "location": {
+ "select_country": "🌍 Select your country:",
+ "select_city": "🏙 Select city in {{country}}:",
+ "select_district": "📍 Select district in {{city}}:",
+ "no_locations": "No locations available yet.",
+ "back_to_profile": "« Back to Profile",
+ "back_to_countries": "« Back to Countries",
+ "location_updated": "✅ Location updated successfully!",
+ "country": "Country",
+ "city": "City",
+ "district": "District",
+ "error_loading_countries": "Error loading countries. Please try again.",
+ "error_loading_cities": "Error loading cities. Please try again.",
+ "error_loading_districts": "Error loading districts. Please try again.",
+ "error_updating": "Error updating location. Please try again."
+ },
+ "deletion": {
+ "confirm_title": "⚠️ Are you sure you want to delete your account?",
+ "confirm_body": "This action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!",
+ "confirm_button": "✅ Confirm Delete",
+ "cancel_button": "❌ Cancel",
+ "deleted": "⚠️ Your account has been successfully deleted",
+ "error_processing": "Error processing delete request. Please try again.",
+ "error_deleting": "Error deleting user. Please try again."
+ },
+ "keyboard": {
+ "products": "📦 Products",
+ "profile": "👤 Profile",
+ "purchases": "🛍 Purchases",
+ "wallets": "💰 Wallets",
+ "manage_products": "📦 Manage Products",
+ "manage_users": "👥 Manage Users",
+ "manage_locations": "📍 Manage Locations",
+ "database_backup": "💾 Database Backup",
+ "manage_wallets": "💰 Manage Wallets"
+ }
+}
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
new file mode 100644
index 0000000..4da2500
--- /dev/null
+++ b/src/i18n/locales/es.json
@@ -0,0 +1,219 @@
+{
+ "bot": {
+ "welcome": "¡Bienvenido a la tienda! Elige una opción:",
+ "language_select": "🌍 Por favor, selecciona tu idioma:",
+ "language_changed": "✅ ¡Idioma cambiado a {{language}}!",
+ "error_generic": "Error al procesar la solicitud. Inténtalo de nuevo.",
+ "account_blocked": "⚠️ Tu cuenta ha sido bloqueada por el administrador",
+ "account_deleted": "⚠️ Tu cuenta ha sido eliminada por el administrador",
+ "contact_support": "Contactar soporte"
+ },
+ "profile": {
+ "title": "👤 *Tu Perfil*",
+ "not_found": "Perfil no encontrado. Usa /start para crear uno.",
+ "telegram_id": "📱 Telegram ID",
+ "location": "📍 Ubicación",
+ "location_not_set": "No establecida",
+ "stats": "📊 Estadísticas:",
+ "total_purchases": "Compras totales",
+ "total_spent": "Total gastado",
+ "active_wallets": "Billeteras activas",
+ "archived_wallets": "Billeteras archivadas",
+ "bonus_balance": "Saldo de bonificación",
+ "available_balance": "Saldo disponible",
+ "member_since": "📅 Miembro desde",
+ "set_location": "📍 Establecer ubicación",
+ "delete_account": "❌ Eliminar cuenta",
+ "error_loading": "Error al cargar perfil. Inténtalo de nuevo."
+ },
+ "products": {
+ "select_country": "🌍 Selecciona tu país:",
+ "select_city": "🏙 Selecciona ciudad en {{country}}:",
+ "select_district": "📍 Selecciona distrito en {{city}}:",
+ "select_category": "📦 Selecciona categoría:",
+ "select_product": "Selecciona un producto:",
+ "no_products": "No hay productos disponibles en este momento.",
+ "no_products_category": "No hay productos disponibles en esta categoría.",
+ "no_products_subcategory": "No hay productos disponibles en esta subcategoría.",
+ "back_to_countries": "« Volver a países",
+ "back_to_cities": "« Volver a ciudades",
+ "back_to_subcategories": "« Volver a subcategorías",
+ "back": "« Volver",
+ "product_price": "💰 Precio",
+ "product_description": "📝 Descripción",
+ "product_available": "📦 Disponibles",
+ "product_category": "Categoría",
+ "buy_now": "🛒 Comprar ahora",
+ "increase": "➕",
+ "decrease": "➖",
+ "products_in": "📦 Productos en {{name}}:",
+ "error_loading": "Error al cargar productos. Inténtalo de nuevo.",
+ "error_loading_cities": "Error al cargar ciudades. Inténtalo de nuevo.",
+ "error_loading_districts": "Error al cargar distritos. Inténtalo de nuevo.",
+ "error_loading_categories": "Error al cargar categorías. Inténtalo de nuevo.",
+ "error_loading_product": "Error al cargar detalles del producto. Inténtalo de nuevo.",
+ "not_found": "Ubicación no encontrada. Volviendo al menú anterior."
+ },
+ "purchase": {
+ "summary": "🛒 Resumen de compra:",
+ "product": "Producto",
+ "quantity": "Cantidad",
+ "total": "Total",
+ "pay": "Pagar",
+ "cancel": "« Cancelar",
+ "insufficient_balance": "❌ Saldo insuficiente. Tu saldo actual es ${{balance}}. Necesitas ${{total}} para completar esta compra.",
+ "need_wallet": "Necesitas agregar una billetera cripto primero para hacer compras.",
+ "add_wallet": "➕ Agregar billetera",
+ "top_up_balance": "💰 Recargar saldo",
+ "not_enough_stock": "❌ No hay suficientes unidades en stock. Solo {{count}} disponibles.",
+ "not_enough_money": "Dinero insuficiente",
+ "details": "📦 Detalles de la compra:",
+ "location": "Ubicación",
+ "category": "Categoría",
+ "private_info": "🔒 Información privada:",
+ "hidden_location": "Ubicación oculta",
+ "coordinates": "Coordenadas",
+ "view_purchase": "Ver nueva compra",
+ "confirm_received": "¡Lo recibí!",
+ "back_to_list": "« Volver a la lista de compras",
+ "history_empty": "Tu historial de compras está vacío.",
+ "browse_products": "🛍 Ver productos",
+ "select_purchase": "📦 Selecciona una compra para ver detalles (Página {{page}} de {{total}}):",
+ "page_back": "« Anterior (Pág. {{page}})",
+ "page_next": "Siguiente » (Pág. {{page}})",
+ "page_info": "Pág. {{current}} de {{total}}",
+ "no_such_purchase": "No existe esa compra",
+ "no_such_product": "No existe ese producto",
+ "purchase_received": "¡Gracias! Tu compra ha sido marcada como recibida.",
+ "admin_notification": "El usuario {{username}} ha confirmado recibir la compra #{{purchaseId}}.",
+ "error_loading": "Error al cargar historial de compras. Inténtalo de nuevo.",
+ "error_loading_details": "Error al cargar detalles de la compra. Inténtalo de nuevo.",
+ "error_confirming": "Error al confirmar recepción. Inténtalo de nuevo.",
+ "error_processing": "Error al procesar la compra. Inténtalo de nuevo.",
+ "invalid_wallet": "Tipo de billetera inválido.",
+ "invalid_product": "Producto inválido.",
+ "invalid_quantity": "Cantidad inválida."
+ },
+ "wallet": {
+ "your_wallets": "💰 *Tus billeteras*",
+ "your_active_wallets": "💰 *Tus billeteras activas:*",
+ "balance": "Saldo",
+ "value": "Valor",
+ "address": "Dirección",
+ "network": "Red",
+ "deposit": "💳 Depositar {{type}}",
+ "deposit_via_changenow": "💳 Depositar vía ChangeNOW",
+ "select_crypto": "🔐 Selecciona criptomoneda para generar billetera:",
+ "wallet_generated": "✅ ¡Nueva billetera generada exitosamente!",
+ "wallet_type": "Tipo",
+ "previous_archived": "ℹ️ Tu billetera anterior ha sido archivada.",
+ "recovery_stored": "⚠️ Importante: Tu frase de recuperación ha sido almacenada de forma segura.",
+ "error_generating": "❌ Error al generar billetera. Inténtalo de nuevo.",
+ "no_wallets": "Aún no tienes billeteras. Crea una primero.",
+ "no_wallets_prefix": "❌ Aún no tienes billeteras. Crea una primero.",
+ "no_active_wallets": "Aún no tienes billeteras activas.",
+ "back_to_balance": "« Volver al saldo",
+ "back": "« Volver",
+ "invalid_wallet_type": "Tipo de billetera inválido.",
+ "user_not_found": "Usuario no encontrado.",
+ "profile_not_found": "Perfil no encontrado. Usa /start para crear uno.",
+ "profile_not_found_short": "Perfil no encontrado.",
+ "error_loading": "Error al cargar billeteras. Inténtalo de nuevo.",
+ "error_loading_balance": "Error al cargar saldo. Inténtalo de nuevo.",
+ "error_loading_archived": "Error al cargar billeteras archivadas. Inténtalo de nuevo.",
+ "error_loading_history": "Error al cargar historial de transacciones. Inténtalo de nuevo.",
+ "error_refreshing_balances": "❌ Error al actualizar saldos.",
+ "error_refreshing_balances_retry": "❌ Error al actualizar saldos. Inténtalo de nuevo.",
+ "error_deposit_instructions": "Error al crear instrucciones de depósito. Inténtalo de nuevo.",
+ "error_copying_address": "Error al copiar dirección.",
+ "add_crypto_wallet": "➕ Agregar billetera cripto",
+ "top_up": "💸 Recargar",
+ "refresh_balance": "🔄 Actualizar saldo",
+ "refreshing_balances": "🔄 Actualizando saldos...",
+ "archived_wallets_count": "📁 Billeteras archivadas ({{count}})",
+ "archived_wallets_title": "📁 *Billeteras archivadas:*",
+ "no_archived_wallets": "No se encontraron billeteras archivadas.",
+ "archived_date": "Archivada",
+ "total_crypto_balance": "📊 *Saldo total cripto:*",
+ "bonus_balance_label": "🎁 *Saldo de bonificación:*",
+ "available_balance_label": "💰 *Saldo disponible:*",
+ "total_type": "📊 *Total {{type}}:*",
+ "amount": "Cantidad",
+ "total_archived_value": "💰 *Valor total de billeteras archivadas:*",
+ "transaction_history": "📊 Historial de transacciones",
+ "transaction_history_title": "📊 *Historial de transacciones:*",
+ "recent_transactions": "📊 *Transacciones recientes:*",
+ "no_transactions": "No se encontraron transacciones.",
+ "tx_amount": "💰 Cantidad",
+ "tx_hash": "🔗 TX Hash",
+ "tx_date": "🕒 Fecha",
+ "tx_wallet_type": "💼 Tipo de billetera",
+ "previous_page": "⬅️ Anterior",
+ "next_page": "➡️ Siguiente",
+ "wallet_not_found": "Billetera no encontrada. Inténtalo de nuevo.",
+ "wallet_not_found_short": "Billetera no encontrada.",
+ "deposit_changenow_select": "💳 *Depositar vía ChangeNOW*\n\nSelecciona la billetera que quieres recargar:",
+ "deposit_select_amount": "💳 *Depositar {{type}}*\n\nSelecciona la cantidad (USD) que quieres depositar:",
+ "deposit_title": "💳 *Depositar {{type}} — €{{amount}}*",
+ "deposit_instructions_title": "📋 *Instrucciones paso a paso:*",
+ "deposit_step1": "1️⃣ Toca *Copiar dirección* abajo para copiar tu dirección de billetera",
+ "deposit_step2": "2️⃣ Toca *Abrir ChangeNOW* \\— la cantidad €{{amount}} y la moneda {{type}} ya están configuradas",
+ "deposit_step3": "3️⃣ En ChangeNOW\\, pega la dirección copiada como billetera receptora",
+ "deposit_step4": "4️⃣ Ingresa tu correo y crea una contraseña cuando se solicite \\— esto es *requerido por ley* para pagos con tarjeta \\(verificación KYC\\)\\. Tus datos están protegidos por la seguridad de ChangeNOW",
+ "deposit_step5": "5️⃣ Paga con tu tarjeta bancaria \\(Visa\\/Mastercard\\)",
+ "deposit_step6": "6️⃣ Las criptomonedas llegarán a tu billetera en 5\\-30 minutos",
+ "deposit_your_address": "🔐 *Tu dirección de billetera {{type}}:*",
+ "deposit_important_title": "⚠️ *Importante:*",
+ "deposit_important1": "• Verifica dos veces la dirección de la billetera antes de confirmar",
+ "deposit_important2": "• Correo \\+ contraseña en ChangeNOW es un paso de verificación estándar para pagos con tarjeta \\— no te preocupes\\, es seguro",
+ "deposit_important3": "• Si las criptomonedas no llegan en 30 min \\— verifica el estado de la transacción en tu confirmación por correo de ChangeNOW",
+ "deposit_open_changenow": "🌐 Abrir ChangeNOW — €{{amount}} → {{type}}",
+ "deposit_copy_address": "📋 Copiar dirección",
+ "deposit_change_amount": "🔄 Cambiar cantidad",
+ "deposit_choose_different": "💸 Elegir otra billetera",
+ "deposit_wallet_address": "Dirección de billetera {{type}}:",
+ "back_to_deposit": "« Volver al depósito",
+ "deposit_address_sent": "📋 ¡Dirección {{type}} enviada! Cópiala del mensaje de abajo.",
+ "network_erc20": "Red Ethereum (ERC-20)",
+ "network_btc": "Red Bitcoin",
+ "network_ltc": "Red Litecoin",
+ "network_eth": "Red Ethereum",
+ "network_unknown": "Red desconocida"
+ },
+ "location": {
+ "select_country": "🌍 Selecciona tu país:",
+ "select_city": "🏙 Selecciona ciudad en {{country}}:",
+ "select_district": "📍 Selecciona distrito en {{city}}:",
+ "no_locations": "No hay ubicaciones disponibles aún.",
+ "back_to_profile": "« Volver al perfil",
+ "back_to_countries": "« Volver a países",
+ "location_updated": "✅ ¡Ubicación actualizada exitosamente!",
+ "country": "País",
+ "city": "Ciudad",
+ "district": "Distrito",
+ "error_loading_countries": "Error al cargar países. Inténtalo de nuevo.",
+ "error_loading_cities": "Error al cargar ciudades. Inténtalo de nuevo.",
+ "error_loading_districts": "Error al cargar distritos. Inténtalo de nuevo.",
+ "error_updating": "Error al actualizar ubicación. Inténtalo de nuevo."
+ },
+ "deletion": {
+ "confirm_title": "⚠️ ¿Estás seguro de que quieres eliminar tu cuenta?",
+ "confirm_body": "Esta acción:\n- Eliminará todos los datos de usuario\n- Eliminará todas las billeteras\n- Borrará el historial de compras\n\n¡Esta acción no se puede deshacer!",
+ "confirm_button": "✅ Confirmar eliminación",
+ "cancel_button": "❌ Cancelar",
+ "deleted": "⚠️ Tu cuenta ha sido eliminada exitosamente",
+ "error_processing": "Error al procesar la solicitud de eliminación. Inténtalo de nuevo.",
+ "error_deleting": "Error al eliminar usuario. Inténtalo de nuevo."
+ },
+ "keyboard": {
+ "products": "📦 Productos",
+ "profile": "👤 Perfil",
+ "purchases": "🛍 Compras",
+ "wallets": "💰 Billeteras",
+ "manage_products": "📦 Gestionar productos",
+ "manage_users": "👥 Gestionar usuarios",
+ "manage_locations": "📍 Gestionar ubicaciones",
+ "database_backup": "💾 Respaldo de base de datos",
+ "manage_wallets": "💰 Gestionar billeteras"
+ }
+}
diff --git a/src/index.js b/src/index.js
index 038dbcf..4c309e6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -27,6 +27,14 @@ if (bot && botAvailable) {
}
});
+ bot.onText(/\/language/, async (msg) => {
+ try {
+ await userHandler.handleLanguageCommand(msg);
+ } catch (error) {
+ await ErrorHandler.handleError(bot, msg.chat.id, error, 'language command');
+ }
+ });
+
bot.onText(/\/admin/, async (msg) => {
try {
await adminHandler.handleAdminCommand(msg);
diff --git a/src/migrations/008_user_language.js b/src/migrations/008_user_language.js
new file mode 100644
index 0000000..8d8973d
--- /dev/null
+++ b/src/migrations/008_user_language.js
@@ -0,0 +1,16 @@
+import logger from '../utils/logger.js';
+import { checkColumnExists } from './runner.js';
+
+export default async function migration008(db) {
+ if (await checkColumnExists(db, 'users', 'language')) {
+ logger.info('Migration 008: language column already exists, skipping');
+ return;
+ }
+
+ await db.runAsync('BEGIN TRANSACTION');
+
+ await db.runAsync(`ALTER TABLE users ADD COLUMN language TEXT DEFAULT 'en'`);
+
+ await db.runAsync('COMMIT');
+ logger.info('Migration 008: Added language column to users table');
+}
\ No newline at end of file
diff --git a/src/migrations/009_user_language_set.js b/src/migrations/009_user_language_set.js
new file mode 100644
index 0000000..9d386d6
--- /dev/null
+++ b/src/migrations/009_user_language_set.js
@@ -0,0 +1,16 @@
+import logger from '../utils/logger.js';
+import { checkColumnExists } from './runner.js';
+
+export default async function migration009(db) {
+ if (await checkColumnExists(db, 'users', 'language_set')) {
+ logger.info('Migration 009: language_set column already exists, skipping');
+ return;
+ }
+
+ await db.runAsync('BEGIN TRANSACTION');
+
+ await db.runAsync(`ALTER TABLE users ADD COLUMN language_set INTEGER DEFAULT 0`);
+
+ await db.runAsync('COMMIT');
+ logger.info('Migration 009: Added language_set column to users table');
+}
\ No newline at end of file
diff --git a/src/migrations/runner.js b/src/migrations/runner.js
index 4b80f0c..616aed2 100644
--- a/src/migrations/runner.js
+++ b/src/migrations/runner.js
@@ -42,6 +42,8 @@ export async function runMigrations() {
(await import('./005_audit_log.js')).default,
(await import('./006_subcategories.js')).default,
(await import('./007_commission_payments.js')).default,
+ (await import('./008_user_language.js')).default,
+ (await import('./009_user_language_set.js')).default,
];
for (let i = currentVersion; i < migrations.length; i++) {
diff --git a/src/router/messageRouter.js b/src/router/messageRouter.js
index c05e586..1d0664b 100644
--- a/src/router/messageRouter.js
+++ b/src/router/messageRouter.js
@@ -1,15 +1,23 @@
+import UserService from '../services/userService.js';
+import { tForLang, AVAILABLE_LANGUAGES } from '../i18n/index.js';
+
class MessageRouter {
constructor() {
this.inputHandlers = [];
this.textHandlers = new Map();
+ this.localeKeyMap = new Map();
}
registerInput(handler) {
this.inputHandlers.push(handler);
}
- registerText(text, handler) {
- this.textHandlers.set(text, handler);
+ registerText(localeKey, handler) {
+ this.textHandlers.set(localeKey, handler);
+ for (const lang of AVAILABLE_LANGUAGES) {
+ const translatedText = tForLang(lang, localeKey);
+ this.localeKeyMap.set(`${lang}:${translatedText}`, localeKey);
+ }
}
async dispatch(msg) {
@@ -17,10 +25,27 @@ class MessageRouter {
if (await handler(msg)) return;
}
- if (msg.text && this.textHandlers.has(msg.text)) {
- await this.textHandlers.get(msg.text)(msg);
+ if (!msg.text) return;
+
+ const user = msg.__user || await UserService.getUserByTelegramId(msg.from.id);
+ const lang = user?.language || 'en';
+
+ const userKey = `${lang}:${msg.text}`;
+ const localeKey = this.localeKeyMap.get(userKey);
+
+ if (localeKey && this.textHandlers.has(localeKey)) {
+ await this.textHandlers.get(localeKey)(msg);
return;
}
+
+ for (const l of AVAILABLE_LANGUAGES) {
+ const key = `${l}:${msg.text}`;
+ const lk = this.localeKeyMap.get(key);
+ if (lk && this.textHandlers.has(lk)) {
+ await this.textHandlers.get(lk)(msg);
+ return;
+ }
+ }
}
}
diff --git a/src/router/routes.js b/src/router/routes.js
index 2c6a6aa..e938046 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -32,44 +32,50 @@ export function registerRoutes() {
messageRouter.registerInput(adminUserHandler.handleBonusBalanceInput.bind(adminUserHandler));
messageRouter.registerInput(productHandler.handleCategoryUpdate.bind(productHandler));
+ // === Language Selection ===
+ callbackRouter.registerPrefix('set_language_', async (cq) => {
+ logDebug(cq.data, 'handleSetLanguage');
+ await userHandler.handleSetLanguage(cq);
+ });
+
// === Text Commands ===
- messageRouter.registerText('📦 Products', async (msg) => {
+ messageRouter.registerText('keyboard.products', async (msg) => {
logDebug(msg.text, 'showProducts');
await userProductHandler.showProducts(msg);
});
- messageRouter.registerText('👤 Profile', async (msg) => {
+ messageRouter.registerText('keyboard.profile', async (msg) => {
logDebug(msg.text, 'showProfile');
await userHandler.showProfile(msg);
});
- messageRouter.registerText('💰 Wallets', async (msg) => {
+ messageRouter.registerText('keyboard.wallets', async (msg) => {
logDebug(msg.text, 'showBalance');
await userWalletsHandler.showBalance(msg);
});
- messageRouter.registerText('🛍 Purchases', async (msg) => {
+ messageRouter.registerText('keyboard.purchases', async (msg) => {
logDebug(msg.text, 'showPurchases');
await userPurchaseHandler.showPurchases(msg);
});
- messageRouter.registerText('📦 Manage Products', async (msg) => {
+ messageRouter.registerText('keyboard.manage_products', async (msg) => {
if (!isAdmin(msg.from.id)) return;
logDebug(msg.text, 'handleProductManagement');
await productHandler.handleProductManagement(msg);
});
- messageRouter.registerText('👥 Manage Users', async (msg) => {
+ messageRouter.registerText('keyboard.manage_users', async (msg) => {
if (!isAdmin(msg.from.id)) return;
logDebug(msg.text, 'handleUserList');
await adminUserHandler.handleUserList(msg);
});
- messageRouter.registerText('📍 Manage Locations', async (msg) => {
+ messageRouter.registerText('keyboard.manage_locations', async (msg) => {
if (!isAdmin(msg.from.id)) return;
logDebug(msg.text, 'handleViewLocations');
await adminLocationHandler.handleViewLocations(msg);
});
- messageRouter.registerText('💾 Database Backup', async (msg) => {
+ messageRouter.registerText('keyboard.database_backup', async (msg) => {
if (!isAdmin(msg.from.id)) return;
logDebug(msg.text, 'handleDump');
await adminDumpHandler.handleDump(msg);
});
- messageRouter.registerText('💰 Manage Wallets', async (msg) => {
+ messageRouter.registerText('keyboard.manage_wallets', async (msg) => {
if (!isAdmin(msg.from.id)) return;
logDebug(msg.text, 'handleWalletManagement');
await adminWalletsHandler.handleWalletManagement(msg);
diff --git a/src/services/userService.js b/src/services/userService.js
index 16d5b65..694af1b 100644
--- a/src/services/userService.js
+++ b/src/services/userService.js
@@ -1,13 +1,14 @@
// userService.js
import db from "../config/database.js";
+import config from '../config/config.js';
import Wallet from "../models/Wallet.js";
import WalletUtils from "../utils/walletUtils.js";
import logger from "../utils/logger.js";
const ALLOWED_USER_FIELDS = new Set([
'telegram_id', 'username', 'country', 'city',
- 'district', 'status', 'total_balance', 'bonus_balance'
+ 'district', 'status', 'total_balance', 'bonus_balance', 'language', 'language_set'
]);
@@ -215,6 +216,19 @@ class UserService {
throw error;
}
}
+
+ static async getUserLanguage(telegramId) {
+ const user = await this.getUserByTelegramId(telegramId);
+ return user?.language || 'en';
+ }
+
+ static async setUserLanguage(telegramId, lang) {
+ const normalizedTelegramId = this.normalizeTelegramId(telegramId);
+ await db.runAsync(
+ 'UPDATE users SET language = ?, language_set = 1 WHERE telegram_id = ?',
+ [lang, normalizedTelegramId]
+ );
+ }
}
export default UserService;
\ No newline at end of file