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:
@@ -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
171
src/admin/routes/locales.js
Normal 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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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' }]] }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' }]] }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
66
src/i18n/index.js
Normal 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
219
src/i18n/locales/de.json
Normal 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
219
src/i18n/locales/en.json
Normal 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
219
src/i18n/locales/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
16
src/migrations/008_user_language.js
Normal file
16
src/migrations/008_user_language.js
Normal 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');
|
||||
}
|
||||
16
src/migrations/009_user_language_set.js
Normal file
16
src/migrations/009_user_language_set.js
Normal 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');
|
||||
}
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user