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

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

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

@@ -0,0 +1,219 @@
{
"bot": {
"welcome": "¡Bienvenido a la tienda! Elige una opción:",
"language_select": "🌍 Por favor, selecciona tu idioma:",
"language_changed": "✅ ¡Idioma cambiado a {{language}}!",
"error_generic": "Error al procesar la solicitud. Inténtalo de nuevo.",
"account_blocked": "⚠️ Tu cuenta ha sido bloqueada por el administrador",
"account_deleted": "⚠️ Tu cuenta ha sido eliminada por el administrador",
"contact_support": "Contactar soporte"
},
"profile": {
"title": "👤 *Tu Perfil*",
"not_found": "Perfil no encontrado. Usa /start para crear uno.",
"telegram_id": "📱 Telegram ID",
"location": "📍 Ubicación",
"location_not_set": "No establecida",
"stats": "📊 Estadísticas:",
"total_purchases": "Compras totales",
"total_spent": "Total gastado",
"active_wallets": "Billeteras activas",
"archived_wallets": "Billeteras archivadas",
"bonus_balance": "Saldo de bonificación",
"available_balance": "Saldo disponible",
"member_since": "📅 Miembro desde",
"set_location": "📍 Establecer ubicación",
"delete_account": "❌ Eliminar cuenta",
"error_loading": "Error al cargar perfil. Inténtalo de nuevo."
},
"products": {
"select_country": "🌍 Selecciona tu país:",
"select_city": "🏙 Selecciona ciudad en {{country}}:",
"select_district": "📍 Selecciona distrito en {{city}}:",
"select_category": "📦 Selecciona categoría:",
"select_product": "Selecciona un producto:",
"no_products": "No hay productos disponibles en este momento.",
"no_products_category": "No hay productos disponibles en esta categoría.",
"no_products_subcategory": "No hay productos disponibles en esta subcategoría.",
"back_to_countries": "« Volver a países",
"back_to_cities": "« Volver a ciudades",
"back_to_subcategories": "« Volver a subcategorías",
"back": "« Volver",
"product_price": "💰 Precio",
"product_description": "📝 Descripción",
"product_available": "📦 Disponibles",
"product_category": "Categoría",
"buy_now": "🛒 Comprar ahora",
"increase": "",
"decrease": "",
"products_in": "📦 Productos en {{name}}:",
"error_loading": "Error al cargar productos. Inténtalo de nuevo.",
"error_loading_cities": "Error al cargar ciudades. Inténtalo de nuevo.",
"error_loading_districts": "Error al cargar distritos. Inténtalo de nuevo.",
"error_loading_categories": "Error al cargar categorías. Inténtalo de nuevo.",
"error_loading_product": "Error al cargar detalles del producto. Inténtalo de nuevo.",
"not_found": "Ubicación no encontrada. Volviendo al menú anterior."
},
"purchase": {
"summary": "🛒 Resumen de compra:",
"product": "Producto",
"quantity": "Cantidad",
"total": "Total",
"pay": "Pagar",
"cancel": "« Cancelar",
"insufficient_balance": "❌ Saldo insuficiente. Tu saldo actual es ${{balance}}. Necesitas ${{total}} para completar esta compra.",
"need_wallet": "Necesitas agregar una billetera cripto primero para hacer compras.",
"add_wallet": " Agregar billetera",
"top_up_balance": "💰 Recargar saldo",
"not_enough_stock": "❌ No hay suficientes unidades en stock. Solo {{count}} disponibles.",
"not_enough_money": "Dinero insuficiente",
"details": "📦 Detalles de la compra:",
"location": "Ubicación",
"category": "Categoría",
"private_info": "🔒 Información privada:",
"hidden_location": "Ubicación oculta",
"coordinates": "Coordenadas",
"view_purchase": "Ver nueva compra",
"confirm_received": "¡Lo recibí!",
"back_to_list": "« Volver a la lista de compras",
"history_empty": "Tu historial de compras está vacío.",
"browse_products": "🛍 Ver productos",
"select_purchase": "📦 Selecciona una compra para ver detalles (Página {{page}} de {{total}}):",
"page_back": "« Anterior (Pág. {{page}})",
"page_next": "Siguiente » (Pág. {{page}})",
"page_info": "Pág. {{current}} de {{total}}",
"no_such_purchase": "No existe esa compra",
"no_such_product": "No existe ese producto",
"purchase_received": "¡Gracias! Tu compra ha sido marcada como recibida.",
"admin_notification": "El usuario {{username}} ha confirmado recibir la compra #{{purchaseId}}.",
"error_loading": "Error al cargar historial de compras. Inténtalo de nuevo.",
"error_loading_details": "Error al cargar detalles de la compra. Inténtalo de nuevo.",
"error_confirming": "Error al confirmar recepción. Inténtalo de nuevo.",
"error_processing": "Error al procesar la compra. Inténtalo de nuevo.",
"invalid_wallet": "Tipo de billetera inválido.",
"invalid_product": "Producto inválido.",
"invalid_quantity": "Cantidad inválida."
},
"wallet": {
"your_wallets": "💰 *Tus billeteras*",
"your_active_wallets": "💰 *Tus billeteras activas:*",
"balance": "Saldo",
"value": "Valor",
"address": "Dirección",
"network": "Red",
"deposit": "💳 Depositar {{type}}",
"deposit_via_changenow": "💳 Depositar vía ChangeNOW",
"select_crypto": "🔐 Selecciona criptomoneda para generar billetera:",
"wallet_generated": "✅ ¡Nueva billetera generada exitosamente!",
"wallet_type": "Tipo",
"previous_archived": " Tu billetera anterior ha sido archivada.",
"recovery_stored": "⚠️ Importante: Tu frase de recuperación ha sido almacenada de forma segura.",
"error_generating": "❌ Error al generar billetera. Inténtalo de nuevo.",
"no_wallets": "Aún no tienes billeteras. Crea una primero.",
"no_wallets_prefix": "❌ Aún no tienes billeteras. Crea una primero.",
"no_active_wallets": "Aún no tienes billeteras activas.",
"back_to_balance": "« Volver al saldo",
"back": "« Volver",
"invalid_wallet_type": "Tipo de billetera inválido.",
"user_not_found": "Usuario no encontrado.",
"profile_not_found": "Perfil no encontrado. Usa /start para crear uno.",
"profile_not_found_short": "Perfil no encontrado.",
"error_loading": "Error al cargar billeteras. Inténtalo de nuevo.",
"error_loading_balance": "Error al cargar saldo. Inténtalo de nuevo.",
"error_loading_archived": "Error al cargar billeteras archivadas. Inténtalo de nuevo.",
"error_loading_history": "Error al cargar historial de transacciones. Inténtalo de nuevo.",
"error_refreshing_balances": "❌ Error al actualizar saldos.",
"error_refreshing_balances_retry": "❌ Error al actualizar saldos. Inténtalo de nuevo.",
"error_deposit_instructions": "Error al crear instrucciones de depósito. Inténtalo de nuevo.",
"error_copying_address": "Error al copiar dirección.",
"add_crypto_wallet": " Agregar billetera cripto",
"top_up": "💸 Recargar",
"refresh_balance": "🔄 Actualizar saldo",
"refreshing_balances": "🔄 Actualizando saldos...",
"archived_wallets_count": "📁 Billeteras archivadas ({{count}})",
"archived_wallets_title": "📁 *Billeteras archivadas:*",
"no_archived_wallets": "No se encontraron billeteras archivadas.",
"archived_date": "Archivada",
"total_crypto_balance": "📊 *Saldo total cripto:*",
"bonus_balance_label": "🎁 *Saldo de bonificación:*",
"available_balance_label": "💰 *Saldo disponible:*",
"total_type": "📊 *Total {{type}}:*",
"amount": "Cantidad",
"total_archived_value": "💰 *Valor total de billeteras archivadas:*",
"transaction_history": "📊 Historial de transacciones",
"transaction_history_title": "📊 *Historial de transacciones:*",
"recent_transactions": "📊 *Transacciones recientes:*",
"no_transactions": "No se encontraron transacciones.",
"tx_amount": "💰 Cantidad",
"tx_hash": "🔗 TX Hash",
"tx_date": "🕒 Fecha",
"tx_wallet_type": "💼 Tipo de billetera",
"previous_page": "⬅️ Anterior",
"next_page": "➡️ Siguiente",
"wallet_not_found": "Billetera no encontrada. Inténtalo de nuevo.",
"wallet_not_found_short": "Billetera no encontrada.",
"deposit_changenow_select": "💳 *Depositar vía ChangeNOW*\n\nSelecciona la billetera que quieres recargar:",
"deposit_select_amount": "💳 *Depositar {{type}}*\n\nSelecciona la cantidad (USD) que quieres depositar:",
"deposit_title": "💳 *Depositar {{type}} — €{{amount}}*",
"deposit_instructions_title": "📋 *Instrucciones paso a paso:*",
"deposit_step1": "1⃣ Toca *Copiar dirección* abajo para copiar tu dirección de billetera",
"deposit_step2": "2⃣ Toca *Abrir ChangeNOW* \\— la cantidad €{{amount}} y la moneda {{type}} ya están configuradas",
"deposit_step3": "3⃣ En ChangeNOW\\, pega la dirección copiada como billetera receptora",
"deposit_step4": "4⃣ Ingresa tu correo y crea una contraseña cuando se solicite \\— esto es *requerido por ley* para pagos con tarjeta \\(verificación KYC\\)\\. Tus datos están protegidos por la seguridad de ChangeNOW",
"deposit_step5": "5⃣ Paga con tu tarjeta bancaria \\(Visa\\/Mastercard\\)",
"deposit_step6": "6⃣ Las criptomonedas llegarán a tu billetera en 5\\-30 minutos",
"deposit_your_address": "🔐 *Tu dirección de billetera {{type}}:*",
"deposit_important_title": "⚠️ *Importante:*",
"deposit_important1": "• Verifica dos veces la dirección de la billetera antes de confirmar",
"deposit_important2": "• Correo \\+ contraseña en ChangeNOW es un paso de verificación estándar para pagos con tarjeta \\— no te preocupes\\, es seguro",
"deposit_important3": "• Si las criptomonedas no llegan en 30 min \\— verifica el estado de la transacción en tu confirmación por correo de ChangeNOW",
"deposit_open_changenow": "🌐 Abrir ChangeNOW — €{{amount}} → {{type}}",
"deposit_copy_address": "📋 Copiar dirección",
"deposit_change_amount": "🔄 Cambiar cantidad",
"deposit_choose_different": "💸 Elegir otra billetera",
"deposit_wallet_address": "Dirección de billetera {{type}}:",
"back_to_deposit": "« Volver al depósito",
"deposit_address_sent": "📋 ¡Dirección {{type}} enviada! Cópiala del mensaje de abajo.",
"network_erc20": "Red Ethereum (ERC-20)",
"network_btc": "Red Bitcoin",
"network_ltc": "Red Litecoin",
"network_eth": "Red Ethereum",
"network_unknown": "Red desconocida"
},
"location": {
"select_country": "🌍 Selecciona tu país:",
"select_city": "🏙 Selecciona ciudad en {{country}}:",
"select_district": "📍 Selecciona distrito en {{city}}:",
"no_locations": "No hay ubicaciones disponibles aún.",
"back_to_profile": "« Volver al perfil",
"back_to_countries": "« Volver a países",
"location_updated": "✅ ¡Ubicación actualizada exitosamente!",
"country": "País",
"city": "Ciudad",
"district": "Distrito",
"error_loading_countries": "Error al cargar países. Inténtalo de nuevo.",
"error_loading_cities": "Error al cargar ciudades. Inténtalo de nuevo.",
"error_loading_districts": "Error al cargar distritos. Inténtalo de nuevo.",
"error_updating": "Error al actualizar ubicación. Inténtalo de nuevo."
},
"deletion": {
"confirm_title": "⚠️ ¿Estás seguro de que quieres eliminar tu cuenta?",
"confirm_body": "Esta acción:\n- Eliminará todos los datos de usuario\n- Eliminará todas las billeteras\n- Borrará el historial de compras\n\n¡Esta acción no se puede deshacer!",
"confirm_button": "✅ Confirmar eliminación",
"cancel_button": "❌ Cancelar",
"deleted": "⚠️ Tu cuenta ha sido eliminada exitosamente",
"error_processing": "Error al procesar la solicitud de eliminación. Inténtalo de nuevo.",
"error_deleting": "Error al eliminar usuario. Inténtalo de nuevo."
},
"keyboard": {
"products": "📦 Productos",
"profile": "👤 Perfil",
"purchases": "🛍 Compras",
"wallets": "💰 Billeteras",
"manage_products": "📦 Gestionar productos",
"manage_users": "👥 Gestionar usuarios",
"manage_locations": "📍 Gestionar ubicaciones",
"database_backup": "💾 Respaldo de base de datos",
"manage_wallets": "💰 Gestionar billeteras"
}
}