Adds app/static/i18n.js: a flat key -> string table for en/it/es/fr/de/ru/zh, loaded before app.js so t() is available everywhere. No build step and no fetch, consistent with the rest of these static pages. Language comes from localStorage, then navigator.language, then en. Static markup is translated by attribute (data-i18n and its -html/-placeholder/ -title/-aria-label/-alt variants); anything rendered from server data goes through t() in app.js and is re-rendered by onLanguageChange(). An element belongs to one mechanism or the other, never both, or the two overwrite each other — which is why #bet-btn has no data-i18n: its label carries the admin-configurable bet amount, so renderBetButton() owns it and reads the amount from /rounds/current instead of hardcoding "10 PLM" in seven files. The switcher sits in the chain-bar rather than the navbar because the navbar is hidden until login, which would leave the landing page and the login form untranslatable for exactly the users who need to switch. It uses language names rather than flag emoji: flags don't render on every platform and don't map one-to-one onto languages. withLoading now snapshots innerHTML instead of textContent — several of these buttons wrap an <svg> plus a <span data-i18n>, both of which a textContent round-trip flattened away, permanently losing the icon and the translation hook. It re-applies translations to the restored subtree in case the language changed while the request was in flight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
870 lines
42 KiB
JavaScript
870 lines
42 KiB
JavaScript
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 <code>plm1q...</code>). Legacy (<code>P...</code>) 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.',
|
||
|
||
'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 <code>plm1q...</code>). Indirizzi legacy (<code>P...</code>) 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.',
|
||
|
||
'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 <code>plm1q...</code>). No se admiten direcciones legacy (<code>P...</code>) 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.',
|
||
|
||
'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 <code>plm1q...</code>). Les adresses legacy (<code>P...</code>) 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é.',
|
||
|
||
'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 <code>plm1q...</code>). Legacy-Adressen (<code>P...</code>) 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.',
|
||
|
||
'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 (начинающиеся с <code>plm1q...</code>). Устаревшие адреса (<code>P...</code>) или 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': 'Вывод средств отправлен.',
|
||
|
||
'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 地址(以 <code>plm1q...</code> 开头)。不支持传统地址(<code>P...</code>)或 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': '提现已发送。',
|
||
|
||
'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);
|
||
}
|
||
|
||
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);
|