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}

+ + + + + + + + + + ${rowsHtml} +
КлючEnglishDeutschEspañol
+
`; + } + + 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