const SUPPORTED_LANGS = ['en', 'it', 'es', 'fr', 'de', 'ru', 'zh'];
const LANG_STORAGE_KEY = 'plm_lang';
// date-locale used for toLocaleDateString, keyed by the same language codes
const DATE_LOCALES = { en: 'en-US', it: 'it-IT', es: 'es-ES', fr: 'fr-FR', de: 'de-DE', ru: 'ru-RU', zh: 'zh-CN' };
const TRANSLATIONS = {
en: {
'nav.ariaSections': 'Sections',
'nav.guideTitle': 'Guide',
'nav.guideAria': 'Open the user guide',
'nav.bugReport': 'Report a bug',
'nav.logoutTitle': 'Log out',
'nav.logoutAria': 'Log out of your account',
'nav.deposit': 'Deposit',
'nav.bet': 'Bet',
'nav.withdraw': 'Withdraw',
'nav.profile': 'Profile',
'chain.connecting': 'Connecting…',
'chain.block': 'Block {n}',
'chain.connectionLost': 'Connection to server lost — retrying…',
'maintenance.banner': 'Scheduled maintenance: the current round completes normally (winner included), but the next round will not open until maintenance ends.',
'hero.lead': 'Deposit PLM, join the round with a fixed entry fee, and if your number is drawn you win the jackpot.',
'hero.step1.title': '1. Deposit',
'hero.step1.hint': 'Get your own personal PLM address, yours forever',
'hero.step2.title': '2. Play',
'hero.step2.hint': 'A fixed-cost bet to enter the current round',
'hero.step3.title': '3. Win',
'hero.step3.hint': 'Drawn from a block hash, jackpot credited instantly',
'trust.fixedRate': 'Declared fixed entry fee',
'trust.blockHash': 'Drawn from a block hash',
'trust.freeWithdraw': 'Withdraw freely at any time',
'auth.tabLogin': 'Login',
'auth.tabRegister': 'Register',
'auth.username': 'Username',
'auth.password': 'Password',
'auth.passwordConfirm': 'Confirm password',
'auth.loginBtn': 'Log in',
'auth.registerBtn': 'Create account',
'round.players': 'Players',
'round.jackpot': 'Jackpot',
'round.status.open': 'open',
'round.status.closing': 'closing',
'round.status.drawing': 'drawing in progress',
'round.status.paying_out': 'paying out the winner',
'round.title': 'Round #{id} — {status}',
'round.none': 'No active round',
'chain.status.waiting': 'Waiting for the next round',
'chain.status.open': 'Round open',
'chain.status.closing': 'Round closed — waiting for bet confirmations',
'chain.status.drawing': 'Drawing in progress',
'chain.status.paying_out': 'Paying out the winner',
'draw.defaultLabel': 'Drawing the winner…',
'draw.closing': 'Round closed — waiting for the last bet to confirm before drawing the winner…',
'draw.drawing': 'Waiting for the next block to draw the winner…',
'draw.payingOutBlock': 'Winner drawn from block #{height} — paying out the winner…',
'draw.payingOut': 'Winner drawn — paying out the winner…',
'result.win': '🎉 You won! +{amount} PLM',
'result.lose': 'Not a win this time.',
'deposit.balanceTitle': 'Internal balance',
'deposit.balanceHint': 'Updated after 1 network confirmation',
'deposit.refreshBtn': 'Refresh',
'deposit.refreshAria': 'Refresh balance',
'deposit.addressTitle': 'Deposit address',
'deposit.addressHint': 'This also receives any winnings',
'deposit.copyAria': 'Copy address',
'deposit.qrAlt': 'QR code of the deposit address',
'bet.title': 'Bet',
'bet.hint': 'Fixed entry to the current round',
'bet.button': 'Place bet ({amount} PLM)',
'bet.buttonNoAmount': 'Place bet',
'withdraw.title': 'Withdrawal',
'withdraw.hint': 'Send funds to an external PLM address',
'withdraw.addressLabel': 'External address',
'withdraw.addressHint': 'Only P2WPKH bech32 addresses (starting with plm1q...). Legacy (P...) or P2SH addresses are not supported.',
'withdraw.amountLabel': 'Amount (PLM)',
'withdraw.amountPlaceholder': 'e.g. 2',
'withdraw.fullAmountPrefix': 'Withdraw the full amount (',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Withdraw',
'profile.title': 'Profile',
'profile.hint': 'Your account information',
'profile.usernameLabel': 'Username',
'profile.addressLabel': 'Deposit address',
'profile.balanceLabel': 'Internal balance',
'profile.createdLabel': 'User since',
'settings.title': 'Settings',
'settings.hint': "Change your account's password",
'settings.currentPasswordLabel': 'Current password',
'settings.newPasswordLabel': 'New password',
'settings.newPasswordConfirmLabel': 'Confirm new password',
'settings.updateBtn': 'Update password',
'toast.passwordMismatch': 'Passwords do not match.',
'toast.accountCreated': 'Account created.',
'toast.loginSuccess': 'Logged in successfully.',
'toast.requestTimeout': 'Request to the server timed out.',
'toast.addressCopied': 'Address copied.',
'toast.copyFailed': "Couldn't copy automatically.",
'toast.newPasswordMismatch': 'The new passwords do not match.',
'toast.passwordTooShort': 'The new password must be at least 8 characters.',
'toast.passwordUpdated': 'Password updated.',
'toast.betPlaced': 'Bet placed on round #{id}.',
'toast.roundWon': 'You won round #{id}! +{amount} PLM',
'toast.withdrawSent': 'Withdrawal sent.',
// Keyed by the `code` the API returns in its structured error detail
// (app/api/errors.py). A code with no entry here falls back to the API's
// own English `message`, so a new server-side error is never a blank toast.
'error.network_unavailable': 'Not connected to the network, please try again shortly.',
'error.no_round_open': 'No round is open right now, please try again shortly.',
'error.round_closing': 'The current round is closing, please try again shortly.',
'error.already_betting': 'You already have an active bet in the current round.',
'error.insufficient_balance': 'Insufficient balance.',
'error.amount_below_network_fee': 'The amount is too small to cover the network fee.',
'error.invalid_address': 'Not a valid PLM address (it must start with plm1q…).',
'error.amount_below_minimum': 'The minimum withdrawal amount is {minimum_plm} PLM.',
'error.current_password_incorrect': 'The current password is incorrect.',
'error.password_too_short': 'The new password must be at least {minimum} characters.',
'error.username_taken': 'This username is already taken.',
'error.invalid_credentials': 'Wrong username or password.',
'error.derivation_index_conflict': 'Registration failed, please try again.',
'error.session_expired': 'Session expired, please log in again.',
'error.invalid_request': 'Invalid request, please check the entered data.',
'error.invalid_amount': 'Enter an amount greater than zero.',
'loading.creating': 'Creating…',
'loading.loggingIn': 'Logging in…',
'loading.sendingBet': 'Placing bet…',
'loading.updating': 'Updating…',
'loading.refreshing': '…',
'loading.sending': 'Sending…',
},
it: {
'nav.ariaSections': 'Sezioni',
'nav.guideTitle': 'Guida',
'nav.guideAria': 'Apri la guida utente',
'nav.bugReport': 'Segnala un bug',
'nav.logoutTitle': 'Esci',
'nav.logoutAria': "Esci dall'account",
'nav.deposit': 'Deposito',
'nav.bet': 'Bet',
'nav.withdraw': 'Prelievo',
'nav.profile': 'Profilo',
'chain.connecting': 'Connessione…',
'chain.block': 'Blocco {n}',
'chain.connectionLost': 'Connessione al server persa — riprovo…',
'maintenance.banner': 'Manutenzione in programma: il round in corso viene completato regolarmente (vincitore incluso), ma il round successivo non si aprirà finché la manutenzione non sarà terminata.',
'hero.lead': 'Deposita PLM, entra nel round con una quota fissa, e se viene estratto il tuo numero vinci il montepremi.',
'hero.step1.title': '1. Deposita',
'hero.step1.hint': 'Ricevi un indirizzo PLM personale, tuo per sempre',
'hero.step2.title': '2. Gioca',
'hero.step2.hint': 'Una bet a quota fissa per entrare nel round corrente',
'hero.step3.title': '3. Vinci',
'hero.step3.hint': 'Estrazione dal blocco, montepremi accreditato subito',
'trust.fixedRate': 'Quota fissa dichiarata',
'trust.blockHash': 'Estrazione da hash di blocco',
'trust.freeWithdraw': 'Prelievo libero in ogni momento',
'auth.tabLogin': 'Login',
'auth.tabRegister': 'Registrati',
'auth.username': 'Username',
'auth.password': 'Password',
'auth.passwordConfirm': 'Conferma password',
'auth.loginBtn': 'Accedi',
'auth.registerBtn': 'Crea account',
'round.players': 'Giocatori',
'round.jackpot': 'Jackpot',
'round.status.open': 'aperto',
'round.status.closing': 'in chiusura',
'round.status.drawing': 'estrazione in corso',
'round.status.paying_out': 'pagamento al vincitore in corso',
'round.title': 'Round #{id} — {status}',
'round.none': 'Nessun round attivo',
'chain.status.waiting': 'In attesa del prossimo round',
'chain.status.open': 'Round aperto',
'chain.status.closing': 'Round chiuso — attesa conferma puntate',
'chain.status.drawing': 'Estrazione in corso',
'chain.status.paying_out': 'Pagamento al vincitore in corso',
'draw.defaultLabel': 'Estrazione del vincitore in corso…',
'draw.closing': "Round chiuso — in attesa di conferma dell'ultima giocata prima di estrarre il vincitore…",
'draw.drawing': 'In attesa del prossimo blocco per estrarre il vincitore…',
'draw.payingOutBlock': 'Vincitore estratto dal blocco #{height} — pagamento al vincitore in corso…',
'draw.payingOut': 'Vincitore estratto — pagamento al vincitore in corso…',
'result.win': '🎉 Hai vinto! +{amount} PLM',
'result.lose': 'Non hai vinto questa volta.',
'deposit.balanceTitle': 'Saldo interno',
'deposit.balanceHint': 'Aggiornato dopo 1 conferma sulla rete',
'deposit.refreshBtn': 'Aggiorna',
'deposit.refreshAria': 'Aggiorna saldo',
'deposit.addressTitle': 'Indirizzo di deposito',
'deposit.addressHint': "È anche l'indirizzo su cui ricevi eventuali vincite",
'deposit.copyAria': 'Copia indirizzo',
'deposit.qrAlt': "QR code dell'indirizzo di deposito",
'bet.title': 'Bet',
'bet.hint': 'Ingresso fisso al round corrente',
'bet.button': 'Piazza bet ({amount} PLM)',
'bet.buttonNoAmount': 'Piazza bet',
'withdraw.title': 'Withdrawal',
'withdraw.hint': 'Invia fondi a un indirizzo PLM esterno',
'withdraw.addressLabel': 'Indirizzo esterno',
'withdraw.addressHint': 'Solo indirizzi P2WPKH bech32 (quelli che iniziano con plm1q...). Indirizzi legacy (P...) o P2SH non sono supportati.',
'withdraw.amountLabel': 'Importo (PLM)',
'withdraw.amountPlaceholder': 'es. 2',
'withdraw.fullAmountPrefix': "Preleva l'intero importo (",
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Preleva',
'profile.title': 'Profilo',
'profile.hint': 'Le tue informazioni account',
'profile.usernameLabel': 'Username',
'profile.addressLabel': 'Indirizzo di deposito',
'profile.balanceLabel': 'Saldo interno',
'profile.createdLabel': 'Utente dal',
'settings.title': 'Impostazioni',
'settings.hint': 'Cambia la password del tuo account',
'settings.currentPasswordLabel': 'Password attuale',
'settings.newPasswordLabel': 'Nuova password',
'settings.newPasswordConfirmLabel': 'Conferma nuova password',
'settings.updateBtn': 'Aggiorna password',
'toast.passwordMismatch': 'Le password non coincidono.',
'toast.accountCreated': 'Account creato.',
'toast.loginSuccess': 'Accesso riuscito.',
'toast.requestTimeout': 'Richiesta al server scaduta.',
'toast.addressCopied': 'Indirizzo copiato.',
'toast.copyFailed': 'Impossibile copiare automaticamente.',
'toast.newPasswordMismatch': 'Le nuove password non coincidono.',
'toast.passwordTooShort': 'La nuova password deve avere almeno 8 caratteri.',
'toast.passwordUpdated': 'Password aggiornata.',
'toast.betPlaced': 'Bet piazzata sul round #{id}.',
'toast.roundWon': 'Hai vinto il round #{id}! +{amount} PLM',
'toast.withdrawSent': 'Withdrawal inviato.',
'error.network_unavailable': 'Nessuna connessione alla rete, riprova tra poco.',
'error.no_round_open': 'Nessun round aperto in questo momento, riprova tra poco.',
'error.round_closing': 'Il round corrente si sta chiudendo, riprova tra poco.',
'error.already_betting': 'Hai già una bet attiva nel round corrente.',
'error.insufficient_balance': 'Saldo insufficiente.',
'error.amount_below_network_fee': "L'importo è troppo basso per coprire la fee di rete.",
'error.invalid_address': 'Indirizzo PLM non valido (deve iniziare con plm1q…).',
'error.amount_below_minimum': "L'importo minimo di prelievo è {minimum_plm} PLM.",
'error.current_password_incorrect': 'La password attuale non è corretta.',
'error.password_too_short': 'La nuova password deve avere almeno {minimum} caratteri.',
'error.username_taken': 'Questo username è già stato preso.',
'error.invalid_credentials': 'Username o password errati.',
'error.derivation_index_conflict': 'Registrazione non riuscita, riprova.',
'error.session_expired': 'Sessione scaduta, accedi di nuovo.',
'error.invalid_request': 'Richiesta non valida, controlla i dati inseriti.',
'error.invalid_amount': 'Inserisci un importo maggiore di zero.',
'loading.creating': 'Creazione…',
'loading.loggingIn': 'Accesso…',
'loading.sendingBet': 'Invio bet…',
'loading.updating': 'Aggiornamento…',
'loading.refreshing': '…',
'loading.sending': 'Invio…',
},
es: {
'nav.ariaSections': 'Secciones',
'nav.guideTitle': 'Guía',
'nav.guideAria': 'Abrir la guía del usuario',
'nav.bugReport': 'Reportar un error',
'nav.logoutTitle': 'Salir',
'nav.logoutAria': 'Cerrar sesión',
'nav.deposit': 'Depósito',
'nav.bet': 'Apuesta',
'nav.withdraw': 'Retiro',
'nav.profile': 'Perfil',
'chain.connecting': 'Conectando…',
'chain.block': 'Bloque {n}',
'chain.connectionLost': 'Conexión con el servidor perdida — reintentando…',
'maintenance.banner': 'Mantenimiento programado: la ronda actual se completa con normalidad (ganador incluido), pero la siguiente ronda no se abrirá hasta que finalice el mantenimiento.',
'hero.lead': 'Deposita PLM, únete a la ronda con una cuota fija de entrada, y si sale tu número ganas el bote.',
'hero.step1.title': '1. Deposita',
'hero.step1.hint': 'Recibe tu dirección PLM personal, tuya para siempre',
'hero.step2.title': '2. Juega',
'hero.step2.hint': 'Una apuesta a cuota fija para entrar en la ronda actual',
'hero.step3.title': '3. Gana',
'hero.step3.hint': 'Sorteo a partir del hash de un bloque, bote acreditado al instante',
'trust.fixedRate': 'Cuota de entrada fija y declarada',
'trust.blockHash': 'Sorteo a partir del hash de un bloque',
'trust.freeWithdraw': 'Retiro libre en cualquier momento',
'auth.tabLogin': 'Iniciar sesión',
'auth.tabRegister': 'Registrarse',
'auth.username': 'Usuario',
'auth.password': 'Contraseña',
'auth.passwordConfirm': 'Confirmar contraseña',
'auth.loginBtn': 'Entrar',
'auth.registerBtn': 'Crear cuenta',
'round.players': 'Jugadores',
'round.jackpot': 'Bote',
'round.status.open': 'abierta',
'round.status.closing': 'cerrando',
'round.status.drawing': 'sorteo en curso',
'round.status.paying_out': 'pagando al ganador',
'round.title': 'Ronda #{id} — {status}',
'round.none': 'No hay ninguna ronda activa',
'chain.status.waiting': 'Esperando la próxima ronda',
'chain.status.open': 'Ronda abierta',
'chain.status.closing': 'Ronda cerrada — esperando confirmación de apuestas',
'chain.status.drawing': 'Sorteo en curso',
'chain.status.paying_out': 'Pagando al ganador',
'draw.defaultLabel': 'Sorteando al ganador…',
'draw.closing': 'Ronda cerrada — esperando la confirmación de la última apuesta antes de sortear al ganador…',
'draw.drawing': 'Esperando el próximo bloque para sortear al ganador…',
'draw.payingOutBlock': 'Ganador sorteado en el bloque #{height} — pagando al ganador…',
'draw.payingOut': 'Ganador sorteado — pagando al ganador…',
'result.win': '🎉 ¡Has ganado! +{amount} PLM',
'result.lose': 'Esta vez no has ganado.',
'deposit.balanceTitle': 'Saldo interno',
'deposit.balanceHint': 'Actualizado tras 1 confirmación en la red',
'deposit.refreshBtn': 'Actualizar',
'deposit.refreshAria': 'Actualizar saldo',
'deposit.addressTitle': 'Dirección de depósito',
'deposit.addressHint': 'También es la dirección donde recibes tus posibles ganancias',
'deposit.copyAria': 'Copiar dirección',
'deposit.qrAlt': 'Código QR de la dirección de depósito',
'bet.title': 'Apuesta',
'bet.hint': 'Entrada fija a la ronda actual',
'bet.button': 'Realizar apuesta ({amount} PLM)',
'bet.buttonNoAmount': 'Realizar apuesta',
'withdraw.title': 'Retiro',
'withdraw.hint': 'Envía fondos a una dirección PLM externa',
'withdraw.addressLabel': 'Dirección externa',
'withdraw.addressHint': 'Solo direcciones P2WPKH bech32 (las que empiezan con plm1q...). No se admiten direcciones legacy (P...) ni P2SH.',
'withdraw.amountLabel': 'Importe (PLM)',
'withdraw.amountPlaceholder': 'p.ej. 2',
'withdraw.fullAmountPrefix': 'Retirar el importe completo (',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Retirar',
'profile.title': 'Perfil',
'profile.hint': 'La información de tu cuenta',
'profile.usernameLabel': 'Usuario',
'profile.addressLabel': 'Dirección de depósito',
'profile.balanceLabel': 'Saldo interno',
'profile.createdLabel': 'Usuario desde',
'settings.title': 'Ajustes',
'settings.hint': 'Cambia la contraseña de tu cuenta',
'settings.currentPasswordLabel': 'Contraseña actual',
'settings.newPasswordLabel': 'Nueva contraseña',
'settings.newPasswordConfirmLabel': 'Confirmar nueva contraseña',
'settings.updateBtn': 'Actualizar contraseña',
'toast.passwordMismatch': 'Las contraseñas no coinciden.',
'toast.accountCreated': 'Cuenta creada.',
'toast.loginSuccess': 'Sesión iniciada correctamente.',
'toast.requestTimeout': 'La solicitud al servidor ha caducado.',
'toast.addressCopied': 'Dirección copiada.',
'toast.copyFailed': 'No se pudo copiar automáticamente.',
'toast.newPasswordMismatch': 'Las nuevas contraseñas no coinciden.',
'toast.passwordTooShort': 'La nueva contraseña debe tener al menos 8 caracteres.',
'toast.passwordUpdated': 'Contraseña actualizada.',
'toast.betPlaced': 'Apuesta realizada en la ronda #{id}.',
'toast.roundWon': '¡Has ganado la ronda #{id}! +{amount} PLM',
'toast.withdrawSent': 'Retiro enviado.',
'error.network_unavailable': 'Sin conexión con la red, inténtalo de nuevo en un momento.',
'error.no_round_open': 'No hay ninguna ronda abierta ahora mismo, inténtalo de nuevo en un momento.',
'error.round_closing': 'La ronda actual se está cerrando, inténtalo de nuevo en un momento.',
'error.already_betting': 'Ya tienes una apuesta activa en la ronda actual.',
'error.insufficient_balance': 'Saldo insuficiente.',
'error.amount_below_network_fee': 'El importe es demasiado pequeño para cubrir la comisión de red.',
'error.invalid_address': 'Dirección PLM no válida (debe empezar por plm1q…).',
'error.amount_below_minimum': 'El importe mínimo de retiro es {minimum_plm} PLM.',
'error.current_password_incorrect': 'La contraseña actual no es correcta.',
'error.password_too_short': 'La nueva contraseña debe tener al menos {minimum} caracteres.',
'error.username_taken': 'Este nombre de usuario ya está en uso.',
'error.invalid_credentials': 'Usuario o contraseña incorrectos.',
'error.derivation_index_conflict': 'No se pudo completar el registro, inténtalo de nuevo.',
'error.session_expired': 'Sesión caducada, vuelve a iniciar sesión.',
'error.invalid_request': 'Solicitud no válida, revisa los datos introducidos.',
'error.invalid_amount': 'Introduce un importe mayor que cero.',
'loading.creating': 'Creando…',
'loading.loggingIn': 'Entrando…',
'loading.sendingBet': 'Enviando apuesta…',
'loading.updating': 'Actualizando…',
'loading.refreshing': '…',
'loading.sending': 'Enviando…',
},
fr: {
'nav.ariaSections': 'Sections',
'nav.guideTitle': 'Guide',
'nav.guideAria': "Ouvrir le guide de l'utilisateur",
'nav.bugReport': 'Signaler un bug',
'nav.logoutTitle': 'Se déconnecter',
'nav.logoutAria': 'Se déconnecter du compte',
'nav.deposit': 'Dépôt',
'nav.bet': 'Mise',
'nav.withdraw': 'Retrait',
'nav.profile': 'Profil',
'chain.connecting': 'Connexion…',
'chain.block': 'Bloc {n}',
'chain.connectionLost': 'Connexion au serveur perdue — nouvelle tentative…',
'maintenance.banner': "Maintenance programmée : le round en cours se termine normalement (gagnant inclus), mais le round suivant ne s'ouvrira qu'une fois la maintenance terminée.",
'hero.lead': 'Déposez des PLM, rejoignez le round avec une mise fixe, et si votre numéro est tiré vous remportez le jackpot.',
'hero.step1.title': '1. Déposez',
'hero.step1.hint': 'Recevez votre adresse PLM personnelle, à vous pour toujours',
'hero.step2.title': '2. Jouez',
'hero.step2.hint': 'Une mise à coût fixe pour entrer dans le round en cours',
'hero.step3.title': '3. Gagnez',
'hero.step3.hint': "Tirage à partir du hash d'un bloc, jackpot crédité instantanément",
'trust.fixedRate': "Mise d'entrée fixe et déclarée",
'trust.blockHash': "Tirage à partir du hash d'un bloc",
'trust.freeWithdraw': 'Retrait libre à tout moment',
'auth.tabLogin': 'Connexion',
'auth.tabRegister': "S'inscrire",
'auth.username': "Nom d'utilisateur",
'auth.password': 'Mot de passe',
'auth.passwordConfirm': 'Confirmer le mot de passe',
'auth.loginBtn': 'Se connecter',
'auth.registerBtn': 'Créer un compte',
'round.players': 'Joueurs',
'round.jackpot': 'Jackpot',
'round.status.open': 'ouvert',
'round.status.closing': 'en fermeture',
'round.status.drawing': 'tirage en cours',
'round.status.paying_out': 'paiement du gagnant en cours',
'round.title': 'Round #{id} — {status}',
'round.none': 'Aucun round actif',
'chain.status.waiting': 'En attente du prochain round',
'chain.status.open': 'Round ouvert',
'chain.status.closing': 'Round fermé — attente de confirmation des mises',
'chain.status.drawing': 'Tirage en cours',
'chain.status.paying_out': 'Paiement du gagnant en cours',
'draw.defaultLabel': 'Tirage du gagnant en cours…',
'draw.closing': 'Round fermé — en attente de la confirmation de la dernière mise avant de tirer le gagnant…',
'draw.drawing': 'En attente du prochain bloc pour tirer le gagnant…',
'draw.payingOutBlock': 'Gagnant tiré au bloc #{height} — paiement du gagnant en cours…',
'draw.payingOut': 'Gagnant tiré — paiement du gagnant en cours…',
'result.win': '🎉 Vous avez gagné ! +{amount} PLM',
'result.lose': "Pas de gain cette fois-ci.",
'deposit.balanceTitle': 'Solde interne',
'deposit.balanceHint': 'Mis à jour après 1 confirmation sur le réseau',
'deposit.refreshBtn': 'Actualiser',
'deposit.refreshAria': 'Actualiser le solde',
'deposit.addressTitle': 'Adresse de dépôt',
'deposit.addressHint': "C'est aussi l'adresse sur laquelle vous recevez d'éventuels gains",
'deposit.copyAria': "Copier l'adresse",
'deposit.qrAlt': "Code QR de l'adresse de dépôt",
'bet.title': 'Mise',
'bet.hint': 'Entrée fixe pour le round en cours',
'bet.button': 'Placer une mise ({amount} PLM)',
'bet.buttonNoAmount': 'Placer une mise',
'withdraw.title': 'Retrait',
'withdraw.hint': 'Envoyez des fonds vers une adresse PLM externe',
'withdraw.addressLabel': 'Adresse externe',
'withdraw.addressHint': "Uniquement les adresses P2WPKH bech32 (celles qui commencent par plm1q...). Les adresses legacy (P...) ou P2SH ne sont pas prises en charge.",
'withdraw.amountLabel': 'Montant (PLM)',
'withdraw.amountPlaceholder': 'ex. 2',
'withdraw.fullAmountPrefix': 'Retirer le montant total (',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Retirer',
'profile.title': 'Profil',
'profile.hint': 'Les informations de votre compte',
'profile.usernameLabel': "Nom d'utilisateur",
'profile.addressLabel': 'Adresse de dépôt',
'profile.balanceLabel': 'Solde interne',
'profile.createdLabel': 'Utilisateur depuis',
'settings.title': 'Paramètres',
'settings.hint': 'Changez le mot de passe de votre compte',
'settings.currentPasswordLabel': 'Mot de passe actuel',
'settings.newPasswordLabel': 'Nouveau mot de passe',
'settings.newPasswordConfirmLabel': 'Confirmer le nouveau mot de passe',
'settings.updateBtn': 'Mettre à jour le mot de passe',
'toast.passwordMismatch': 'Les mots de passe ne correspondent pas.',
'toast.accountCreated': 'Compte créé.',
'toast.loginSuccess': 'Connexion réussie.',
'toast.requestTimeout': 'La requête au serveur a expiré.',
'toast.addressCopied': 'Adresse copiée.',
'toast.copyFailed': 'Impossible de copier automatiquement.',
'toast.newPasswordMismatch': 'Les nouveaux mots de passe ne correspondent pas.',
'toast.passwordTooShort': 'Le nouveau mot de passe doit comporter au moins 8 caractères.',
'toast.passwordUpdated': 'Mot de passe mis à jour.',
'toast.betPlaced': 'Mise placée sur le round #{id}.',
'toast.roundWon': 'Vous avez gagné le round #{id} ! +{amount} PLM',
'toast.withdrawSent': 'Retrait envoyé.',
'error.network_unavailable': 'Pas de connexion au réseau, réessayez dans un instant.',
'error.no_round_open': "Aucun round n'est ouvert pour le moment, réessayez dans un instant.",
'error.round_closing': 'Le round en cours est en train de se fermer, réessayez dans un instant.',
'error.already_betting': 'Vous avez déjà une mise active dans le round en cours.',
'error.insufficient_balance': 'Solde insuffisant.',
'error.amount_below_network_fee': 'Le montant est trop faible pour couvrir les frais de réseau.',
'error.invalid_address': 'Adresse PLM invalide (elle doit commencer par plm1q…).',
'error.amount_below_minimum': 'Le montant minimum de retrait est de {minimum_plm} PLM.',
'error.current_password_incorrect': "Le mot de passe actuel n'est pas correct.",
'error.password_too_short': 'Le nouveau mot de passe doit comporter au moins {minimum} caractères.',
'error.username_taken': "Ce nom d'utilisateur est déjà pris.",
'error.invalid_credentials': "Nom d'utilisateur ou mot de passe incorrect.",
'error.derivation_index_conflict': "L'inscription a échoué, veuillez réessayer.",
'error.session_expired': 'Session expirée, veuillez vous reconnecter.',
'error.invalid_request': 'Requête invalide, vérifiez les données saisies.',
'error.invalid_amount': 'Saisissez un montant supérieur à zéro.',
'loading.creating': 'Création…',
'loading.loggingIn': 'Connexion…',
'loading.sendingBet': 'Envoi de la mise…',
'loading.updating': 'Mise à jour…',
'loading.refreshing': '…',
'loading.sending': 'Envoi…',
},
de: {
'nav.ariaSections': 'Bereiche',
'nav.guideTitle': 'Anleitung',
'nav.guideAria': 'Benutzerhandbuch öffnen',
'nav.bugReport': 'Fehler melden',
'nav.logoutTitle': 'Abmelden',
'nav.logoutAria': 'Vom Konto abmelden',
'nav.deposit': 'Einzahlung',
'nav.bet': 'Wette',
'nav.withdraw': 'Auszahlung',
'nav.profile': 'Profil',
'chain.connecting': 'Verbindung wird hergestellt…',
'chain.block': 'Block {n}',
'chain.connectionLost': 'Verbindung zum Server verloren — erneuter Versuch…',
'maintenance.banner': 'Geplante Wartung: Die laufende Runde wird regulär abgeschlossen (Gewinner inklusive), aber die nächste Runde öffnet erst, wenn die Wartung beendet ist.',
'hero.lead': 'Zahle PLM ein, nimm mit einem festen Einsatz an der Runde teil, und wenn deine Zahl gezogen wird, gewinnst du den Jackpot.',
'hero.step1.title': '1. Einzahlen',
'hero.step1.hint': 'Erhalte deine persönliche PLM-Adresse, für immer deine',
'hero.step2.title': '2. Spielen',
'hero.step2.hint': 'Ein Einsatz mit Festpreis für die Teilnahme an der laufenden Runde',
'hero.step3.title': '3. Gewinnen',
'hero.step3.hint': 'Ziehung aus einem Block-Hash, Jackpot sofort gutgeschrieben',
'trust.fixedRate': 'Fester, offen genannter Einsatz',
'trust.blockHash': 'Ziehung aus einem Block-Hash',
'trust.freeWithdraw': 'Jederzeit freie Auszahlung',
'auth.tabLogin': 'Anmelden',
'auth.tabRegister': 'Registrieren',
'auth.username': 'Benutzername',
'auth.password': 'Passwort',
'auth.passwordConfirm': 'Passwort bestätigen',
'auth.loginBtn': 'Anmelden',
'auth.registerBtn': 'Konto erstellen',
'round.players': 'Spieler',
'round.jackpot': 'Jackpot',
'round.status.open': 'offen',
'round.status.closing': 'wird geschlossen',
'round.status.drawing': 'Ziehung läuft',
'round.status.paying_out': 'Auszahlung an den Gewinner läuft',
'round.title': 'Runde #{id} — {status}',
'round.none': 'Keine aktive Runde',
'chain.status.waiting': 'Warten auf die nächste Runde',
'chain.status.open': 'Runde offen',
'chain.status.closing': 'Runde geschlossen — warte auf Bestätigung der Wetten',
'chain.status.drawing': 'Ziehung läuft',
'chain.status.paying_out': 'Auszahlung an den Gewinner läuft',
'draw.defaultLabel': 'Gewinner wird gezogen…',
'draw.closing': 'Runde geschlossen — warte auf Bestätigung der letzten Wette, bevor der Gewinner gezogen wird…',
'draw.drawing': 'Warte auf den nächsten Block, um den Gewinner zu ziehen…',
'draw.payingOutBlock': 'Gewinner aus Block #{height} gezogen — Auszahlung an den Gewinner läuft…',
'draw.payingOut': 'Gewinner gezogen — Auszahlung an den Gewinner läuft…',
'result.win': '🎉 Du hast gewonnen! +{amount} PLM',
'result.lose': 'Diesmal kein Gewinn.',
'deposit.balanceTitle': 'Internes Guthaben',
'deposit.balanceHint': 'Aktualisiert nach 1 Netzwerkbestätigung',
'deposit.refreshBtn': 'Aktualisieren',
'deposit.refreshAria': 'Guthaben aktualisieren',
'deposit.addressTitle': 'Einzahlungsadresse',
'deposit.addressHint': 'Dies ist auch die Adresse, auf der du eventuelle Gewinne erhältst',
'deposit.copyAria': 'Adresse kopieren',
'deposit.qrAlt': 'QR-Code der Einzahlungsadresse',
'bet.title': 'Wette',
'bet.hint': 'Fester Einsatz für die laufende Runde',
'bet.button': 'Wette platzieren ({amount} PLM)',
'bet.buttonNoAmount': 'Wette platzieren',
'withdraw.title': 'Auszahlung',
'withdraw.hint': 'Sende Guthaben an eine externe PLM-Adresse',
'withdraw.addressLabel': 'Externe Adresse',
'withdraw.addressHint': 'Nur P2WPKH-Bech32-Adressen (beginnend mit plm1q...). Legacy-Adressen (P...) oder P2SH werden nicht unterstützt.',
'withdraw.amountLabel': 'Betrag (PLM)',
'withdraw.amountPlaceholder': 'z. B. 2',
'withdraw.fullAmountPrefix': 'Gesamten Betrag auszahlen (',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Auszahlen',
'profile.title': 'Profil',
'profile.hint': 'Deine Kontoinformationen',
'profile.usernameLabel': 'Benutzername',
'profile.addressLabel': 'Einzahlungsadresse',
'profile.balanceLabel': 'Internes Guthaben',
'profile.createdLabel': 'Nutzer seit',
'settings.title': 'Einstellungen',
'settings.hint': 'Ändere das Passwort deines Kontos',
'settings.currentPasswordLabel': 'Aktuelles Passwort',
'settings.newPasswordLabel': 'Neues Passwort',
'settings.newPasswordConfirmLabel': 'Neues Passwort bestätigen',
'settings.updateBtn': 'Passwort aktualisieren',
'toast.passwordMismatch': 'Die Passwörter stimmen nicht überein.',
'toast.accountCreated': 'Konto erstellt.',
'toast.loginSuccess': 'Anmeldung erfolgreich.',
'toast.requestTimeout': 'Anfrage an den Server ist abgelaufen.',
'toast.addressCopied': 'Adresse kopiert.',
'toast.copyFailed': 'Automatisches Kopieren fehlgeschlagen.',
'toast.newPasswordMismatch': 'Die neuen Passwörter stimmen nicht überein.',
'toast.passwordTooShort': 'Das neue Passwort muss mindestens 8 Zeichen lang sein.',
'toast.passwordUpdated': 'Passwort aktualisiert.',
'toast.betPlaced': 'Wette auf Runde #{id} platziert.',
'toast.roundWon': 'Du hast Runde #{id} gewonnen! +{amount} PLM',
'toast.withdrawSent': 'Auszahlung gesendet.',
'error.network_unavailable': 'Keine Verbindung zum Netzwerk, bitte versuche es gleich erneut.',
'error.no_round_open': 'Derzeit ist keine Runde offen, bitte versuche es gleich erneut.',
'error.round_closing': 'Die laufende Runde wird gerade geschlossen, bitte versuche es gleich erneut.',
'error.already_betting': 'Du hast bereits eine aktive Wette in der laufenden Runde.',
'error.insufficient_balance': 'Nicht genügend Guthaben.',
'error.amount_below_network_fee': 'Der Betrag ist zu klein, um die Netzwerkgebühr zu decken.',
'error.invalid_address': 'Keine gültige PLM-Adresse (sie muss mit plm1q… beginnen).',
'error.amount_below_minimum': 'Der Mindestauszahlungsbetrag beträgt {minimum_plm} PLM.',
'error.current_password_incorrect': 'Das aktuelle Passwort ist nicht korrekt.',
'error.password_too_short': 'Das neue Passwort muss mindestens {minimum} Zeichen lang sein.',
'error.username_taken': 'Dieser Benutzername ist bereits vergeben.',
'error.invalid_credentials': 'Benutzername oder Passwort falsch.',
'error.derivation_index_conflict': 'Registrierung fehlgeschlagen, bitte erneut versuchen.',
'error.session_expired': 'Sitzung abgelaufen, bitte melde dich erneut an.',
'error.invalid_request': 'Ungültige Anfrage, bitte überprüfe die eingegebenen Daten.',
'error.invalid_amount': 'Gib einen Betrag größer als null ein.',
'loading.creating': 'Wird erstellt…',
'loading.loggingIn': 'Anmeldung…',
'loading.sendingBet': 'Wette wird gesendet…',
'loading.updating': 'Aktualisierung…',
'loading.refreshing': '…',
'loading.sending': 'Wird gesendet…',
},
ru: {
'nav.ariaSections': 'Разделы',
'nav.guideTitle': 'Инструкция',
'nav.guideAria': 'Открыть руководство пользователя',
'nav.bugReport': 'Сообщить об ошибке',
'nav.logoutTitle': 'Выйти',
'nav.logoutAria': 'Выйти из аккаунта',
'nav.deposit': 'Депозит',
'nav.bet': 'Ставка',
'nav.withdraw': 'Вывод',
'nav.profile': 'Профиль',
'chain.connecting': 'Подключение…',
'chain.block': 'Блок {n}',
'chain.connectionLost': 'Соединение с сервером потеряно — повторная попытка…',
'maintenance.banner': 'Запланировано техобслуживание: текущий раунд завершится в обычном порядке (включая победителя), но следующий раунд не откроется до окончания техобслуживания.',
'hero.lead': 'Внесите PLM, вступите в раунд с фиксированной ставкой, и если выпадет ваш номер — вы выиграете джекпот.',
'hero.step1.title': '1. Внесите депозит',
'hero.step1.hint': 'Получите личный адрес PLM, ваш навсегда',
'hero.step2.title': '2. Играйте',
'hero.step2.hint': 'Ставка с фиксированной стоимостью для участия в текущем раунде',
'hero.step3.title': '3. Выигрывайте',
'hero.step3.hint': 'Розыгрыш по хешу блока, джекпот начисляется сразу',
'trust.fixedRate': 'Заявленная фиксированная ставка',
'trust.blockHash': 'Розыгрыш по хешу блока',
'trust.freeWithdraw': 'Свободный вывод средств в любой момент',
'auth.tabLogin': 'Вход',
'auth.tabRegister': 'Регистрация',
'auth.username': 'Имя пользователя',
'auth.password': 'Пароль',
'auth.passwordConfirm': 'Подтвердите пароль',
'auth.loginBtn': 'Войти',
'auth.registerBtn': 'Создать аккаунт',
'round.players': 'Игроки',
'round.jackpot': 'Джекпот',
'round.status.open': 'открыт',
'round.status.closing': 'закрывается',
'round.status.drawing': 'идёт розыгрыш',
'round.status.paying_out': 'выплата победителю',
'round.title': 'Раунд #{id} — {status}',
'round.none': 'Нет активного раунда',
'chain.status.waiting': 'Ожидание следующего раунда',
'chain.status.open': 'Раунд открыт',
'chain.status.closing': 'Раунд закрыт — ожидание подтверждения ставок',
'chain.status.drawing': 'Идёт розыгрыш',
'chain.status.paying_out': 'Выплата победителю',
'draw.defaultLabel': 'Розыгрыш победителя…',
'draw.closing': 'Раунд закрыт — ожидание подтверждения последней ставки перед розыгрышем победителя…',
'draw.drawing': 'Ожидание следующего блока для розыгрыша победителя…',
'draw.payingOutBlock': 'Победитель определён по блоку #{height} — выплата победителю…',
'draw.payingOut': 'Победитель определён — выплата победителю…',
'result.win': '🎉 Вы выиграли! +{amount} PLM',
'result.lose': 'На этот раз без выигрыша.',
'deposit.balanceTitle': 'Внутренний баланс',
'deposit.balanceHint': 'Обновляется после 1 подтверждения в сети',
'deposit.refreshBtn': 'Обновить',
'deposit.refreshAria': 'Обновить баланс',
'deposit.addressTitle': 'Адрес для депозита',
'deposit.addressHint': 'Это также адрес, на который вы получаете возможные выигрыши',
'deposit.copyAria': 'Скопировать адрес',
'deposit.qrAlt': 'QR-код адреса для депозита',
'bet.title': 'Ставка',
'bet.hint': 'Фиксированный вход в текущий раунд',
'bet.button': 'Сделать ставку ({amount} PLM)',
'bet.buttonNoAmount': 'Сделать ставку',
'withdraw.title': 'Вывод средств',
'withdraw.hint': 'Отправьте средства на внешний адрес PLM',
'withdraw.addressLabel': 'Внешний адрес',
'withdraw.addressHint': 'Только адреса P2WPKH bech32 (начинающиеся с plm1q...). Устаревшие адреса (P...) или P2SH не поддерживаются.',
'withdraw.amountLabel': 'Сумма (PLM)',
'withdraw.amountPlaceholder': 'напр. 2',
'withdraw.fullAmountPrefix': 'Вывести всю сумму (',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': 'Вывести',
'profile.title': 'Профиль',
'profile.hint': 'Информация о вашем аккаунте',
'profile.usernameLabel': 'Имя пользователя',
'profile.addressLabel': 'Адрес для депозита',
'profile.balanceLabel': 'Внутренний баланс',
'profile.createdLabel': 'Пользователь с',
'settings.title': 'Настройки',
'settings.hint': 'Измените пароль вашего аккаунта',
'settings.currentPasswordLabel': 'Текущий пароль',
'settings.newPasswordLabel': 'Новый пароль',
'settings.newPasswordConfirmLabel': 'Подтвердите новый пароль',
'settings.updateBtn': 'Обновить пароль',
'toast.passwordMismatch': 'Пароли не совпадают.',
'toast.accountCreated': 'Аккаунт создан.',
'toast.loginSuccess': 'Вход выполнен успешно.',
'toast.requestTimeout': 'Истекло время ожидания ответа сервера.',
'toast.addressCopied': 'Адрес скопирован.',
'toast.copyFailed': 'Не удалось скопировать автоматически.',
'toast.newPasswordMismatch': 'Новые пароли не совпадают.',
'toast.passwordTooShort': 'Новый пароль должен содержать не менее 8 символов.',
'toast.passwordUpdated': 'Пароль обновлён.',
'toast.betPlaced': 'Ставка сделана на раунд #{id}.',
'toast.roundWon': 'Вы выиграли раунд #{id}! +{amount} PLM',
'toast.withdrawSent': 'Вывод средств отправлен.',
'error.network_unavailable': 'Нет соединения с сетью, повторите попытку чуть позже.',
'error.no_round_open': 'Сейчас нет открытого раунда, повторите попытку чуть позже.',
'error.round_closing': 'Текущий раунд закрывается, повторите попытку чуть позже.',
'error.already_betting': 'У вас уже есть активная ставка в текущем раунде.',
'error.insufficient_balance': 'Недостаточно средств.',
'error.amount_below_network_fee': 'Сумма слишком мала, чтобы покрыть комиссию сети.',
'error.invalid_address': 'Некорректный адрес PLM (он должен начинаться с plm1q…).',
'error.amount_below_minimum': 'Минимальная сумма вывода — {minimum_plm} PLM.',
'error.current_password_incorrect': 'Текущий пароль указан неверно.',
'error.password_too_short': 'Новый пароль должен содержать не менее {minimum} символов.',
'error.username_taken': 'Это имя пользователя уже занято.',
'error.invalid_credentials': 'Неверное имя пользователя или пароль.',
'error.derivation_index_conflict': 'Не удалось завершить регистрацию, попробуйте ещё раз.',
'error.session_expired': 'Сессия истекла, войдите снова.',
'error.invalid_request': 'Некорректный запрос, проверьте введённые данные.',
'error.invalid_amount': 'Введите сумму больше нуля.',
'loading.creating': 'Создание…',
'loading.loggingIn': 'Вход…',
'loading.sendingBet': 'Отправка ставки…',
'loading.updating': 'Обновление…',
'loading.refreshing': '…',
'loading.sending': 'Отправка…',
},
zh: {
'nav.ariaSections': '导航区',
'nav.guideTitle': '指南',
'nav.guideAria': '打开用户指南',
'nav.bugReport': '报告问题',
'nav.logoutTitle': '退出登录',
'nav.logoutAria': '退出账户',
'nav.deposit': '存款',
'nav.bet': '下注',
'nav.withdraw': '提现',
'nav.profile': '个人资料',
'chain.connecting': '正在连接…',
'chain.block': '区块 {n}',
'chain.connectionLost': '与服务器的连接已断开——正在重试…',
'maintenance.banner': '计划维护:当前回合将照常完成(包括中奖者),但下一回合要等维护结束后才会开启。',
'hero.lead': '存入 PLM,以固定金额参与本回合,若抽中你的号码即可赢得奖池。',
'hero.step1.title': '1. 存款',
'hero.step1.hint': '获得专属于你的 PLM 地址,永久有效',
'hero.step2.title': '2. 参与',
'hero.step2.hint': '以固定金额下注即可参与当前回合',
'hero.step3.title': '3. 获胜',
'hero.step3.hint': '根据区块哈希开奖,奖金即时到账',
'trust.fixedRate': '公开声明的固定下注金额',
'trust.blockHash': '根据区块哈希开奖',
'trust.freeWithdraw': '随时自由提现',
'auth.tabLogin': '登录',
'auth.tabRegister': '注册',
'auth.username': '用户名',
'auth.password': '密码',
'auth.passwordConfirm': '确认密码',
'auth.loginBtn': '登录',
'auth.registerBtn': '创建账户',
'round.players': '参与人数',
'round.jackpot': '奖池',
'round.status.open': '进行中',
'round.status.closing': '即将结束',
'round.status.drawing': '正在开奖',
'round.status.paying_out': '正在向中奖者付款',
'round.title': '第 {id} 回合 — {status}',
'round.none': '当前没有进行中的回合',
'chain.status.waiting': '等待下一回合开启',
'chain.status.open': '回合进行中',
'chain.status.closing': '回合已结束——等待下注确认',
'chain.status.drawing': '正在开奖',
'chain.status.paying_out': '正在向中奖者付款',
'draw.defaultLabel': '正在抽取中奖者…',
'draw.closing': '回合已结束——在开奖前等待最后一笔下注确认…',
'draw.drawing': '等待下一个区块以抽取中奖者…',
'draw.payingOutBlock': '已从区块 #{height} 抽取中奖者——正在向中奖者付款…',
'draw.payingOut': '中奖者已确定——正在向中奖者付款…',
'result.win': '🎉 恭喜你赢了!+{amount} PLM',
'result.lose': '这次没有中奖。',
'deposit.balanceTitle': '内部余额',
'deposit.balanceHint': '在网络确认 1 次后更新',
'deposit.refreshBtn': '刷新',
'deposit.refreshAria': '刷新余额',
'deposit.addressTitle': '存款地址',
'deposit.addressHint': '这也是接收任何奖金的地址',
'deposit.copyAria': '复制地址',
'deposit.qrAlt': '存款地址的二维码',
'bet.title': '下注',
'bet.hint': '以固定金额参与当前回合',
'bet.button': '下注({amount} PLM)',
'bet.buttonNoAmount': '下注',
'withdraw.title': '提现',
'withdraw.hint': '将资金发送到外部 PLM 地址',
'withdraw.addressLabel': '外部地址',
'withdraw.addressHint': '仅支持 P2WPKH bech32 地址(以 plm1q... 开头)。不支持传统地址(P...)或 P2SH 地址。',
'withdraw.amountLabel': '金额(PLM)',
'withdraw.amountPlaceholder': '例如 2',
'withdraw.fullAmountPrefix': '提取全部金额(',
'withdraw.fullAmountSuffix': ' PLM)',
'withdraw.button': '提现',
'profile.title': '个人资料',
'profile.hint': '你的账户信息',
'profile.usernameLabel': '用户名',
'profile.addressLabel': '存款地址',
'profile.balanceLabel': '内部余额',
'profile.createdLabel': '注册时间',
'settings.title': '设置',
'settings.hint': '修改你的账户密码',
'settings.currentPasswordLabel': '当前密码',
'settings.newPasswordLabel': '新密码',
'settings.newPasswordConfirmLabel': '确认新密码',
'settings.updateBtn': '更新密码',
'toast.passwordMismatch': '两次输入的密码不一致。',
'toast.accountCreated': '账户已创建。',
'toast.loginSuccess': '登录成功。',
'toast.requestTimeout': '服务器请求超时。',
'toast.addressCopied': '地址已复制。',
'toast.copyFailed': '自动复制失败。',
'toast.newPasswordMismatch': '两次输入的新密码不一致。',
'toast.passwordTooShort': '新密码长度至少需要 8 个字符。',
'toast.passwordUpdated': '密码已更新。',
'toast.betPlaced': '已在第 {id} 回合下注。',
'toast.roundWon': '你赢得了第 {id} 回合!+{amount} PLM',
'toast.withdrawSent': '提现已发送。',
'error.network_unavailable': '未连接到网络,请稍后重试。',
'error.no_round_open': '当前没有开放的回合,请稍后重试。',
'error.round_closing': '当前回合正在结束,请稍后重试。',
'error.already_betting': '你在当前回合已有一笔有效下注。',
'error.insufficient_balance': '余额不足。',
'error.amount_below_network_fee': '金额太小,不足以支付网络手续费。',
'error.invalid_address': 'PLM 地址无效(必须以 plm1q… 开头)。',
'error.amount_below_minimum': '最低提现金额为 {minimum_plm} PLM。',
'error.current_password_incorrect': '当前密码不正确。',
'error.password_too_short': '新密码长度至少需要 {minimum} 个字符。',
'error.username_taken': '该用户名已被占用。',
'error.invalid_credentials': '用户名或密码错误。',
'error.derivation_index_conflict': '注册失败,请重试。',
'error.session_expired': '会话已过期,请重新登录。',
'error.invalid_request': '请求无效,请检查填写的内容。',
'error.invalid_amount': '请输入大于零的金额。',
'loading.creating': '正在创建…',
'loading.loggingIn': '正在登录…',
'loading.sendingBet': '正在下注…',
'loading.updating': '正在更新…',
'loading.refreshing': '…',
'loading.sending': '正在发送…',
},
};
function detectDefaultLang() {
const saved = localStorage.getItem(LANG_STORAGE_KEY);
if (saved && SUPPORTED_LANGS.includes(saved)) return saved;
const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
return SUPPORTED_LANGS.includes(nav) ? nav : 'en';
}
let currentLang = detectDefaultLang();
function interpolate(str, params) {
if (!params) return str;
for (const [k, v] of Object.entries(params)) str = str.replaceAll('{' + k + '}', v);
return str;
}
function t(key, params) {
const dict = TRANSLATIONS[currentLang] || TRANSLATIONS.en;
return interpolate(dict[key] ?? TRANSLATIONS.en[key] ?? key, params);
}
// Like t(), but returns null instead of echoing the key back when nothing is
// defined for it — lets a caller fall back to a string of its own (e.g. the
// API's English message for an error code this build doesn't know yet).
function tOrNull(key, params) {
const str = TRANSLATIONS[currentLang]?.[key] ?? TRANSLATIONS.en[key];
return str === undefined ? null : interpolate(str, params);
}
function currentDateLocale() {
return DATE_LOCALES[currentLang] || 'en-US';
}
// `root` defaults to the whole document; pass a subtree to re-translate just
// that part (see withLoading in app.js, which restores a button's markup from a
// snapshot that may predate a language change).
function applyStaticTranslations(root) {
const scope = root instanceof Element || root instanceof Document ? root : document;
const each = (selector, fn) => {
if (scope !== document && scope.matches?.(selector)) fn(scope);
scope.querySelectorAll(selector).forEach(fn);
};
each('[data-i18n]', (el) => { el.textContent = t(el.getAttribute('data-i18n')); });
each('[data-i18n-html]', (el) => { el.innerHTML = t(el.getAttribute('data-i18n-html')); });
each('[data-i18n-placeholder]', (el) => { el.placeholder = t(el.getAttribute('data-i18n-placeholder')); });
each('[data-i18n-title]', (el) => { el.title = t(el.getAttribute('data-i18n-title')); });
each('[data-i18n-aria-label]', (el) => { el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label'))); });
each('[data-i18n-alt]', (el) => { el.alt = t(el.getAttribute('data-i18n-alt')); });
if (scope !== document) return;
document.documentElement.lang = currentLang;
const switcher = document.getElementById('lang-switcher');
if (switcher) switcher.value = currentLang;
}
function setLanguage(lang) {
if (!SUPPORTED_LANGS.includes(lang) || lang === currentLang) return;
currentLang = lang;
localStorage.setItem(LANG_STORAGE_KEY, lang);
applyStaticTranslations();
if (typeof onLanguageChange === 'function') onLanguageChange();
}
document.addEventListener('DOMContentLoaded', applyStaticTranslations);