feat: add i18n localization system (en/es/de) with admin panel

- Add i18n module with tForUser/tForLang/t functions and {{param}} interpolation
- Add 3 locale files: en.json, es.json, de.json (201 keys each)
- Add language selection on /start and /language command with flag emojis
- Localize all bot user-facing strings (handlers, keyboards, errors)
- Localize messageRouter keyboard matching via locale keys
- Add DB migrations 008 (language column) and 009 (language_set column)
- Add localization admin tab at /locales for editing translations
- Add userService.getUserLanguage/setUserLanguage methods
- Cache user object on msg.__user to avoid triple DB fetch
- Idempotent migrations with checkColumnExists guards
- Error boundary on i18n locale file loading
- Admin locales route uses AVAILABLE_LANGUAGES import
This commit is contained in:
NW
2026-06-25 21:22:32 +01:00
parent 41ff2b8769
commit a8bf50df24
29 changed files with 1606 additions and 365 deletions

View File

@@ -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);
}

171
src/admin/routes/locales.js Normal file
View File

@@ -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 += `
<tr data-key="${fullKey}">
<td class="key-cell"><code>${fullKey}</code></td>
<td><input type="text" data-lang="en" data-key="${fullKey}" value="${escapeAttr(enVal)}" class="locale-input"></td>
<td><input type="text" data-lang="de" data-key="${fullKey}" value="${escapeAttr(deVal)}" class="locale-input"></td>
<td><input type="text" data-lang="es" data-key="${fullKey}" value="${escapeAttr(esVal)}" class="locale-input"></td>
</tr>`;
}
sectionsHtml += `
<div class="locale-section">
<h3>${section}</h3>
<table class="locale-table">
<thead>
<tr>
<th>Ключ</th>
<th>English</th>
<th>Deutsch</th>
<th>Español</th>
</tr>
</thead>
<tbody>${rowsHtml}</tbody>
</table>
</div>`;
}
const content = `
<div class="locale-actions">
<button id="saveAllBtn" class="btn btn-primary">💾 Сохранить все изменения</button>
<span id="saveStatus" class="save-status"></span>
</div>
${sectionsHtml}
<script>
document.getElementById('saveAllBtn').addEventListener('click', async () => {
const btn = document.getElementById('saveAllBtn');
const status = document.getElementById('saveStatus');
btn.disabled = true;
status.textContent = 'Сохранение...';
const inputs = document.querySelectorAll('.locale-input');
const changes = [];
for (const input of inputs) {
if (input.dataset.originalValue !== input.value) {
changes.push({
lang: input.dataset.lang,
key: input.dataset.key,
value: input.value
});
}
}
let saved = 0;
let errors = 0;
for (const change of changes) {
try {
const res = await fetch('/locales/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(change)
});
if (res.ok) saved++;
else errors++;
} catch (e) {
errors++;
}
}
status.textContent = errors > 0
? 'Сохранено ' + saved + ' из ' + changes.length + '. Ошибок: ' + errors
: 'Сохранено ' + saved + ' из ' + changes.length + ' изменений ✓';
btn.disabled = false;
for (const input of inputs) {
input.dataset.originalValue = input.value;
}
});
document.querySelectorAll('.locale-input').forEach(input => {
input.dataset.originalValue = input.value;
});
</script>
`;
return layout('Локализация', content, 'locales');
}
function escapeAttr(str) {
return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export default router;

View File

@@ -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);

View File

@@ -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 =>

View File

@@ -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,

View File

@@ -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'));
}
}
}

View File

@@ -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},

View File

@@ -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'));
}
}
}

View File

@@ -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'));
}
}
}
}

View File

@@ -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'));
}
}
}

View File

@@ -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'));
}
}
}

View File

@@ -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'));
}
}

View File

@@ -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' }]] }
});
}
}

View File

@@ -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') });
}
}
}

View File

@@ -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) {

View File

@@ -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'));
}
}
}

View File

@@ -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' }]] }
});
}
}

View File

@@ -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'));
}
}
}

66
src/i18n/index.js Normal file
View File

@@ -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 };

219
src/i18n/locales/de.json Normal file
View File

@@ -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"
}
}

219
src/i18n/locales/en.json Normal file
View File

@@ -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"
}
}

219
src/i18n/locales/es.json Normal file
View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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++) {

View File

@@ -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;
}
}
}
}

View File

@@ -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);

View File

@@ -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;