The payout has to spend one pool UTXO per bet, so reusing MAX_TX_INPUTS (50) for it made any round past ~50 players unpayable: select_utxos raised too_many_inputs, the round stayed "paying_out" retrying every 60s forever, and since no new round may open while one is active, the whole lottery stopped with the pool stuck. The cap was being enforced on the payout side, i.e. discovered once the money was already committed and there was no way back. Two halves: - select_utxos takes the cap as a parameter. Bets and withdrawals keep MAX_TX_INPUTS = 50, which protects a user from a fee that eats into the amount they are moving; the payout uses MAX_PAYOUT_TX_INPUTS = 500, where that argument doesn't apply — 400 inputs at 1 sat/vB cost ~0.00027 PLM out of the winner's 70% share. What actually bounds it is relay policy: 500 inputs is ~34 kvB against the 100 kvB standardness limit, and signing that many measures ~0.4s, once per round, inside a background task. - place_bet refuses the 401st bet with a new round_full error (translated into all 7 languages), so "a round can always be paid out" is an invariant checked before any money moves. MAX_PARTICIPANTS_PER_ROUND sits below the input cap to leave the payout headroom for pool change from earlier rounds, and counts every participant row rather than only confirmed ones, since a failed bet frees a slot. A round already wedged past the old cap now pays out on the next retry tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1203 lines
70 KiB
JavaScript
1203 lines
70 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',
|
||
'bugReport.pageTitle': 'Report a bug',
|
||
'bugReport.heading': 'Report a bug',
|
||
'bugReport.intro': 'Found a problem? Describe it below — your report goes straight to the admin panel.',
|
||
'bugReport.englishNotice': "Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster.",
|
||
'bugReport.descriptionLabel': 'What happened?',
|
||
'bugReport.descriptionPlaceholder': 'Describe the bug: what you were doing, what you expected, and what happened instead.',
|
||
'bugReport.contactLabel': 'Contact (optional)',
|
||
'bugReport.contactPlaceholder': "Email or other contact, if you'd like a reply",
|
||
'bugReport.submitBtn': 'Send report',
|
||
'bugReport.submitting': 'Sending…',
|
||
'bugReport.blankError': 'Describe the bug before sending.',
|
||
'bugReport.successToast': 'Thanks! Report sent.',
|
||
'bugReport.errorPrefix': 'Error sending: ',
|
||
'bugReport.myReportsTitle': 'Your reports',
|
||
'bugReport.myReportsHint': 'Only reports sent from this account, with the status set by the admin team.',
|
||
'bugReport.myReportsEmpty': "You haven't sent any reports yet.",
|
||
'bugReport.statusOpen': 'Not read yet',
|
||
'bugReport.statusRead': 'Acknowledged',
|
||
'bugReport.statusResolved': 'Resolved',
|
||
'bugReport.backLink': 'Back to home',
|
||
'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.round_full': 'This round has reached its maximum of {max_participants} players — wait for the next one, it opens shortly.',
|
||
'error.already_betting': 'You already have an active bet in the current round.',
|
||
'error.insufficient_balance': 'Insufficient balance.',
|
||
'error.balance_pending_confirmation': 'You have {pending_plm} PLM pending confirmation — it is not spendable yet.',
|
||
'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.',
|
||
'error.broadcast_failed': 'The network refused the transaction. Please try again shortly.',
|
||
'error.amount_below_dust_limit': 'The amount is too small to be sent.',
|
||
'error.too_many_inputs': 'Your balance is split across too many small deposits to be spent in a single transaction (max {max_inputs}). Please contact support to consolidate it.',
|
||
'error.withdrawal_to_own_address': 'That is your own deposit address — withdraw to an external wallet.',
|
||
'error.internal_error': 'Unexpected server error. Please try again shortly.',
|
||
'error.guide_unavailable': 'The guide is not available right now.',
|
||
'error.rate_limited': 'Too many attempts, please try again in {retry_after_seconds} seconds.',
|
||
|
||
'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',
|
||
'bugReport.pageTitle': 'Segnala un bug',
|
||
'bugReport.heading': 'Segnala un bug',
|
||
'bugReport.intro': 'Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.',
|
||
'bugReport.englishNotice': "Scrivi la segnalazione in inglese, indipendentemente dalla lingua che stai usando per navigare: questo ci aiuta a gestirla più velocemente.",
|
||
'bugReport.descriptionLabel': 'Cosa è successo?',
|
||
'bugReport.descriptionPlaceholder': 'Descrivi il bug: cosa stavi facendo, cosa ti aspettavi e cosa è successo invece.',
|
||
'bugReport.contactLabel': 'Contatto (opzionale)',
|
||
'bugReport.contactPlaceholder': 'Email o altro recapito, se vuoi essere ricontattato',
|
||
'bugReport.submitBtn': 'Invia segnalazione',
|
||
'bugReport.submitting': 'Invio…',
|
||
'bugReport.blankError': 'Descrivi il bug prima di inviare.',
|
||
'bugReport.successToast': 'Grazie! Segnalazione inviata.',
|
||
'bugReport.errorPrefix': "Errore nell'invio: ",
|
||
'bugReport.myReportsTitle': 'Le tue segnalazioni',
|
||
'bugReport.myReportsHint': "Solo le segnalazioni inviate da questo account, con lo stato aggiornato dall'amministrazione.",
|
||
'bugReport.myReportsEmpty': 'Non hai ancora inviato segnalazioni.',
|
||
'bugReport.statusOpen': 'Da leggere',
|
||
'bugReport.statusRead': 'Presa in carico',
|
||
'bugReport.statusResolved': 'Risolta',
|
||
'bugReport.backLink': 'Torna alla home',
|
||
'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.round_full': 'Questo round ha raggiunto il massimo di {max_participants} giocatori — aspetta il prossimo, si apre tra poco.',
|
||
'error.already_betting': 'Hai già una bet attiva nel round corrente.',
|
||
'error.insufficient_balance': 'Saldo insufficiente.',
|
||
'error.balance_pending_confirmation': 'Hai {pending_plm} PLM in attesa di conferma — non ancora disponibili per la spesa.',
|
||
'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.',
|
||
'error.broadcast_failed': 'La rete ha rifiutato la transazione. Riprova tra poco.',
|
||
'error.amount_below_dust_limit': "L'importo è troppo basso per essere inviato.",
|
||
'error.too_many_inputs': 'Il tuo saldo è suddiviso in troppi piccoli depositi per essere speso in una sola transazione (max {max_inputs}). Contatta l\'assistenza per consolidarlo.',
|
||
'error.withdrawal_to_own_address': 'Questo è il tuo indirizzo di deposito — preleva verso un wallet esterno.',
|
||
'error.internal_error': 'Errore inatteso del server. Riprova tra poco.',
|
||
'error.guide_unavailable': 'La guida non è disponibile in questo momento.',
|
||
'error.rate_limited': 'Troppi tentativi, riprova tra {retry_after_seconds} secondi.',
|
||
|
||
'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',
|
||
'bugReport.pageTitle': 'Reportar un error',
|
||
'bugReport.heading': 'Reportar un error',
|
||
'bugReport.intro': '¿Encontraste un problema? Descríbelo a continuación: el informe llega directamente al panel de administración.',
|
||
'bugReport.englishNotice': 'Escribe el informe en inglés, independientemente del idioma que estés usando para navegar: esto nos ayuda a gestionarlo más rápido.',
|
||
'bugReport.descriptionLabel': '¿Qué pasó?',
|
||
'bugReport.descriptionPlaceholder': 'Describe el error: qué estabas haciendo, qué esperabas y qué sucedió en su lugar.',
|
||
'bugReport.contactLabel': 'Contacto (opcional)',
|
||
'bugReport.contactPlaceholder': 'Correo u otro contacto, si quieres que te respondamos',
|
||
'bugReport.submitBtn': 'Enviar informe',
|
||
'bugReport.submitting': 'Enviando…',
|
||
'bugReport.blankError': 'Describe el error antes de enviarlo.',
|
||
'bugReport.successToast': '¡Gracias! Informe enviado.',
|
||
'bugReport.errorPrefix': 'Error al enviar: ',
|
||
'bugReport.myReportsTitle': 'Tus informes',
|
||
'bugReport.myReportsHint': 'Solo los informes enviados desde esta cuenta, con el estado actualizado por el equipo de administración.',
|
||
'bugReport.myReportsEmpty': 'Todavía no has enviado ningún informe.',
|
||
'bugReport.statusOpen': 'Sin leer',
|
||
'bugReport.statusRead': 'En curso',
|
||
'bugReport.statusResolved': 'Resuelto',
|
||
'bugReport.backLink': 'Volver al inicio',
|
||
'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.round_full': 'Esta ronda ha alcanzado su máximo de {max_participants} jugadores: espera la siguiente, se abre en breve.',
|
||
'error.already_betting': 'Ya tienes una apuesta activa en la ronda actual.',
|
||
'error.insufficient_balance': 'Saldo insuficiente.',
|
||
'error.balance_pending_confirmation': 'Tienes {pending_plm} PLM pendientes de confirmación — todavía no se pueden gastar.',
|
||
'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.',
|
||
'error.broadcast_failed': 'La red rechazó la transacción. Inténtalo de nuevo en un momento.',
|
||
'error.amount_below_dust_limit': 'El importe es demasiado pequeño para enviarse.',
|
||
'error.too_many_inputs': 'Tu saldo está repartido en demasiados depósitos pequeños para gastarse en una sola transacción (máx. {max_inputs}). Contacta con soporte para consolidarlo.',
|
||
'error.withdrawal_to_own_address': 'Esa es tu propia dirección de depósito — retira a una cartera externa.',
|
||
'error.internal_error': 'Error inesperado del servidor. Inténtalo de nuevo en un momento.',
|
||
'error.guide_unavailable': 'La guía no está disponible en este momento.',
|
||
'error.rate_limited': 'Demasiados intentos, inténtalo de nuevo en {retry_after_seconds} segundos.',
|
||
|
||
'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',
|
||
'bugReport.pageTitle': 'Signaler un bug',
|
||
'bugReport.heading': 'Signaler un bug',
|
||
'bugReport.intro': "Vous avez trouvé un problème ? Décrivez-le ci-dessous : le signalement arrive directement dans le panneau d'administration.",
|
||
'bugReport.englishNotice': "Rédigez votre signalement en anglais, quelle que soit la langue que vous utilisez pour naviguer : cela nous aide à le traiter plus rapidement.",
|
||
'bugReport.descriptionLabel': "Que s'est-il passé ?",
|
||
'bugReport.descriptionPlaceholder': "Décrivez le bug : ce que vous faisiez, ce que vous attendiez et ce qui s'est passé à la place.",
|
||
'bugReport.contactLabel': 'Contact (facultatif)',
|
||
'bugReport.contactPlaceholder': 'Email ou autre contact, si vous souhaitez une réponse',
|
||
'bugReport.submitBtn': 'Envoyer le signalement',
|
||
'bugReport.submitting': 'Envoi…',
|
||
'bugReport.blankError': "Décrivez le bug avant d'envoyer.",
|
||
'bugReport.successToast': 'Merci ! Signalement envoyé.',
|
||
'bugReport.errorPrefix': "Erreur lors de l'envoi : ",
|
||
'bugReport.myReportsTitle': 'Vos signalements',
|
||
'bugReport.myReportsHint': "Seulement les signalements envoyés depuis ce compte, avec le statut mis à jour par l'équipe d'administration.",
|
||
'bugReport.myReportsEmpty': "Vous n'avez encore envoyé aucun signalement.",
|
||
'bugReport.statusOpen': 'Non lu',
|
||
'bugReport.statusRead': 'Prise en charge',
|
||
'bugReport.statusResolved': 'Résolu',
|
||
'bugReport.backLink': "Retour à l'accueil",
|
||
'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.round_full': 'Ce tour a atteint son maximum de {max_participants} joueurs — attendez le prochain, il ouvre dans un instant.',
|
||
'error.already_betting': 'Vous avez déjà une mise active dans le round en cours.',
|
||
'error.insufficient_balance': 'Solde insuffisant.',
|
||
'error.balance_pending_confirmation': 'Vous avez {pending_plm} PLM en attente de confirmation — pas encore disponibles.',
|
||
'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.',
|
||
'error.broadcast_failed': 'Le réseau a refusé la transaction. Veuillez réessayer dans un instant.',
|
||
'error.amount_below_dust_limit': "Le montant est trop faible pour être envoyé.",
|
||
'error.too_many_inputs': 'Votre solde est réparti sur trop de petits dépôts pour être dépensé en une seule transaction (max {max_inputs}). Contactez le support pour le consolider.',
|
||
'error.withdrawal_to_own_address': "C'est votre propre adresse de dépôt — retirez vers un portefeuille externe.",
|
||
'error.internal_error': 'Erreur inattendue du serveur. Veuillez réessayer dans un instant.',
|
||
'error.guide_unavailable': "Le guide n'est pas disponible pour le moment.",
|
||
'error.rate_limited': 'Trop de tentatives, réessayez dans {retry_after_seconds} secondes.',
|
||
|
||
'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',
|
||
'bugReport.pageTitle': 'Fehler melden',
|
||
'bugReport.heading': 'Fehler melden',
|
||
'bugReport.intro': 'Ein Problem gefunden? Beschreibe es unten — die Meldung geht direkt an das Admin-Panel.',
|
||
'bugReport.englishNotice': 'Bitte schreibe die Fehlermeldung auf Englisch, unabhängig von der Sprache, die du gerade verwendest — das hilft uns, sie schneller zu bearbeiten.',
|
||
'bugReport.descriptionLabel': 'Was ist passiert?',
|
||
'bugReport.descriptionPlaceholder': 'Beschreibe den Fehler: was du getan hast, was du erwartet hast und was stattdessen passiert ist.',
|
||
'bugReport.contactLabel': 'Kontakt (optional)',
|
||
'bugReport.contactPlaceholder': 'E-Mail oder anderer Kontakt, falls du eine Antwort möchtest',
|
||
'bugReport.submitBtn': 'Meldung senden',
|
||
'bugReport.submitting': 'Senden…',
|
||
'bugReport.blankError': 'Beschreibe den Fehler, bevor du sendest.',
|
||
'bugReport.successToast': 'Danke! Meldung gesendet.',
|
||
'bugReport.errorPrefix': 'Fehler beim Senden: ',
|
||
'bugReport.myReportsTitle': 'Deine Meldungen',
|
||
'bugReport.myReportsHint': 'Nur Meldungen, die von diesem Konto gesendet wurden, mit dem vom Admin-Team aktualisierten Status.',
|
||
'bugReport.myReportsEmpty': 'Du hast noch keine Meldungen gesendet.',
|
||
'bugReport.statusOpen': 'Ungelesen',
|
||
'bugReport.statusRead': 'In Bearbeitung',
|
||
'bugReport.statusResolved': 'Gelöst',
|
||
'bugReport.backLink': 'Zurück zur Startseite',
|
||
'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.round_full': 'Diese Runde hat ihr Maximum von {max_participants} Spielern erreicht — warten Sie auf die nächste, sie beginnt in Kürze.',
|
||
'error.already_betting': 'Du hast bereits eine aktive Wette in der laufenden Runde.',
|
||
'error.insufficient_balance': 'Nicht genügend Guthaben.',
|
||
'error.balance_pending_confirmation': 'Sie haben {pending_plm} PLM, die noch auf Bestätigung warten — noch nicht verfügbar.',
|
||
'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.',
|
||
'error.broadcast_failed': 'Das Netzwerk hat die Transaktion abgelehnt. Bitte versuche es in Kürze erneut.',
|
||
'error.amount_below_dust_limit': 'Der Betrag ist zu klein, um gesendet zu werden.',
|
||
'error.too_many_inputs': 'Ihr Guthaben ist auf zu viele kleine Einzahlungen verteilt, um in einer einzigen Transaktion ausgegeben zu werden (max. {max_inputs}). Bitte wenden Sie sich an den Support, um es zusammenzufassen.',
|
||
'error.withdrawal_to_own_address': 'Das ist deine eigene Einzahlungsadresse — zahle auf eine externe Wallet aus.',
|
||
'error.internal_error': 'Unerwarteter Serverfehler. Bitte versuche es in Kürze erneut.',
|
||
'error.guide_unavailable': 'Die Anleitung ist derzeit nicht verfügbar.',
|
||
'error.rate_limited': 'Zu viele Versuche, bitte versuche es in {retry_after_seconds} Sekunden erneut.',
|
||
|
||
'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': 'Сообщить об ошибке',
|
||
'bugReport.pageTitle': 'Сообщить об ошибке',
|
||
'bugReport.heading': 'Сообщить об ошибке',
|
||
'bugReport.intro': 'Нашли проблему? Опишите её ниже — сообщение сразу попадёт в панель администратора.',
|
||
'bugReport.englishNotice': 'Пожалуйста, опишите ошибку на английском языке, независимо от языка интерфейса — это поможет нам обработать её быстрее.',
|
||
'bugReport.descriptionLabel': 'Что произошло?',
|
||
'bugReport.descriptionPlaceholder': 'Опишите ошибку: что вы делали, что ожидали и что произошло вместо этого.',
|
||
'bugReport.contactLabel': 'Контакт (необязательно)',
|
||
'bugReport.contactPlaceholder': 'Email или другой контакт, если хотите получить ответ',
|
||
'bugReport.submitBtn': 'Отправить сообщение',
|
||
'bugReport.submitting': 'Отправка…',
|
||
'bugReport.blankError': 'Опишите ошибку перед отправкой.',
|
||
'bugReport.successToast': 'Спасибо! Сообщение отправлено.',
|
||
'bugReport.errorPrefix': 'Ошибка отправки: ',
|
||
'bugReport.myReportsTitle': 'Ваши сообщения',
|
||
'bugReport.myReportsHint': 'Только сообщения, отправленные с этого аккаунта, со статусом, обновлённым администрацией.',
|
||
'bugReport.myReportsEmpty': 'Вы ещё не отправляли сообщений.',
|
||
'bugReport.statusOpen': 'Не прочитано',
|
||
'bugReport.statusRead': 'В обработке',
|
||
'bugReport.statusResolved': 'Решено',
|
||
'bugReport.backLink': 'Назад на главную',
|
||
'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.round_full': 'В этом раунде достигнут максимум участников ({max_participants}) — дождитесь следующего, он начнётся совсем скоро.',
|
||
'error.already_betting': 'У вас уже есть активная ставка в текущем раунде.',
|
||
'error.insufficient_balance': 'Недостаточно средств.',
|
||
'error.balance_pending_confirmation': 'У вас есть {pending_plm} PLM, ожидающих подтверждения — они пока недоступны для расходования.',
|
||
'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': 'Введите сумму больше нуля.',
|
||
'error.broadcast_failed': 'Сеть отклонила транзакцию. Попробуйте ещё раз через минуту.',
|
||
'error.amount_below_dust_limit': 'Сумма слишком мала для отправки.',
|
||
'error.too_many_inputs': 'Ваш баланс разбит на слишком много мелких депозитов, чтобы потратить его одной транзакцией (максимум {max_inputs}). Обратитесь в поддержку для консолидации.',
|
||
'error.withdrawal_to_own_address': 'Это ваш собственный адрес для депозита — выводите на внешний кошелёк.',
|
||
'error.internal_error': 'Непредвиденная ошибка сервера. Попробуйте ещё раз через минуту.',
|
||
'error.guide_unavailable': 'Руководство сейчас недоступно.',
|
||
'error.rate_limited': 'Слишком много попыток, повторите через {retry_after_seconds} сек.',
|
||
|
||
'loading.creating': 'Создание…',
|
||
'loading.loggingIn': 'Вход…',
|
||
'loading.sendingBet': 'Отправка ставки…',
|
||
'loading.updating': 'Обновление…',
|
||
'loading.refreshing': '…',
|
||
'loading.sending': 'Отправка…',
|
||
},
|
||
zh: {
|
||
'nav.ariaSections': '导航区',
|
||
'nav.guideTitle': '指南',
|
||
'nav.guideAria': '打开用户指南',
|
||
'nav.bugReport': '报告问题',
|
||
'bugReport.pageTitle': '报告问题',
|
||
'bugReport.heading': '报告问题',
|
||
'bugReport.intro': '发现问题了吗?请在下面描述——您的反馈会直接发送到管理员面板。',
|
||
'bugReport.englishNotice': '请用英文描述问题,无论您当前使用的是哪种语言界面——这有助于我们更快处理。',
|
||
'bugReport.descriptionLabel': '发生了什么?',
|
||
'bugReport.descriptionPlaceholder': '描述问题:您当时在做什么、期望的结果是什么,以及实际发生了什么。',
|
||
'bugReport.contactLabel': '联系方式(可选)',
|
||
'bugReport.contactPlaceholder': '如果希望得到回复,请留下邮箱或其他联系方式',
|
||
'bugReport.submitBtn': '发送反馈',
|
||
'bugReport.submitting': '发送中…',
|
||
'bugReport.blankError': '请先描述问题再发送。',
|
||
'bugReport.successToast': '谢谢!反馈已发送。',
|
||
'bugReport.errorPrefix': '发送出错:',
|
||
'bugReport.myReportsTitle': '您的反馈',
|
||
'bugReport.myReportsHint': '仅显示此账户发送的反馈,状态由管理团队更新。',
|
||
'bugReport.myReportsEmpty': '您还没有发送过反馈。',
|
||
'bugReport.statusOpen': '待处理',
|
||
'bugReport.statusRead': '处理中',
|
||
'bugReport.statusResolved': '已解决',
|
||
'bugReport.backLink': '返回首页',
|
||
'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.round_full': '本轮已达到 {max_participants} 名玩家的上限 —— 请等待下一轮,很快就会开始。',
|
||
'error.already_betting': '你在当前回合已有一笔有效下注。',
|
||
'error.insufficient_balance': '余额不足。',
|
||
'error.balance_pending_confirmation': '您有 {pending_plm} PLM 待确认 —— 尚不可用于支出。',
|
||
'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': '请输入大于零的金额。',
|
||
'error.broadcast_failed': '网络拒绝了该交易,请稍后重试。',
|
||
'error.amount_below_dust_limit': '金额过小,无法发送。',
|
||
'error.too_many_inputs': '您的余额分散在过多的小额存款中,无法在一笔交易中花费(最多 {max_inputs} 笔)。请联系客服进行归集。',
|
||
'error.withdrawal_to_own_address': '这是你自己的充值地址 — 请提现到外部钱包。',
|
||
'error.internal_error': '服务器发生意外错误,请稍后重试。',
|
||
'error.guide_unavailable': '指南当前不可用。',
|
||
'error.rate_limited': '尝试次数过多,请在 {retry_after_seconds} 秒后重试。',
|
||
|
||
'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);
|