Three failure paths that ended at an English HTTP status line or at no
recovery at all:
An empty or non-numeric withdrawal amount parsed to NaN, which JSON.stringify
sends as null, which pydantic rejects with a 422 — and FastAPI's validation
errors use a list of field objects rather than the {code, message} shape, so
apiErrorMessage fell through to res.statusText and the user read
"Unprocessable Content". The amount is now checked before the request, and the
list shape maps to a translated "invalid request" as a backstop for any other
field that fails validation.
A token the server no longer accepts left the dashboard looking logged in
while every poll failed, re-toasting "session expired" indefinitely. call()
now logs out on that specific code, dropping back to the login form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1000 lines
52 KiB
JavaScript
1000 lines
52 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.',
|
||
|
||
// 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 <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.',
|
||
|
||
'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 <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.',
|
||
|
||
'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 <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é.',
|
||
|
||
'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 <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.',
|
||
|
||
'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 (начинающиеся с <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': 'Вывод средств отправлен.',
|
||
|
||
'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 地址(以 <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': '提现已发送。',
|
||
|
||
'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);
|