diff options
| author | Adam Soltys <[email protected]> | 2026-06-19 08:23:57 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-19 08:23:57 -0700 |
| commit | 47293eb1dd2016358af0af18ff68d417630fca3d (patch) | |
| tree | ca3f00fe532d0d4e629de5f1db1572153a6e971c /src | |
| parent | 4527ec3815caf314a717823220fc630ab9831988 (diff) | |
| download | coinosv3-47293eb1dd2016358af0af18ff68d417630fca3d.tar.gz coinosv3-47293eb1dd2016358af0af18ff68d417630fca3d.zip | |
Add How it works page, plainer copy, faster import scan
UI / copy:
- Footer: "made by Adam Soltys" (drop the heart) + a "How it works"
link on its own line, matching the footer's size.
- New "How it works" page: plain-language basics plus an expandable
"for the technically curious" section. Routed via ui.screen, with
Back/logo returning to wherever you opened it from.
- Scrub jargon from the main UI for newcomers: drop BIP39, soften
"raw hex" -> "code", "txid" -> "transaction ID", "air-gapped" ->
"offline"; replace em dashes in the intro/keys notes. The technical
terms now live only in the How it works technical section.
Import / scan performance (fewer requests -> fewer 429s, faster):
- Skip the /utxo fetch for fully-settled, zero-balance addresses
(they can't hold UTXOs) -- the bulk of a history-heavy wallet.
- Show balance as soon as the address scan finishes, then load UTXOs
and history in the background.
- Probe the receive and change chains concurrently so their per-hit
delays overlap.
- History fills in progressively as each address resolves, with a
loading indicator on the History tab.
i18n.js is now tracked; new English keys fall back to English for the
other languages until translated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 392 | ||||
| -rw-r--r-- | src/i18n.js | 1062 | ||||
| -rw-r--r-- | src/style.css | 9 | ||||
| -rw-r--r-- | src/wallet.js | 66 |
4 files changed, 1373 insertions, 156 deletions
@@ -6,6 +6,7 @@ import { Wallet, newMnemonic, isValidMnemonic, utxoId } from './wallet.js'; import { qrSvg } from './qr.js'; +import { t, LANGS, getLang, setLang, isRTL } from './i18n.js'; import { fmtBtc, fmtSats, @@ -19,7 +20,8 @@ import { const wallet = new Wallet(); const ui = { - screen: 'unlock', // 'unlock' | 'wallet' + screen: 'unlock', // 'unlock' | 'wallet' | 'howItWorks' + returnScreen: 'unlock', // where 'howItWorks' returns to (Back / logo) unlockTab: 'create', // 'create' | 'import' createStep: 'gen', // 'gen' | 'confirm' draftMnemonic: '', @@ -79,18 +81,39 @@ function footer() { return h( 'div', { class: 'footer small muted center' }, - 'made with ', - h('span', { style: 'color:#e25555;font-size:1.25em;vertical-align:-1px' }, '♥'), - ' by ', - h('a', { href: 'https://adamsoltys.com', target: '_blank', rel: 'noopener' }, 'Adam Soltys'), - h('span', { class: 'faint' }, ' · '), - 'source available on ', - h('a', { href: 'https://github.com/asoltys/cold', target: '_blank', rel: 'noopener' }, 'GitHub') + h( + 'div', + {}, + t('footerMadeBy') + ' ', + h('a', { href: 'https://adamsoltys.com', target: '_blank', rel: 'noopener' }, 'Adam Soltys'), + h('span', { class: 'faint' }, ' · '), + t('footerSourceOn') + ' ', + h('a', { href: 'https://github.com/asoltys/cold', target: '_blank', rel: 'noopener' }, 'GitHub') + ), + h( + 'div', + { style: 'margin-top:4px' }, + h('button', { class: 'linklike', style: 'font-weight:400', onClick: openHowItWorks }, t('howItWorks')) + ) ); } +// Open the How it works page, remembering where to return to. +function openHowItWorks() { + if (ui.screen === 'howItWorks') return; + ui.returnScreen = ui.screen; + ui.screen = 'howItWorks'; + render(); +} + function render() { - root.replaceChildren(ui.screen === 'wallet' ? walletScreen() : unlockScreen(), footer()); + const screen = + ui.screen === 'wallet' + ? walletScreen() + : ui.screen === 'howItWorks' + ? howItWorksScreen() + : unlockScreen(); + root.replaceChildren(screen, footer()); } wallet.subscribe(render); @@ -120,7 +143,7 @@ async function copy(text) { } catch {} ta.remove(); } - toast('Copied to clipboard'); + toast(t('copied')); } function download(filename, text, mime = 'application/json') { @@ -132,7 +155,7 @@ function download(filename, text, mime = 'application/json') { setTimeout(() => URL.revokeObjectURL(url), 1000); } -function copyBtn(text, label = 'Copy') { +function copyBtn(text, label = t('copy')) { return h('button', { class: 'btn-sm', onClick: () => copy(text) }, label); } @@ -162,7 +185,7 @@ const fmtAmount = (sats) => (unit === 'sats' ? fmtSats(sats) : fmtBtc(sats)); // A clickable unit label. cls lets callers inherit surrounding sizing. function unitTag(cls = '') { - return h('button', { type: 'button', class: 'unit-tag ' + cls, title: 'Switch BTC / sats', onClick: toggleUnit }, unitLabel()); + return h('button', { type: 'button', class: 'unit-tag ' + cls, title: t('switchUnit'), onClick: toggleUnit }, unitLabel()); } // ================================================================ UNLOCK @@ -177,26 +200,60 @@ function unlockScreen() { h( 'div', { class: 'tabs' }, - tabBtn('Create new', ui.unlockTab === 'create', () => { + tabBtn(t('createNew'), ui.unlockTab === 'create', () => { ui.unlockTab = 'create'; render(); }), - tabBtn('Import existing', ui.unlockTab === 'import', () => { + tabBtn(t('importExisting'), ui.unlockTab === 'import', () => { ui.unlockTab = 'import'; render(); }) ), ui.unlockTab === 'create' ? createPane() : importPane(), ui.unlockError && h('div', { class: 'notice err' }, ui.unlockError) - ), - h( - 'p', - { class: 'small muted center' }, - 'Keys never leave your browser. Works fully offline — save this page and open it from your filesystem.' ) ); } +// ================================================================ HOW IT WORKS +function howItWorksScreen() { + const back = () => { + ui.screen = ui.returnScreen === 'wallet' ? 'wallet' : 'unlock'; + render(); + }; + const para = (key) => h('p', { class: 'muted', style: 'margin:0' }, t(key)); + return h( + 'div', + { class: 'col', style: 'gap:16px' }, + brandHeader(false), + h( + 'div', + { class: 'card col', style: 'gap:14px' }, + h('p', { class: 'lead', style: 'margin:0' }, t('hiwLead')), + h('h3', {}, t('hiwBasicsTitle')), + para('hiwBasics1'), + para('hiwBasics2'), + para('hiwBasics3'), + para('hiwBasics4'), + h( + 'details', + { class: 'hiw-tech' }, + h('summary', {}, t('hiwTechTitle')), + h( + 'div', + { class: 'col', style: 'gap:12px;margin-top:12px' }, + para('hiwTech1'), + para('hiwTech2'), + para('hiwTech3'), + para('hiwTech4'), + para('hiwTech5') + ) + ) + ), + h('button', { class: 'btn-block', onClick: back }, t('back')) + ); +} + function tabBtn(label, active, onClick) { return h('button', { class: active ? 'active' : '', onClick }, label); } @@ -207,7 +264,7 @@ function createPane() { return h( 'div', { class: 'col' }, - h('p', { class: 'muted' }, 'Generate a new 12-word BIP39 seed phrase. Write it down — it is the only way to recover this wallet.'), + h('p', { class: 'muted' }, t('genIntro')), h( 'button', { @@ -217,7 +274,7 @@ function createPane() { render(); }, }, - 'Generate seed phrase' + t('generateSeed') ) ); } @@ -225,7 +282,7 @@ function createPane() { return h( 'div', { class: 'col' }, - h('div', { class: 'warn-box' }, '⚠ Write these 12 words down on paper, in order. Anyone with them can spend your coins.'), + h('div', { class: 'warn-box' }, t('writeDownWarn')), h( 'div', { class: 'words' }, @@ -236,7 +293,7 @@ function createPane() { h( 'div', { class: 'row gap6' }, - copyBtn(ui.draftMnemonic, 'Copy phrase'), + copyBtn(ui.draftMnemonic, t('copyPhrase')), h( 'button', { @@ -246,7 +303,7 @@ function createPane() { render(); }, }, - 'Regenerate' + t('regenerate') ) ), optionsPanel(), @@ -262,12 +319,12 @@ function createPane() { render(); }, }, - 'Verify backup' + t('verifyBackup') ), h( 'button', { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic) }, - 'Skip verification' + t('skipVerification') ) ); } @@ -277,12 +334,12 @@ function createPane() { return h( 'div', { class: 'col' }, - h('p', { class: 'muted' }, 'Confirm your backup by entering the requested words.'), + h('p', { class: 'muted' }, t('confirmBackupIntro')), ...ui.confirm.map((c, i) => h( 'label', { class: 'field' }, - h('span', { class: 'lab' }, `Word #${c.index + 1}`), + h('span', { class: 'lab' }, t('wordN', { n: c.index + 1 })), h('input', { type: 'text', class: 'mono-input', @@ -299,7 +356,7 @@ function createPane() { h( 'label', { class: 'field' }, - h('span', { class: 'lab' }, 'Re-enter passphrase'), + h('span', { class: 'lab' }, t('reenterPassphrase')), h('input', { type: 'password', class: 'mono-input', @@ -309,21 +366,21 @@ function createPane() { }) ), h('div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.createStep = 'gen'; render(); } }, '← Back'), + h('button', { class: 'btn-ghost', onClick: () => { ui.createStep = 'gen'; render(); } }, t('back')), h('button', { class: 'btn-primary grow', onClick: () => { const words = ui.draftMnemonic.split(' '); const ok = ui.confirm.every((c) => c.value.toLowerCase() === words[c.index]); - if (!ok) { ui.unlockError = 'Those words don’t match your phrase. Check your backup.'; render(); return; } + if (!ok) { ui.unlockError = t('wordsMismatch'); render(); return; } if (hasPass && ui.confirmPass !== ui.passphrase) { - ui.unlockError = 'That passphrase doesn’t match the one you entered.'; render(); return; + ui.unlockError = t('passphraseMismatch'); render(); return; } openWallet(ui.draftMnemonic); }, - }, 'Open wallet') + }, t('openWallet')) ), - h('button', { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic) }, 'Skip verification') + h('button', { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic) }, t('skipVerification')) ); } @@ -340,9 +397,9 @@ function importPane() { h( 'label', { class: 'field' }, - h('span', { class: 'lab' }, 'Seed phrase'), + h('span', { class: 'lab' }, t('seedPhrase')), h('textarea', { - placeholder: 'enter your seed words separated by spaces', + placeholder: t('seedPlaceholder'), autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', @@ -351,7 +408,7 @@ function importPane() { }) ), optionsPanel(), - h('button', { class: 'btn-primary btn-block', onClick: () => openWallet(ui.importText) }, 'Open wallet') + h('button', { class: 'btn-primary btn-block', onClick: () => openWallet(ui.importText) }, t('openWallet')) ); } @@ -359,19 +416,19 @@ function optionsPanel() { return h( 'label', { class: 'field' }, - h('span', { class: 'lab' }, 'Optional passphrase'), + h('span', { class: 'lab' }, t('optionalPassphrase')), h( 'div', { class: 'input-group' }, h('input', { type: ui.showPass ? 'text' : 'password', class: 'mono-input', - placeholder: 'leave blank if unused', + placeholder: t('leaveBlank'), autocomplete: 'off', value: ui.passphrase, onInput: (e) => (ui.passphrase = e.target.value), }), - h('button', { class: 'btn-sm', type: 'button', onClick: () => { ui.showPass = !ui.showPass; render(); } }, ui.showPass ? 'Hide' : 'Show') + h('button', { class: 'btn-sm', type: 'button', onClick: () => { ui.showPass = !ui.showPass; render(); } }, ui.showPass ? t('hide') : t('show')) ) ); } @@ -380,7 +437,7 @@ async function openWallet(mnemonic) { ui.unlockError = ''; const m = (mnemonic || '').trim().replace(/\s+/g, ' '); if (!isValidMnemonic(m)) { - ui.unlockError = 'Invalid seed phrase — check the spelling and word order.'; + ui.unlockError = t('invalidSeed'); render(); return; } @@ -502,11 +559,11 @@ function brandHeader(withLock) { { class: 'row between' }, h( 'div', - { class: 'brand', style: 'cursor:pointer', title: 'Home', onClick: goHome }, + { class: 'brand', style: 'cursor:pointer', title: t('home'), onClick: goHome }, h('div', { class: 'logo' }, '₿'), - h('h1', {}, 'Bitcoin Wallet') + h('h1', {}, t('appTitle')) ), - withLock && h('button', { class: 'btn-sm', onClick: lock }, 'Logout') + withLock && h('button', { class: 'btn-sm', onClick: lock }, t('logout')) ); } @@ -530,50 +587,85 @@ function settingsTab() { h( 'div', { class: 'card col' }, - h('h3', {}, 'Recovery phrase'), - h('div', { class: 'warn-box' }, '⚠ Anyone who can see these words can steal your funds. Make sure nobody is watching before you reveal them.'), + h('h3', {}, t('recoveryPhrase')), + h('div', { class: 'warn-box' }, t('recoveryWarn')), h('div', { class: 'words' }, cells), shown && wallet.passphrase ? h('div', { class: 'col gap6' }, - h('span', { class: 'lab' }, 'BIP39 passphrase'), + h('span', { class: 'lab' }, t('bip39Passphrase')), h('div', { class: 'addr-box' }, wallet.passphrase) ) : null, shown ? h('div', { class: 'row gap6 wrap' }, - copyBtn(wallet.mnemonic, 'Copy phrase'), - wallet.passphrase ? copyBtn(wallet.passphrase, 'Copy passphrase') : null, - h('button', { class: 'grow', onClick: () => { ui.revealShown = false; render(); } }, 'Hide') + copyBtn(wallet.mnemonic, t('copyPhrase')), + wallet.passphrase ? copyBtn(wallet.passphrase, t('copyPassphrase')) : null, + h('button', { class: 'grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide')) ) - : h('button', { class: 'btn-primary btn-block', onClick: () => { ui.revealShown = true; render(); } }, 'Reveal recovery phrase') + : h('button', { class: 'btn-primary btn-block', onClick: () => { ui.revealShown = true; render(); } }, t('revealRecovery')) ), h( 'div', { class: 'card col' }, - h('h3', {}, 'Offline transfer'), + h('h3', {}, t('offlineTransfer')), snapshotActions() ), h( 'div', { class: 'card col' }, - h('h3', {}, 'Rescan'), + h('h3', {}, t('rescan')), h('p', { class: 'small muted', style: 'margin:0' }, - 'Your current address updates automatically. Rescan only if a payment was sent to an old (reused) address and isn’t showing.'), + t('rescanDesc')), wallet.offline ? null : h('button', { disabled: wallet.scanning, onClick: () => wallet.scan() }, - wallet.scanning ? 'Scanning…' : 'Rescan wallet') + wallet.scanning ? t('scanning') : t('rescanWallet')) + ), + h( + 'div', + { class: 'card col' }, + h('h3', {}, t('language')), + languagePicker() ) ); } +// Language selector. Changing it persists the choice, flips text direction for +// RTL languages, and re-renders the whole app in the new language. +function languagePicker() { + return h( + 'select', + { + value: getLang(), + onChange: (e) => { + setLang(e.target.value); + applyDir(); + render(); + }, + }, + LANGS.map(([code, name]) => h('option', { value: code, selected: code === getLang() }, name)) + ); +} + +// Reflect the active language's writing direction on <html> (rtl for ar/ur). +function applyDir() { + try { + document.documentElement.dir = isRTL() ? 'rtl' : 'ltr'; + document.documentElement.lang = getLang(); + } catch {} +} + function goHome() { + if (ui.screen === 'howItWorks') { + ui.screen = ui.returnScreen === 'wallet' ? 'wallet' : 'unlock'; + } if (ui.screen === 'wallet') { ui.tab = wallet.offline ? 'settings' : 'receive'; ui.draft = null; ui.sendResult = null; ui.sendError = ''; } else { + ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.createStep = 'gen'; ui.draftMnemonic = ''; @@ -599,8 +691,8 @@ function offlineBanner() { return h( 'div', { class: 'notice info row between', style: 'margin:12px 0 0' }, - h('span', {}, "Can't reach the network — working offline. Import a snapshot on the Settings tab to load coins."), - h('button', { class: 'btn-sm', onClick: retryOnline }, 'Retry') + h('span', {}, t('offlineBanner')), + h('button', { class: 'btn-sm', onClick: retryOnline }, t('retry')) ); } @@ -610,13 +702,13 @@ function balanceCard() { return h( 'div', { class: 'card balance' }, - h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, 'Balance'), + h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, t('balance')), h('div', { class: 'amt', style: firstLoad ? 'opacity:.3' : '' }, fmtAmount(wallet.confirmed), ' ', unitTag('unit')), wallet.pending > 0 ? h( 'div', { class: 'split' }, - h('div', {}, h('div', { class: 'k' }, 'Pending'), h('div', { class: 'v pending' }, fmtAmount(wallet.pending), ' ', unitTag())) + h('div', {}, h('div', { class: 'k' }, t('pending')), h('div', { class: 'v pending' }, fmtAmount(wallet.pending), ' ', unitTag())) ) : null ); @@ -624,10 +716,10 @@ function balanceCard() { function tabsBar() { const tabs = [ - ['receive', 'Receive'], - ['send', 'Send'], - ['history', 'History'], - ['settings', 'Settings'], + ['receive', t('tabReceive')], + ['send', t('tabSend')], + ['history', t('tabHistory')], + ['settings', t('tabSettings')], ]; return h( 'div', @@ -672,9 +764,9 @@ function receiveTab() { onClick: () => { ui.receiveSeenIndex = wallet.nextReceiveIndex; wallet.setReceiveAck(wallet.nextReceiveIndex); render(); }, }, h('div', { class: 'check-badge' }, '✓'), - h('h2', { style: 'margin:0' }, 'Payment received!'), + h('h2', { style: 'margin:0' }, t('paymentReceived')), amt ? h('div', { class: 'amount-pos', style: 'font-size:18px' }, '+' + fmtAmount(amt) + ' ' + unitLabel()) : null, - h('div', { class: 'small muted' }, 'Tap anywhere to proceed') + h('div', { class: 'small muted' }, t('tapToProceed')) ); } @@ -684,7 +776,7 @@ function receiveTab() { { class: 'card col', style: 'align-items:center;gap:14px' }, h('div', { html: qrSvg(fresh.address) }), h('div', { class: 'addr-box', style: 'width:100%' }, fresh.address), - copyBtn(fresh.address, 'Copy address') + copyBtn(fresh.address, t('copyAddress')) ); } @@ -708,7 +800,7 @@ function recipientRow(s, r, i) { autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, onInput: (e) => (r.address = e.target.value), }), - !single && h('button', { type: 'button', class: 'btn-sm', title: 'Remove', onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕') + !single && h('button', { type: 'button', class: 'btn-sm', title: t('remove'), onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕') ), h('div', { class: 'input-group' }, h('input', { @@ -731,8 +823,8 @@ function recipientRow(s, r, i) { r.amount = v; }, }), - h('button', { type: 'button', class: 'btn-sm', title: 'Switch BTC / sats', onClick: toggleUnit }, unitLabel()), - single && h('button', { type: 'button', class: s.max ? 'btn-primary' : '', onClick: () => { s.max = !s.max; render(); } }, 'Max') + h('button', { type: 'button', class: 'btn-sm', title: t('switchUnit'), onClick: toggleUnit }, unitLabel()), + single && h('button', { type: 'button', class: s.max ? 'btn-primary' : '', onClick: () => { s.max = !s.max; render(); } }, t('max')) ) ); } @@ -740,10 +832,10 @@ function recipientRow(s, r, i) { function sendForm() { const s = ui.send; const feeOpts = [ - ['economyFee', 'Economy'], - ['halfHourFee', 'Normal'], - ['fastestFee', 'Priority'], - ['custom', 'Custom'], + ['economyFee', t('feeEconomy')], + ['halfHourFee', t('feeNormal')], + ['fastestFee', t('feePriority')], + ['custom', t('feeCustom')], ]; return h( 'div', @@ -751,7 +843,7 @@ function sendForm() { h( 'div', { class: 'field' }, - h('span', { class: 'lab' }, s.recipients.length > 1 ? 'Recipients' : 'Recipient'), + h('span', { class: 'lab' }, s.recipients.length > 1 ? t('recipients') : t('recipient')), h('div', { class: 'col', style: 'gap:14px' }, s.recipients.map((r, i) => recipientRow(s, r, i)) ), @@ -759,12 +851,12 @@ function sendForm() { h('button', { type: 'button', class: 'linklike small mt8', onClick: () => { s.recipients.push({ address: '', amount: '' }); s.max = false; render(); }, - }, '+ Add recipient') + }, t('addRecipient')) ), h( 'div', { class: 'field' }, - h('span', { class: 'lab' }, 'Fee rate'), + h('span', { class: 'lab' }, t('feeRate')), h( 'div', { class: 'seg', style: 'display:flex;width:100%' }, @@ -787,11 +879,11 @@ function sendForm() { h('span', { class: 'small muted', style: 'align-self:center' }, 'sat/vB') ), s.feeChoice !== 'custom' && - h('div', { class: 'small faint mt8' }, `Selected rate: ${currentFeeRate()} sat/vB`) + h('div', { class: 'small faint mt8' }, t('selectedRate', { n: currentFeeRate() })) ), coinControl(), ui.sendError && h('div', { class: 'notice err' }, ui.sendError), - h('button', { class: 'btn-primary btn-block', onClick: reviewSend }, 'Review transaction') + h('button', { class: 'btn-primary btn-block', onClick: reviewSend }, t('reviewTx')) ); } @@ -800,18 +892,18 @@ function coinControl() { const head = h( 'div', { class: 'row between' }, - h('span', { class: 'lab', style: 'margin:0' }, 'Coin selection'), + h('span', { class: 'lab', style: 'margin:0' }, t('coinSelection')), h( 'div', { class: 'seg' }, - h('button', { type: 'button', class: !s.manual ? 'active' : '', onClick: () => { s.manual = false; render(); } }, 'Automatic'), - h('button', { type: 'button', class: s.manual ? 'active' : '', onClick: () => { s.manual = true; render(); } }, 'Manual') + h('button', { type: 'button', class: !s.manual ? 'active' : '', onClick: () => { s.manual = false; render(); } }, t('automatic')), + h('button', { type: 'button', class: s.manual ? 'active' : '', onClick: () => { s.manual = true; render(); } }, t('manual')) ) ); if (!s.manual) return h('div', { class: 'col gap6' }, head); if (!wallet.utxos.length) - return h('div', { class: 'col gap6' }, head, h('div', { class: 'small muted' }, 'No coins available.')); + return h('div', { class: 'col gap6' }, head, h('div', { class: 'small muted' }, t('noCoins'))); let selTotal = 0; const rows = wallet.utxos.map((u) => { @@ -838,7 +930,7 @@ function coinControl() { head, h('div', { class: 'list' }, rows), h('div', { class: 'row between small' }, - h('span', { class: 'muted' }, `${s.coins.size} selected`), + h('span', { class: 'muted' }, t('nSelected', { n: s.coins.size })), h('span', { class: 'amount' }, fmtAmount(selTotal), ' ', unitTag()) ) ); @@ -875,20 +967,20 @@ function reviewSend() { let coinIds = null; if (s.manual) { coinIds = [...s.coins]; - if (!coinIds.length) throw new Error('Select at least one coin to spend.'); + if (!coinIds.length) throw new Error(t('selectCoin')); } let recipients, sendMax = false; if (s.max && s.recipients.length === 1) { const addr = s.recipients[0].address.trim(); - if (!addr) throw new Error('Enter a recipient address.'); + if (!addr) throw new Error(t('enterRecipientAddr')); recipients = [{ address: addr, amount: 0 }]; sendMax = true; } else { recipients = s.recipients.map((r, i) => { const addr = r.address.trim(); - if (!addr) throw new Error(`Enter an address for recipient ${i + 1}.`); + if (!addr) throw new Error(t('enterAddrForN', { n: i + 1 })); const sats = parseAmount(r.amount, unit); - if (!sats || sats <= 0) throw new Error(`Enter a valid amount for recipient ${i + 1}.`); + if (!sats || sats <= 0) throw new Error(t('enterValidAmtForN', { n: i + 1 })); return { address: addr, amount: sats }; }); } @@ -907,7 +999,7 @@ function reviewView() { return h( 'div', { class: 'card col' }, - h('h3', {}, 'Review transaction'), + h('h3', {}, t('reviewTx')), h( 'div', { class: 'summary col', style: 'gap:0' }, @@ -918,23 +1010,23 @@ function reviewView() { ) ), h('div', { class: 'line' }, - h('span', { class: 'k' }, 'Network fee'), + h('span', { class: 'k' }, t('networkFee')), h('span', { class: 'v' }, fmtAmount(d.fee), ' ', unitTag()) ) ), ui.sendError && h('div', { class: 'notice err' }, ui.sendError), wallet.offline - ? h('div', { class: 'notice info' }, 'Offline: sign now, then broadcast the signed transaction from an online device.') + ? h('div', { class: 'notice info' }, t('offlineSignNote')) : null, h( 'div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.draft = null; ui.sendError = ''; render(); } }, '← Back'), + h('button', { class: 'btn-ghost', onClick: () => { ui.draft = null; ui.sendError = ''; render(); } }, t('back')), ui.busy ? h('button', { class: 'btn-primary grow', disabled: true }, h('span', { class: 'spinner' })) : wallet.offline - ? h('button', { class: 'btn-primary grow', onClick: signForExport }, 'Sign transaction') - : h('button', { class: 'btn-primary grow', onClick: broadcast }, 'Sign & broadcast') + ? h('button', { class: 'btn-primary grow', onClick: signForExport }, t('signTx')) + : h('button', { class: 'btn-primary grow', onClick: broadcast }, t('signBroadcast')) ) ); } @@ -951,7 +1043,7 @@ async function broadcast() { ui.send = blankSend(); await wallet.scan(); } catch (e) { - ui.sendError = 'Broadcast failed: ' + e.message; + ui.sendError = t('broadcastFailed', { msg: e.message }); } ui.busy = false; render(); @@ -966,41 +1058,41 @@ function signForExport() { ui.draft = null; ui.send = blankSend(); } catch (e) { - ui.sendError = 'Signing failed: ' + e.message; + ui.sendError = t('signingFailed', { msg: e.message }); } render(); } function sendResultView() { const r = ui.sendResult; - const again = h('button', { class: 'btn-block mt8', onClick: () => { ui.sendResult = null; render(); } }, 'Done'); + const again = h('button', { class: 'btn-block mt8', onClick: () => { ui.sendResult = null; render(); } }, t('done')); if (r.signedHex) { return h( 'div', { class: 'card col' }, - h('div', { class: 'notice ok' }, '✓ Transaction signed. Broadcast the hex below from any online device (e.g. mempool.space → Broadcast).'), - h('div', { class: 'small muted' }, 'Transaction ID'), + h('div', { class: 'notice ok' }, t('txSignedNote')), + h('div', { class: 'small muted' }, t('transactionId')), h('div', { class: 'addr-box' }, r.txid), - h('div', { class: 'small muted mt8' }, 'Signed transaction (raw hex)'), + h('div', { class: 'small muted mt8' }, t('signedTxRaw')), h('textarea', { readonly: true, style: 'min-height:120px', value: r.signedHex }), h('div', { class: 'row gap6' }, - copyBtn(r.signedHex, 'Copy hex'), - h('button', { class: 'btn-sm', onClick: () => download(`tx-${r.txid.slice(0, 8)}.txt`, r.signedHex, 'text/plain') }, 'Download'), + copyBtn(r.signedHex, t('copyHex')), + h('button', { class: 'btn-sm', onClick: () => download(`tx-${r.txid.slice(0, 8)}.txt`, r.signedHex, 'text/plain') }, t('downloadLabel')), h('div', { class: 'grow', html: '' }) ), - h('details', { class: 'mt8' }, h('summary', { class: 'small muted' }, 'Show QR (for air-gapped transfer)'), h('div', { style: 'margin-top:10px', html: qrSvg(r.signedHex) })), + h('details', { class: 'mt8' }, h('summary', { class: 'small muted' }, t('showQrAirgap')), h('div', { style: 'margin-top:10px', html: qrSvg(r.signedHex) })), again ); } return h( 'div', { class: 'card col', style: 'align-items:center' }, - h('div', { class: 'notice ok', style: 'width:100%' }, '✓ Transaction broadcast!'), - h('div', { class: 'small muted' }, 'Transaction ID'), + h('div', { class: 'notice ok', style: 'width:100%' }, t('txBroadcast')), + h('div', { class: 'small muted' }, t('transactionId')), h('div', { class: 'addr-box' }, r.txid), h('div', { class: 'row gap6' }, - copyBtn(r.txid, 'Copy txid'), - h('a', { class: 'btn btn-sm', href: wallet.api.explorerTx(r.txid), target: '_blank', rel: 'noopener' }, 'View on mempool.space ↗') + copyBtn(r.txid, t('copyTxid')), + h('a', { class: 'btn btn-sm', href: wallet.api.explorerTx(r.txid), target: '_blank', rel: 'noopener' }, t('viewOnMempool')) ), again ); @@ -1009,73 +1101,87 @@ function sendResultView() { // ---------------------------------------------------------------- History function historyTab() { if (ui.txDetail) { - const t = wallet.txs.find((x) => x.txid === ui.txDetail); - if (t) return txDetailView(t); + const tx = wallet.txs.find((x) => x.txid === ui.txDetail); + if (tx) return txDetailView(tx); ui.txDetail = null; } if (wallet.offline) - return h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, 'Transaction history is unavailable in offline mode.')); - if (wallet.scanning && !wallet.loaded) return h('div', { class: 'card center' }, h('span', { class: 'spinner' })); + return h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, t('historyOffline'))); + if ((wallet.scanning && !wallet.loaded) || (wallet.historyLoading && !wallet.txs.length)) + return h( + 'div', + { class: 'card center col', style: 'align-items:center;gap:10px' }, + h('span', { class: 'spinner' }), + wallet.historyLoading ? h('p', { class: 'small muted', style: 'margin:0' }, t('loadingHistory')) : null + ); if (!wallet.txs.length) - return h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, 'No transactions yet.')); + return h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, t('noTxYet'))); return h( 'div', { class: 'card' }, h( 'div', { class: 'list' }, - wallet.txs.map((t) => { - const incoming = t.net >= 0; + wallet.txs.map((tx) => { + const incoming = tx.net >= 0; return h( 'div', - { class: 'item', style: 'cursor:pointer', onClick: () => { ui.txDetail = t.txid; render(); } }, + { class: 'item', style: 'cursor:pointer', onClick: () => { ui.txDetail = tx.txid; render(); } }, h('div', { class: `ico ${incoming ? 'in' : 'out'}` }, incoming ? '↓' : '↑'), h('div', { class: 'grow' }, h('div', { class: 'row gap6' }, - incoming ? 'Received' : 'Sent', - t.confirmed ? null : h('span', { class: 'tag pending' }, '⏳ pending') + incoming ? t('received') : t('sent'), + tx.confirmed ? null : h('span', { class: 'tag pending' }, t('pendingTag')) ), - h('div', { class: 'small faint' }, t.confirmed ? timeAgo(t.blockTime) : 'awaiting confirmation') + h('div', { class: 'small faint' }, tx.confirmed ? timeAgo(tx.blockTime) : t('awaitingConfirmation')) ), h('div', { style: 'text-align:right' }, - h('div', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(t.net)), - !incoming && t.fee ? h('div', { class: 'small faint' }, `fee ${fmtAmount(t.fee)}`) : null + h('div', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net)), + !incoming && tx.fee ? h('div', { class: 'small faint' }, t('feeShort', { x: fmtAmount(tx.fee) })) : null ) ); }) - ) + ), + wallet.historyLoading + ? h( + 'div', + { class: 'row gap6', style: 'padding:10px 0 2px;justify-content:center' }, + h('span', { class: 'spinner sm' }), + h('span', { class: 'small muted' }, t('loadingHistory')) + ) + : null ); } -function txDetailView(t) { - const incoming = t.net >= 0; +function txDetailView(tx) { + const incoming = tx.net >= 0; const line = (k, v) => h('div', { class: 'line' }, h('span', { class: 'k' }, k), h('span', { class: 'v' }, v)); return h( 'div', { class: 'card col' }, h('div', { class: 'row between' }, - h('h3', {}, incoming ? 'Received' : 'Sent'), - h('span', { class: `tag ${t.confirmed ? 'conf' : 'pending'}` }, t.confirmed ? 'confirmed' : '⏳ pending') + h('h3', {}, incoming ? t('received') : t('sent')), + h('span', { class: `tag ${tx.confirmed ? 'conf' : 'pending'}` }, tx.confirmed ? t('confirmedTag') : t('pendingTag')) ), h('div', { class: 'amt', style: 'font-size:30px' }, - h('span', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(t.net)), + h('span', { class: incoming ? 'amount-pos' : 'amount-neg' }, (incoming ? '+' : '') + fmtAmount(tx.net)), ' ', unitTag('unit') ), h('div', { class: 'summary col', style: 'gap:0' }, - line('Status', t.confirmed ? 'Confirmed' : 'Pending (in mempool)'), - t.confirmed ? line('Block', String(t.blockHeight || '—')) : null, - t.confirmed && t.blockTime ? line('Date', new Date(t.blockTime * 1000).toLocaleString()) : null, - !incoming && t.fee ? line('Network fee', fmtAmount(t.fee) + ' ' + unitLabel()) : null + line(t('status'), tx.confirmed ? t('confirmed') : t('pendingInMempool')), + tx.confirmed ? line(t('block'), String(tx.blockHeight || '—')) : null, + tx.confirmed && tx.blockTime ? line(t('date'), new Date(tx.blockTime * 1000).toLocaleString()) : null, + !incoming && tx.fee ? line(t('networkFee'), fmtAmount(tx.fee) + ' ' + unitLabel()) : null ), h('div', { class: 'col gap6' }, - h('span', { class: 'lab' }, 'Transaction ID'), - h('div', { class: 'addr-box', style: 'font-size:13px' }, t.txid) + h('span', { class: 'lab' }, t('transactionId')), + h('div', { class: 'addr-box', style: 'font-size:13px' }, tx.txid) ), h('div', { class: 'row gap6 wrap' }, - copyBtn(t.txid, 'Copy ID'), - h('a', { class: 'btn btn-sm', href: wallet.api.explorerTx(t.txid), target: '_blank', rel: 'noopener' }, 'View on mempool.space ↗') + copyBtn(tx.txid, t('copyId')), + h('a', { class: 'btn btn-sm', href: wallet.api.explorerTx(tx.txid), target: '_blank', rel: 'noopener' }, t('viewOnMempool')) ), - h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.txDetail = null; render(); } }, '← Back to history') + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.txDetail = null; render(); } }, t('backToHistory')) ); } @@ -1086,10 +1192,10 @@ function snapshotActions() { 'div', { class: 'col gap6' }, h('p', { class: 'small muted', style: 'margin:0' }, - 'Offline transfer — export your coins on an online device, then import the file on an offline device (with this wallet + seed) to sign without internet.'), + t('offlineTransferDesc')), h('div', { class: 'row gap6 wrap' }, - h('button', { class: 'btn-sm', disabled: !wallet.utxos.length, onClick: exportSnapshot }, '⤓ Export snapshot'), - h('label', { class: 'btn btn-sm', style: 'cursor:pointer' }, 'Import snapshot…', + h('button', { class: 'btn-sm', disabled: !wallet.utxos.length, onClick: exportSnapshot }, t('exportSnapshot')), + h('label', { class: 'btn btn-sm', style: 'cursor:pointer' }, t('importSnapshot'), h('input', { type: 'file', accept: 'application/json,.json', style: 'display:none', onChange: importSnapshotFile }) ) ) @@ -1100,7 +1206,7 @@ function exportSnapshot() { const snap = wallet.exportSnapshot(); const stamp = new Date().toISOString().slice(0, 10); download(`wallet-snapshot-${wallet.netName}-${stamp}.json`, JSON.stringify(snap, null, 2)); - toast('Snapshot exported'); + toast(t('snapshotExported')); } async function importSnapshotFile(e) { @@ -1109,19 +1215,21 @@ async function importSnapshotFile(e) { try { const snap = JSON.parse(await file.text()); const res = wallet.importSnapshot(snap); - let msg = `Imported ${res.imported} coin(s)`; - if (res.unmatched.length) msg += ` · ${res.unmatched.length} address(es) not in this wallet`; + let msg = t('importedNCoins', { n: res.imported }); + if (res.unmatched.length) msg += t('unmatchedSuffix', { n: res.unmatched.length }); toast(msg); ui.tab = 'settings'; render(); } catch (err) { - toast('Import failed: ' + err.message); + toast(t('importFailed', { msg: err.message })); } e.target.value = ''; } // ================================================================ start -// Restore a wallet left open in this tab; otherwise show the unlock screen. +// Apply the detected/saved language direction, then restore a wallet left open +// in this tab; otherwise show the unlock screen. +applyDir(); if (!restoreSession()) render(); diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..1eb8425 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,1062 @@ +// Lightweight i18n. t('key', {vars}) interpolates {var} and falls back to the +// English string (then the key). The chosen language persists in localStorage; +// otherwise it's guessed from the browser. Arabic/Urdu render right-to-left. +// +// NOTE: non-English translations are machine-generated and would benefit from +// native-speaker review. The English table (en) is the source of truth. + +export const LANGS = [ + ['en', 'English'], + ['zh', '中文'], + ['hi', 'हिन्दी'], + ['es', 'Español'], + ['fr', 'Français'], + ['ar', 'العربية'], + ['bn', 'বাংলা'], + ['pt', 'Português'], + ['ru', 'Русский'], + ['ur', 'اردو'], + ['id', 'Indonesia'], + ['de', 'Deutsch'], + ['ja', '日本語'], + ['sw', 'Kiswahili'], + ['mr', 'मराठी'], + ['te', 'తెలుగు'], + ['tr', 'Türkçe'], + ['ta', 'தமிழ்'], + ['vi', 'Tiếng Việt'], + ['ko', '한국어'], +]; + +const RTL = new Set(['ar', 'ur']); +export const isRTL = (l) => RTL.has(l || lang); + +const STR = { + en: { + appTitle: 'Bitcoin Wallet', + home: 'Home', + logout: 'Logout', + createNew: 'Create new', + importExisting: 'Import existing', + genIntro: 'Generate a new 12-word seed phrase. Write it down. It is the only way to recover this wallet.', + generateSeed: 'Generate seed phrase', + writeDownWarn: '⚠ Write these 12 words down on paper, in order. Anyone with them can spend your coins.', + copyPhrase: 'Copy phrase', + regenerate: 'Regenerate', + verifyBackup: 'Verify backup', + skipVerification: 'Skip verification', + confirmBackupIntro: 'Confirm your backup by entering the requested words.', + wordN: 'Word #{n}', + reenterPassphrase: 'Re-enter passphrase', + back: '← Back', + openWallet: 'Open wallet', + wordsMismatch: 'Those words don’t match your phrase. Check your backup.', + passphraseMismatch: 'That passphrase doesn’t match the one you entered.', + seedPhrase: 'Seed phrase', + seedPlaceholder: 'enter your seed words separated by spaces', + optionalPassphrase: 'Optional passphrase', + leaveBlank: 'leave blank if unused', + show: 'Show', + hide: 'Hide', + invalidSeed: 'Invalid seed phrase — check the spelling and word order.', + keysNote: 'Keys never leave your browser. Works fully offline. Save this page and open it from your filesystem.', + howItWorks: 'How it works', + hiwLead: 'A self-custody Bitcoin wallet that runs entirely on your device. No accounts, no servers, no company in the middle.', + hiwBasicsTitle: 'The basics', + hiwBasics1: 'Your wallet is really just 12 words. When you create a wallet, this page picks a random 12-word phrase. Those words are the wallet — anyone who has them controls the money, so write them down and keep them somewhere safe and private.', + hiwBasics2: 'Nothing is stored on a server. There are no sign-ups and no company holding your coins. The words never leave your device, and this page keeps working even with no internet connection.', + hiwBasics3: 'To get paid, you share an address — a string of letters and numbers, or a QR code. To send, you enter the other person’s address and approve the payment.', + hiwBasics4: 'Because you hold the words yourself, you are in full control — and fully responsible. If you lose all 12 words and have no backup, no one can recover the wallet for you. Back them up before you add real money.', + hiwTechTitle: 'For the technically curious', + hiwTech1: 'No backend. The whole app is a single static HTML page — there is no server, database, or API that ever sees your keys. Balances and history are read from a public block explorer (mempool.space); only your addresses, never your keys, leave the device.', + hiwTech2: 'Open standards. The recovery phrase is a 12-word BIP39 mnemonic, and keys follow the BIP84 native SegWit (P2WPKH) account at m/84\'/0\'/0\'. You can restore the exact same wallet in any other standards-compliant wallet.', + hiwTech3: 'Audited cryptography. Key generation, derivation, signing, and hashing all run locally using the open-source @scure and @noble libraries (bip32, bip39, secp256k1). No hand-rolled crypto.', + hiwTech4: 'Offline & air-gapped signing. Save this page and run it straight from your filesystem with no network. For cold storage, keep the keys on a device that never goes online: export a snapshot on an online device, import and sign it offline, then broadcast the signed transaction from any online device.', + hiwTech5: 'Optional passphrase. You can add a BIP39 passphrase (a “25th word”) for an extra layer. It derives a completely separate wallet and must be entered every time — and there is no way to recover it if forgotten.', + balance: 'Balance', + pending: 'Pending', + tabReceive: 'Receive', + tabSend: 'Send', + tabHistory: 'History', + tabSettings: 'Settings', + copyAddress: 'Copy address', + paymentReceived: 'Payment received!', + tapToProceed: 'Tap anywhere to proceed', + recipient: 'Recipient', + recipients: 'Recipients', + max: 'Max', + feeRate: 'Fee rate', + feeEconomy: 'Economy', + feeNormal: 'Normal', + feePriority: 'Priority', + feeCustom: 'Custom', + selectedRate: 'Selected rate: {n} sat/vB', + coinSelection: 'Coin selection', + automatic: 'Automatic', + manual: 'Manual', + noCoins: 'No coins available.', + nSelected: '{n} selected', + addRecipient: '+ Add recipient', + reviewTx: 'Review transaction', + remove: 'Remove', + enterRecipientAddr: 'Enter a recipient address.', + enterAddrForN: 'Enter an address for recipient {n}.', + enterValidAmtForN: 'Enter a valid amount for recipient {n}.', + selectCoin: 'Select at least one coin to spend.', + networkFee: 'Network fee', + offlineSignNote: 'Offline: sign now, then broadcast the signed transaction from an online device.', + signBroadcast: 'Sign & broadcast', + signTx: 'Sign transaction', + broadcastFailed: 'Broadcast failed: {msg}', + signingFailed: 'Signing failed: {msg}', + done: 'Done', + txSignedNote: '✓ Transaction signed. Broadcast the code below from any online device (e.g. mempool.space → Broadcast).', + transactionId: 'Transaction ID', + signedTxRaw: 'Signed transaction (code)', + copyHex: 'Copy code', + downloadLabel: 'Download', + showQrAirgap: 'Show QR (for offline transfer)', + txBroadcast: '✓ Transaction broadcast!', + copyTxid: 'Copy transaction ID', + viewOnMempool: 'View on mempool.space ↗', + noTxYet: 'No transactions yet.', + loadingHistory: 'Loading transactions…', + received: 'Received', + sent: 'Sent', + pendingTag: '⏳ pending', + awaitingConfirmation: 'awaiting confirmation', + feeShort: 'fee {x}', + historyOffline: 'Transaction history is unavailable in offline mode.', + status: 'Status', + confirmed: 'Confirmed', + confirmedTag: 'confirmed', + pendingInMempool: 'Pending (in mempool)', + block: 'Block', + date: 'Date', + copyId: 'Copy ID', + backToHistory: '← Back to history', + recoveryPhrase: 'Recovery phrase', + recoveryWarn: '⚠ Anyone who can see these words can steal your funds. Make sure nobody is watching before you reveal them.', + bip39Passphrase: 'Passphrase', + copyPassphrase: 'Copy passphrase', + revealRecovery: 'Reveal recovery phrase', + offlineTransfer: 'Offline transfer', + offlineTransferDesc: 'Export your coins on an online device, then import the file on an offline device (with this wallet + seed) to sign without internet.', + exportSnapshot: '⤓ Export snapshot', + importSnapshot: 'Import snapshot…', + rescan: 'Rescan', + rescanDesc: 'Your current address updates automatically. Rescan only if a payment was sent to an old (reused) address and isn’t showing.', + rescanWallet: 'Rescan wallet', + scanning: 'Scanning…', + language: 'Language', + offlineBanner: "Can't reach the network — working offline. Import a snapshot on the Settings tab to load coins.", + retry: 'Retry', + copied: 'Copied to clipboard', + snapshotExported: 'Snapshot exported', + importedNCoins: 'Imported {n} coin(s)', + unmatchedSuffix: ' · {n} address(es) not in this wallet', + importFailed: 'Import failed: {msg}', + copy: 'Copy', + switchUnit: 'Switch BTC / sats', + footerMadeBy: 'made by', + footerSourceOn: 'source available on', + }, +}; + +// --- translations --------------------------------------------------------- +// Helper to define a language as overrides on top of English keys. +function def(code, map) { + STR[code] = map; +} + +def('es', { + appTitle: 'Monedero Bitcoin', home: 'Inicio', logout: 'Cerrar sesión', + createNew: 'Crear nuevo', importExisting: 'Importar existente', + genIntro: 'Genera una nueva frase semilla de 12 palabras. Anótala. Es la única forma de recuperar este monedero.', + generateSeed: 'Generar frase semilla', + writeDownWarn: '⚠ Anota estas 12 palabras en papel, en orden. Cualquiera que las tenga puede gastar tus fondos.', + copyPhrase: 'Copiar frase', regenerate: 'Regenerar', verifyBackup: 'Verificar copia', skipVerification: 'Omitir verificación', + confirmBackupIntro: 'Confirma tu copia introduciendo las palabras solicitadas.', wordN: 'Palabra n.º {n}', + reenterPassphrase: 'Reintroduce la contraseña', back: '← Atrás', openWallet: 'Abrir monedero', + wordsMismatch: 'Esas palabras no coinciden con tu frase. Revisa tu copia.', + passphraseMismatch: 'Esa contraseña no coincide con la que introdujiste.', + seedPhrase: 'Frase semilla', seedPlaceholder: 'escribe tus palabras separadas por espacios', + optionalPassphrase: 'Contraseña opcional', leaveBlank: 'déjalo en blanco si no se usa', show: 'Mostrar', hide: 'Ocultar', + invalidSeed: 'Frase semilla no válida — revisa la ortografía y el orden.', + keysNote: 'Las claves nunca salen de tu navegador. Funciona sin conexión. Guarda esta página y ábrela desde tu equipo.', + balance: 'Saldo', pending: 'Pendiente', + tabReceive: 'Recibir', tabSend: 'Enviar', tabHistory: 'Historial', tabSettings: 'Ajustes', + copyAddress: 'Copiar dirección', paymentReceived: '¡Pago recibido!', tapToProceed: 'Toca en cualquier lugar para continuar', + recipient: 'Destinatario', recipients: 'Destinatarios', max: 'Máx', feeRate: 'Tarifa', + feeEconomy: 'Económica', feeNormal: 'Normal', feePriority: 'Prioritaria', feeCustom: 'Personalizada', + selectedRate: 'Tarifa elegida: {n} sat/vB', coinSelection: 'Selección de monedas', automatic: 'Automática', manual: 'Manual', + noCoins: 'No hay monedas disponibles.', nSelected: '{n} seleccionadas', addRecipient: '+ Añadir destinatario', + reviewTx: 'Revisar transacción', remove: 'Quitar', + enterRecipientAddr: 'Introduce una dirección de destino.', enterAddrForN: 'Introduce una dirección para el destinatario {n}.', + enterValidAmtForN: 'Introduce un importe válido para el destinatario {n}.', selectCoin: 'Selecciona al menos una moneda para gastar.', + networkFee: 'Comisión de red', offlineSignNote: 'Sin conexión: firma ahora y difunde la transacción desde un dispositivo conectado.', + signBroadcast: 'Firmar y difundir', signTx: 'Firmar transacción', broadcastFailed: 'Falló la difusión: {msg}', signingFailed: 'Falló la firma: {msg}', + done: 'Listo', txSignedNote: '✓ Transacción firmada. Difunde el código de abajo desde un dispositivo conectado (p. ej. mempool.space → Broadcast).', + transactionId: 'ID de transacción', signedTxRaw: 'Transacción firmada (código)', copyHex: 'Copiar código', downloadLabel: 'Descargar', + showQrAirgap: 'Mostrar QR (para transferencia sin conexión)', txBroadcast: '✓ ¡Transacción difundida!', copyTxid: 'Copiar ID de transacción', viewOnMempool: 'Ver en mempool.space ↗', + noTxYet: 'Aún no hay transacciones.', received: 'Recibido', sent: 'Enviado', pendingTag: '⏳ pendiente', awaitingConfirmation: 'esperando confirmación', + feeShort: 'comisión {x}', historyOffline: 'El historial no está disponible sin conexión.', + status: 'Estado', confirmed: 'Confirmada', confirmedTag: 'confirmada', pendingInMempool: 'Pendiente (en mempool)', block: 'Bloque', date: 'Fecha', + copyId: 'Copiar ID', backToHistory: '← Volver al historial', + recoveryPhrase: 'Frase de recuperación', recoveryWarn: '⚠ Cualquiera que vea estas palabras puede robar tus fondos. Asegúrate de que nadie mire antes de mostrarlas.', + bip39Passphrase: 'Contraseña', copyPassphrase: 'Copiar contraseña', revealRecovery: 'Mostrar frase de recuperación', + offlineTransfer: 'Transferencia sin conexión', offlineTransferDesc: 'Exporta tus monedas en un dispositivo conectado, luego importa el archivo en uno sin conexión (con este monedero + semilla) para firmar sin internet.', + exportSnapshot: '⤓ Exportar copia', importSnapshot: 'Importar copia…', rescan: 'Reescanear', + rescanDesc: 'Tu dirección actual se actualiza automáticamente. Reescanea solo si enviaste un pago a una dirección antigua (reutilizada) y no aparece.', + rescanWallet: 'Reescanear monedero', scanning: 'Escaneando…', language: 'Idioma', + offlineBanner: 'Sin conexión a la red — trabajando sin conexión. Importa una copia en Ajustes para cargar monedas.', retry: 'Reintentar', + copied: 'Copiado al portapapeles', snapshotExported: 'Copia exportada', importedNCoins: '{n} moneda(s) importada(s)', + unmatchedSuffix: ' · {n} dirección(es) no pertenecen a este monedero', importFailed: 'Falló la importación: {msg}', + copy: 'Copiar', switchUnit: 'Cambiar BTC / sats', footerMadeBy: 'hecho por', footerSourceOn: 'código disponible en', +}); + +def('fr', { + appTitle: 'Portefeuille Bitcoin', home: 'Accueil', logout: 'Déconnexion', + createNew: 'Créer', importExisting: 'Importer', + genIntro: 'Générez une nouvelle phrase de 12 mots. Notez-la. C’est le seul moyen de récupérer ce portefeuille.', + generateSeed: 'Générer la phrase', writeDownWarn: '⚠ Notez ces 12 mots sur papier, dans l’ordre. Quiconque les a peut dépenser vos fonds.', + copyPhrase: 'Copier la phrase', regenerate: 'Régénérer', verifyBackup: 'Vérifier la sauvegarde', skipVerification: 'Ignorer la vérification', + confirmBackupIntro: 'Confirmez votre sauvegarde en saisissant les mots demandés.', wordN: 'Mot n° {n}', + reenterPassphrase: 'Ressaisir la phrase secrète', back: '← Retour', openWallet: 'Ouvrir le portefeuille', + wordsMismatch: 'Ces mots ne correspondent pas à votre phrase. Vérifiez votre sauvegarde.', + passphraseMismatch: 'Cette phrase secrète ne correspond pas à celle saisie.', + seedPhrase: 'Phrase de récupération', seedPlaceholder: 'saisissez vos mots séparés par des espaces', + optionalPassphrase: 'Phrase secrète (facultative)', leaveBlank: 'laisser vide si non utilisée', show: 'Afficher', hide: 'Masquer', + invalidSeed: 'Phrase invalide — vérifiez l’orthographe et l’ordre des mots.', + keysNote: 'Les clés ne quittent jamais votre navigateur. Fonctionne hors ligne. Enregistrez cette page et ouvrez-la localement.', + balance: 'Solde', pending: 'En attente', + tabReceive: 'Recevoir', tabSend: 'Envoyer', tabHistory: 'Historique', tabSettings: 'Réglages', + copyAddress: 'Copier l’adresse', paymentReceived: 'Paiement reçu !', tapToProceed: 'Touchez n’importe où pour continuer', + recipient: 'Destinataire', recipients: 'Destinataires', max: 'Max', feeRate: 'Frais', + feeEconomy: 'Économique', feeNormal: 'Normal', feePriority: 'Prioritaire', feeCustom: 'Personnalisé', + selectedRate: 'Frais choisis : {n} sat/vB', coinSelection: 'Sélection des pièces', automatic: 'Automatique', manual: 'Manuel', + noCoins: 'Aucune pièce disponible.', nSelected: '{n} sélectionnée(s)', addRecipient: '+ Ajouter un destinataire', + reviewTx: 'Vérifier la transaction', remove: 'Retirer', + enterRecipientAddr: 'Saisissez une adresse de destination.', enterAddrForN: 'Saisissez une adresse pour le destinataire {n}.', + enterValidAmtForN: 'Saisissez un montant valide pour le destinataire {n}.', selectCoin: 'Sélectionnez au moins une pièce à dépenser.', + networkFee: 'Frais de réseau', offlineSignNote: 'Hors ligne : signez maintenant, puis diffusez la transaction depuis un appareil connecté.', + signBroadcast: 'Signer et diffuser', signTx: 'Signer la transaction', broadcastFailed: 'Échec de la diffusion : {msg}', signingFailed: 'Échec de la signature : {msg}', + done: 'Terminé', txSignedNote: '✓ Transaction signée. Diffusez le code ci-dessous depuis un appareil connecté (par ex. mempool.space → Broadcast).', + transactionId: 'ID de transaction', signedTxRaw: 'Transaction signée (code)', copyHex: 'Copier le code', downloadLabel: 'Télécharger', + showQrAirgap: 'Afficher le QR (transfert hors ligne)', txBroadcast: '✓ Transaction diffusée !', copyTxid: 'Copier l’ID de transaction', viewOnMempool: 'Voir sur mempool.space ↗', + noTxYet: 'Aucune transaction.', received: 'Reçu', sent: 'Envoyé', pendingTag: '⏳ en attente', awaitingConfirmation: 'en attente de confirmation', + feeShort: 'frais {x}', historyOffline: 'L’historique est indisponible hors ligne.', + status: 'Statut', confirmed: 'Confirmée', confirmedTag: 'confirmée', pendingInMempool: 'En attente (mempool)', block: 'Bloc', date: 'Date', + copyId: 'Copier l’ID', backToHistory: '← Retour à l’historique', + recoveryPhrase: 'Phrase de récupération', recoveryWarn: '⚠ Quiconque voit ces mots peut voler vos fonds. Assurez-vous que personne ne regarde avant de les afficher.', + bip39Passphrase: 'Phrase secrète', copyPassphrase: 'Copier la phrase secrète', revealRecovery: 'Afficher la phrase de récupération', + offlineTransfer: 'Transfert hors ligne', offlineTransferDesc: 'Exportez vos pièces sur un appareil connecté, puis importez le fichier sur un appareil hors ligne (avec ce portefeuille + phrase) pour signer sans internet.', + exportSnapshot: '⤓ Exporter l’instantané', importSnapshot: 'Importer l’instantané…', rescan: 'Rescanner', + rescanDesc: 'Votre adresse actuelle se met à jour automatiquement. Rescannez seulement si un paiement a été envoyé à une ancienne adresse (réutilisée) et n’apparaît pas.', + rescanWallet: 'Rescanner le portefeuille', scanning: 'Analyse…', language: 'Langue', + offlineBanner: 'Réseau inaccessible — mode hors ligne. Importez un instantané dans Réglages pour charger les pièces.', retry: 'Réessayer', + copied: 'Copié dans le presse-papiers', snapshotExported: 'Instantané exporté', importedNCoins: '{n} pièce(s) importée(s)', + unmatchedSuffix: ' · {n} adresse(s) hors de ce portefeuille', importFailed: 'Échec de l’import : {msg}', + copy: 'Copier', switchUnit: 'Basculer BTC / sats', footerMadeBy: 'fait par', footerSourceOn: 'code source sur', +}); + +def('de', { + appTitle: 'Bitcoin-Wallet', home: 'Start', logout: 'Abmelden', + createNew: 'Neu erstellen', importExisting: 'Importieren', + genIntro: 'Erzeuge eine neue Seed-Phrase aus 12 Wörtern. Schreibe sie auf. Sie ist die einzige Möglichkeit, dieses Wallet wiederherzustellen.', + generateSeed: 'Seed-Phrase erzeugen', writeDownWarn: '⚠ Schreibe diese 12 Wörter in der richtigen Reihenfolge auf Papier. Wer sie hat, kann deine Coins ausgeben.', + copyPhrase: 'Phrase kopieren', regenerate: 'Neu erzeugen', verifyBackup: 'Backup prüfen', skipVerification: 'Prüfung überspringen', + confirmBackupIntro: 'Bestätige dein Backup, indem du die abgefragten Wörter eingibst.', wordN: 'Wort #{n}', + reenterPassphrase: 'Passphrase erneut eingeben', back: '← Zurück', openWallet: 'Wallet öffnen', + wordsMismatch: 'Diese Wörter passen nicht zu deiner Phrase. Prüfe dein Backup.', + passphraseMismatch: 'Diese Passphrase stimmt nicht mit der eingegebenen überein.', + seedPhrase: 'Seed-Phrase', seedPlaceholder: 'gib deine Wörter durch Leerzeichen getrennt ein', + optionalPassphrase: 'Optionale Passphrase', leaveBlank: 'leer lassen, falls nicht genutzt', show: 'Zeigen', hide: 'Verbergen', + invalidSeed: 'Ungültige Seed-Phrase — prüfe Schreibweise und Reihenfolge.', + keysNote: 'Schlüssel verlassen nie deinen Browser. Funktioniert offline. Speichere diese Seite und öffne sie lokal.', + balance: 'Guthaben', pending: 'Ausstehend', + tabReceive: 'Empfangen', tabSend: 'Senden', tabHistory: 'Verlauf', tabSettings: 'Einstellungen', + copyAddress: 'Adresse kopieren', paymentReceived: 'Zahlung erhalten!', tapToProceed: 'Tippe irgendwo, um fortzufahren', + recipient: 'Empfänger', recipients: 'Empfänger', max: 'Max', feeRate: 'Gebühr', + feeEconomy: 'Sparsam', feeNormal: 'Normal', feePriority: 'Priorität', feeCustom: 'Benutzerdef.', + selectedRate: 'Gewählte Gebühr: {n} sat/vB', coinSelection: 'Coin-Auswahl', automatic: 'Automatisch', manual: 'Manuell', + noCoins: 'Keine Coins verfügbar.', nSelected: '{n} ausgewählt', addRecipient: '+ Empfänger hinzufügen', + reviewTx: 'Transaktion prüfen', remove: 'Entfernen', + enterRecipientAddr: 'Gib eine Empfängeradresse ein.', enterAddrForN: 'Gib eine Adresse für Empfänger {n} ein.', + enterValidAmtForN: 'Gib einen gültigen Betrag für Empfänger {n} ein.', selectCoin: 'Wähle mindestens einen Coin zum Ausgeben.', + networkFee: 'Netzwerkgebühr', offlineSignNote: 'Offline: jetzt signieren und die Transaktion von einem Online-Gerät senden.', + signBroadcast: 'Signieren & senden', signTx: 'Transaktion signieren', broadcastFailed: 'Senden fehlgeschlagen: {msg}', signingFailed: 'Signieren fehlgeschlagen: {msg}', + done: 'Fertig', txSignedNote: '✓ Transaktion signiert. Sende den Code unten von einem Online-Gerät (z. B. mempool.space → Broadcast).', + transactionId: 'Transaktions-ID', signedTxRaw: 'Signierte Transaktion (Code)', copyHex: 'Code kopieren', downloadLabel: 'Herunterladen', + showQrAirgap: 'QR anzeigen (für Offline-Übertragung)', txBroadcast: '✓ Transaktion gesendet!', copyTxid: 'Transaktions-ID kopieren', viewOnMempool: 'Auf mempool.space ansehen ↗', + noTxYet: 'Noch keine Transaktionen.', received: 'Empfangen', sent: 'Gesendet', pendingTag: '⏳ ausstehend', awaitingConfirmation: 'wartet auf Bestätigung', + feeShort: 'Gebühr {x}', historyOffline: 'Verlauf ist offline nicht verfügbar.', + status: 'Status', confirmed: 'Bestätigt', confirmedTag: 'bestätigt', pendingInMempool: 'Ausstehend (im Mempool)', block: 'Block', date: 'Datum', + copyId: 'ID kopieren', backToHistory: '← Zurück zum Verlauf', + recoveryPhrase: 'Wiederherstellungsphrase', recoveryWarn: '⚠ Wer diese Wörter sieht, kann deine Coins stehlen. Achte darauf, dass niemand zusieht, bevor du sie anzeigst.', + bip39Passphrase: 'Passphrase', copyPassphrase: 'Passphrase kopieren', revealRecovery: 'Wiederherstellungsphrase anzeigen', + offlineTransfer: 'Offline-Übertragung', offlineTransferDesc: 'Exportiere deine Coins auf einem Online-Gerät und importiere die Datei auf einem Offline-Gerät (mit diesem Wallet + Seed), um ohne Internet zu signieren.', + exportSnapshot: '⤓ Snapshot exportieren', importSnapshot: 'Snapshot importieren…', rescan: 'Neu scannen', + rescanDesc: 'Deine aktuelle Adresse aktualisiert sich automatisch. Scanne nur neu, wenn eine Zahlung an eine alte (wiederverwendete) Adresse ging und nicht erscheint.', + rescanWallet: 'Wallet neu scannen', scanning: 'Scanne…', language: 'Sprache', + offlineBanner: 'Netzwerk nicht erreichbar — offline. Importiere einen Snapshot unter Einstellungen, um Coins zu laden.', retry: 'Erneut', + copied: 'In Zwischenablage kopiert', snapshotExported: 'Snapshot exportiert', importedNCoins: '{n} Coin(s) importiert', + unmatchedSuffix: ' · {n} Adresse(n) nicht in diesem Wallet', importFailed: 'Import fehlgeschlagen: {msg}', + copy: 'Kopieren', switchUnit: 'BTC / sats wechseln', footerMadeBy: 'gemacht von', footerSourceOn: 'Quellcode auf', +}); + +def('pt', { + appTitle: 'Carteira Bitcoin', home: 'Início', logout: 'Sair', + createNew: 'Criar nova', importExisting: 'Importar existente', + genIntro: 'Gere uma nova frase semente de 12 palavras. Anote-a. É a única forma de recuperar esta carteira.', + generateSeed: 'Gerar frase semente', writeDownWarn: '⚠ Anote estas 12 palavras no papel, em ordem. Quem as tiver pode gastar suas moedas.', + copyPhrase: 'Copiar frase', regenerate: 'Gerar novamente', verifyBackup: 'Verificar backup', skipVerification: 'Pular verificação', + confirmBackupIntro: 'Confirme seu backup digitando as palavras solicitadas.', wordN: 'Palavra nº {n}', + reenterPassphrase: 'Digite a senha novamente', back: '← Voltar', openWallet: 'Abrir carteira', + wordsMismatch: 'Essas palavras não correspondem à sua frase. Verifique seu backup.', + passphraseMismatch: 'Essa senha não corresponde à que você digitou.', + seedPhrase: 'Frase semente', seedPlaceholder: 'digite suas palavras separadas por espaços', + optionalPassphrase: 'Senha opcional', leaveBlank: 'deixe em branco se não usar', show: 'Mostrar', hide: 'Ocultar', + invalidSeed: 'Frase semente inválida — verifique a grafia e a ordem das palavras.', + keysNote: 'As chaves nunca saem do seu navegador. Funciona offline. Salve esta página e abra-a localmente.', + balance: 'Saldo', pending: 'Pendente', + tabReceive: 'Receber', tabSend: 'Enviar', tabHistory: 'Histórico', tabSettings: 'Ajustes', + copyAddress: 'Copiar endereço', paymentReceived: 'Pagamento recebido!', tapToProceed: 'Toque em qualquer lugar para continuar', + recipient: 'Destinatário', recipients: 'Destinatários', max: 'Máx', feeRate: 'Taxa', + feeEconomy: 'Econômica', feeNormal: 'Normal', feePriority: 'Prioridade', feeCustom: 'Personalizada', + selectedRate: 'Taxa escolhida: {n} sat/vB', coinSelection: 'Seleção de moedas', automatic: 'Automática', manual: 'Manual', + noCoins: 'Nenhuma moeda disponível.', nSelected: '{n} selecionada(s)', addRecipient: '+ Adicionar destinatário', + reviewTx: 'Revisar transação', remove: 'Remover', + enterRecipientAddr: 'Digite um endereço de destino.', enterAddrForN: 'Digite um endereço para o destinatário {n}.', + enterValidAmtForN: 'Digite um valor válido para o destinatário {n}.', selectCoin: 'Selecione ao menos uma moeda para gastar.', + networkFee: 'Taxa de rede', offlineSignNote: 'Offline: assine agora e transmita a transação de um dispositivo conectado.', + signBroadcast: 'Assinar e transmitir', signTx: 'Assinar transação', broadcastFailed: 'Falha ao transmitir: {msg}', signingFailed: 'Falha ao assinar: {msg}', + done: 'Concluído', txSignedNote: '✓ Transação assinada. Transmita o código abaixo de um dispositivo conectado (ex.: mempool.space → Broadcast).', + transactionId: 'ID da transação', signedTxRaw: 'Transação assinada (código)', copyHex: 'Copiar código', downloadLabel: 'Baixar', + showQrAirgap: 'Mostrar QR (transferência offline)', txBroadcast: '✓ Transação transmitida!', copyTxid: 'Copiar ID da transação', viewOnMempool: 'Ver no mempool.space ↗', + noTxYet: 'Nenhuma transação ainda.', received: 'Recebido', sent: 'Enviado', pendingTag: '⏳ pendente', awaitingConfirmation: 'aguardando confirmação', + feeShort: 'taxa {x}', historyOffline: 'O histórico não está disponível offline.', + status: 'Status', confirmed: 'Confirmada', confirmedTag: 'confirmada', pendingInMempool: 'Pendente (no mempool)', block: 'Bloco', date: 'Data', + copyId: 'Copiar ID', backToHistory: '← Voltar ao histórico', + recoveryPhrase: 'Frase de recuperação', recoveryWarn: '⚠ Quem vir estas palavras pode roubar seus fundos. Certifique-se de que ninguém esteja olhando antes de revelá-las.', + bip39Passphrase: 'Senha', copyPassphrase: 'Copiar senha', revealRecovery: 'Revelar frase de recuperação', + offlineTransfer: 'Transferência offline', offlineTransferDesc: 'Exporte suas moedas em um dispositivo conectado, depois importe o arquivo em um offline (com esta carteira + semente) para assinar sem internet.', + exportSnapshot: '⤓ Exportar snapshot', importSnapshot: 'Importar snapshot…', rescan: 'Reescanear', + rescanDesc: 'Seu endereço atual atualiza automaticamente. Reescaneie apenas se um pagamento foi enviado a um endereço antigo (reutilizado) e não aparece.', + rescanWallet: 'Reescanear carteira', scanning: 'Escaneando…', language: 'Idioma', + offlineBanner: 'Sem acesso à rede — trabalhando offline. Importe um snapshot em Ajustes para carregar moedas.', retry: 'Tentar de novo', + copied: 'Copiado', snapshotExported: 'Snapshot exportado', importedNCoins: '{n} moeda(s) importada(s)', + unmatchedSuffix: ' · {n} endereço(s) fora desta carteira', importFailed: 'Falha ao importar: {msg}', + copy: 'Copiar', switchUnit: 'Alternar BTC / sats', footerMadeBy: 'feito por', footerSourceOn: 'código disponível no', +}); + +def('ru', { + appTitle: 'Биткойн-кошелёк', home: 'Главная', logout: 'Выйти', + createNew: 'Создать', importExisting: 'Импортировать', + genIntro: 'Сгенерируйте новую сид-фразу из 12 слов. Запишите её. Это единственный способ восстановить кошелёк.', + generateSeed: 'Сгенерировать фразу', writeDownWarn: '⚠ Запишите эти 12 слов на бумаге, по порядку. Любой, у кого они есть, сможет потратить ваши монеты.', + copyPhrase: 'Копировать фразу', regenerate: 'Сгенерировать заново', verifyBackup: 'Проверить копию', skipVerification: 'Пропустить проверку', + confirmBackupIntro: 'Подтвердите копию, введя запрошенные слова.', wordN: 'Слово №{n}', + reenterPassphrase: 'Повторите пароль-фразу', back: '← Назад', openWallet: 'Открыть кошелёк', + wordsMismatch: 'Эти слова не совпадают с вашей фразой. Проверьте копию.', + passphraseMismatch: 'Эта пароль-фраза не совпадает с введённой.', + seedPhrase: 'Сид-фраза', seedPlaceholder: 'введите слова через пробел', + optionalPassphrase: 'Пароль-фраза (необязательно)', leaveBlank: 'оставьте пустым, если не используется', show: 'Показать', hide: 'Скрыть', + invalidSeed: 'Неверная сид-фраза — проверьте написание и порядок слов.', + keysNote: 'Ключи не покидают браузер. Работает офлайн. Сохраните эту страницу и откройте локально.', + balance: 'Баланс', pending: 'В ожидании', + tabReceive: 'Получить', tabSend: 'Отправить', tabHistory: 'История', tabSettings: 'Настройки', + copyAddress: 'Копировать адрес', paymentReceived: 'Платёж получен!', tapToProceed: 'Нажмите в любом месте, чтобы продолжить', + recipient: 'Получатель', recipients: 'Получатели', max: 'Макс', feeRate: 'Комиссия', + feeEconomy: 'Эконом', feeNormal: 'Обычная', feePriority: 'Приоритет', feeCustom: 'Своя', + selectedRate: 'Выбрано: {n} sat/vB', coinSelection: 'Выбор монет', automatic: 'Авто', manual: 'Вручную', + noCoins: 'Нет доступных монет.', nSelected: 'выбрано: {n}', addRecipient: '+ Добавить получателя', + reviewTx: 'Проверить транзакцию', remove: 'Удалить', + enterRecipientAddr: 'Введите адрес получателя.', enterAddrForN: 'Введите адрес для получателя {n}.', + enterValidAmtForN: 'Введите корректную сумму для получателя {n}.', selectCoin: 'Выберите хотя бы одну монету.', + networkFee: 'Комиссия сети', offlineSignNote: 'Офлайн: подпишите сейчас, затем отправьте транзакцию с устройства с интернетом.', + signBroadcast: 'Подписать и отправить', signTx: 'Подписать транзакцию', broadcastFailed: 'Ошибка отправки: {msg}', signingFailed: 'Ошибка подписи: {msg}', + done: 'Готово', txSignedNote: '✓ Транзакция подписана. Отправьте код ниже с устройства с интернетом (напр. mempool.space → Broadcast).', + transactionId: 'ID транзакции', signedTxRaw: 'Подписанная транзакция (код)', copyHex: 'Копировать код', downloadLabel: 'Скачать', + showQrAirgap: 'Показать QR (для офлайн-передачи)', txBroadcast: '✓ Транзакция отправлена!', copyTxid: 'Копировать ID транзакции', viewOnMempool: 'Открыть на mempool.space ↗', + noTxYet: 'Транзакций пока нет.', received: 'Получено', sent: 'Отправлено', pendingTag: '⏳ в ожидании', awaitingConfirmation: 'ожидает подтверждения', + feeShort: 'комиссия {x}', historyOffline: 'История недоступна офлайн.', + status: 'Статус', confirmed: 'Подтверждена', confirmedTag: 'подтверждена', pendingInMempool: 'В ожидании (в мемпуле)', block: 'Блок', date: 'Дата', + copyId: 'Копировать ID', backToHistory: '← К истории', + recoveryPhrase: 'Фраза восстановления', recoveryWarn: '⚠ Любой, кто увидит эти слова, может украсть ваши средства. Убедитесь, что никто не смотрит.', + bip39Passphrase: 'Пароль-фраза', copyPassphrase: 'Копировать пароль-фразу', revealRecovery: 'Показать фразу восстановления', + offlineTransfer: 'Офлайн-передача', offlineTransferDesc: 'Экспортируйте монеты на устройстве с интернетом, затем импортируйте файл на офлайн-устройстве (с этим кошельком и сид-фразой), чтобы подписать без интернета.', + exportSnapshot: '⤓ Экспорт снимка', importSnapshot: 'Импорт снимка…', rescan: 'Пересканировать', + rescanDesc: 'Текущий адрес обновляется автоматически. Пересканируйте, только если платёж пришёл на старый (повторно использованный) адрес и не отображается.', + rescanWallet: 'Пересканировать кошелёк', scanning: 'Сканирование…', language: 'Язык', + offlineBanner: 'Нет сети — работа офлайн. Импортируйте снимок в Настройках, чтобы загрузить монеты.', retry: 'Повторить', + copied: 'Скопировано', snapshotExported: 'Снимок экспортирован', importedNCoins: 'Импортировано монет: {n}', + unmatchedSuffix: ' · {n} адрес(ов) не из этого кошелька', importFailed: 'Ошибка импорта: {msg}', + copy: 'Копировать', switchUnit: 'Переключить BTC / sats', footerMadeBy: 'сделано —', footerSourceOn: 'исходный код на', +}); + +def('zh', { + appTitle: '比特币钱包', home: '首页', logout: '退出', + createNew: '新建', importExisting: '导入已有', + genIntro: '生成一组新的 12 词助记词。请抄写下来。这是恢复钱包的唯一方式。', + generateSeed: '生成助记词', writeDownWarn: '⚠ 请按顺序将这 12 个词抄在纸上。任何拿到它们的人都能花掉你的币。', + copyPhrase: '复制助记词', regenerate: '重新生成', verifyBackup: '验证备份', skipVerification: '跳过验证', + confirmBackupIntro: '请输入指定的词以确认你的备份。', wordN: '第 {n} 个词', + reenterPassphrase: '再次输入密码短语', back: '← 返回', openWallet: '打开钱包', + wordsMismatch: '这些词与你的助记词不符。请检查备份。', + passphraseMismatch: '该密码短语与你输入的不符。', + seedPhrase: '助记词', seedPlaceholder: '输入助记词,用空格分隔', + optionalPassphrase: '可选密码短语', leaveBlank: '不使用请留空', show: '显示', hide: '隐藏', + invalidSeed: '助记词无效——请检查拼写和顺序。', + keysNote: '密钥永不离开你的浏览器。可完全离线使用。保存此页面并从本地打开。', + balance: '余额', pending: '待确认', + tabReceive: '接收', tabSend: '发送', tabHistory: '历史', tabSettings: '设置', + copyAddress: '复制地址', paymentReceived: '已收到付款!', tapToProceed: '点击任意处继续', + recipient: '收款人', recipients: '收款人', max: '全部', feeRate: '费率', + feeEconomy: '经济', feeNormal: '普通', feePriority: '优先', feeCustom: '自定义', + selectedRate: '所选费率:{n} sat/vB', coinSelection: '选币', automatic: '自动', manual: '手动', + noCoins: '没有可用的币。', nSelected: '已选 {n}', addRecipient: '+ 添加收款人', + reviewTx: '检查交易', remove: '移除', + enterRecipientAddr: '请输入收款地址。', enterAddrForN: '请输入收款人 {n} 的地址。', + enterValidAmtForN: '请输入收款人 {n} 的有效金额。', selectCoin: '请至少选择一枚币来花费。', + networkFee: '网络手续费', offlineSignNote: '离线:现在签名,然后在联网设备上广播该交易。', + signBroadcast: '签名并广播', signTx: '签名交易', broadcastFailed: '广播失败:{msg}', signingFailed: '签名失败:{msg}', + done: '完成', txSignedNote: '✓ 交易已签名。请在任意联网设备上广播下方的代码(例如 mempool.space → Broadcast)。', + transactionId: '交易 ID', signedTxRaw: '已签名交易(代码)', copyHex: '复制代码', downloadLabel: '下载', + showQrAirgap: '显示二维码(用于离线传输)', txBroadcast: '✓ 交易已广播!', copyTxid: '复制交易 ID', viewOnMempool: '在 mempool.space 查看 ↗', + noTxYet: '暂无交易。', received: '已收到', sent: '已发送', pendingTag: '⏳ 待确认', awaitingConfirmation: '等待确认', + feeShort: '手续费 {x}', historyOffline: '离线模式下无法查看历史。', + status: '状态', confirmed: '已确认', confirmedTag: '已确认', pendingInMempool: '待确认(在内存池)', block: '区块', date: '日期', + copyId: '复制 ID', backToHistory: '← 返回历史', + recoveryPhrase: '恢复助记词', recoveryWarn: '⚠ 任何看到这些词的人都能盗走你的资金。显示前请确保无人窥视。', + bip39Passphrase: '密码短语', copyPassphrase: '复制密码短语', revealRecovery: '显示恢复助记词', + offlineTransfer: '离线传输', offlineTransferDesc: '在联网设备上导出你的币,然后在离线设备上(使用此钱包和助记词)导入文件,即可离线签名。', + exportSnapshot: '⤓ 导出快照', importSnapshot: '导入快照…', rescan: '重新扫描', + rescanDesc: '当前地址会自动更新。仅当付款发到旧(重复使用)地址且未显示时才需重新扫描。', + rescanWallet: '重新扫描钱包', scanning: '扫描中…', language: '语言', + offlineBanner: '无法连接网络——离线工作。请在“设置”中导入快照以加载币。', retry: '重试', + copied: '已复制', snapshotExported: '快照已导出', importedNCoins: '已导入 {n} 枚币', + unmatchedSuffix: ' · {n} 个地址不属于此钱包', importFailed: '导入失败:{msg}', + copy: '复制', switchUnit: '切换 BTC / sats', footerMadeBy: '制作者', footerSourceOn: '源代码见', +}); + +def('ja', { + appTitle: 'ビットコインウォレット', home: 'ホーム', logout: 'ログアウト', + createNew: '新規作成', importExisting: 'インポート', + genIntro: '新しい12単語のシードフレーズを生成します。書き留めてください。これがこのウォレットを復元する唯一の方法です。', + generateSeed: 'シードフレーズを生成', writeDownWarn: '⚠ この12単語を順番どおり紙に書き留めてください。これを持つ人は誰でもあなたのコインを使えます。', + copyPhrase: 'フレーズをコピー', regenerate: '再生成', verifyBackup: 'バックアップを確認', skipVerification: '確認をスキップ', + confirmBackupIntro: '指定された単語を入力してバックアップを確認してください。', wordN: '{n} 番目の単語', + reenterPassphrase: 'パスフレーズを再入力', back: '← 戻る', openWallet: 'ウォレットを開く', + wordsMismatch: '単語がフレーズと一致しません。バックアップを確認してください。', + passphraseMismatch: 'パスフレーズが入力したものと一致しません。', + seedPhrase: 'シードフレーズ', seedPlaceholder: '単語をスペース区切りで入力', + optionalPassphrase: '任意のパスフレーズ', leaveBlank: '使わない場合は空欄', show: '表示', hide: '隠す', + invalidSeed: '無効なシードフレーズ — つづりと順番を確認してください。', + keysNote: '鍵はブラウザから出ません。完全にオフラインで動作します。このページを保存してローカルで開けます。', + balance: '残高', pending: '保留中', + tabReceive: '受取', tabSend: '送金', tabHistory: '履歴', tabSettings: '設定', + copyAddress: 'アドレスをコピー', paymentReceived: '入金を受け取りました!', tapToProceed: 'どこでもタップして続行', + recipient: '受取人', recipients: '受取人', max: '全額', feeRate: '手数料', + feeEconomy: '節約', feeNormal: '通常', feePriority: '優先', feeCustom: 'カスタム', + selectedRate: '選択中: {n} sat/vB', coinSelection: 'コイン選択', automatic: '自動', manual: '手動', + noCoins: '利用可能なコインがありません。', nSelected: '{n} 件選択', addRecipient: '+ 受取人を追加', + reviewTx: 'トランザクションを確認', remove: '削除', + enterRecipientAddr: '送金先アドレスを入力してください。', enterAddrForN: '受取人 {n} のアドレスを入力してください。', + enterValidAmtForN: '受取人 {n} の有効な金額を入力してください。', selectCoin: '使用するコインを1つ以上選択してください。', + networkFee: 'ネットワーク手数料', offlineSignNote: 'オフライン:今すぐ署名し、オンライン端末からトランザクションを送信してください。', + signBroadcast: '署名して送信', signTx: 'トランザクションに署名', broadcastFailed: '送信に失敗: {msg}', signingFailed: '署名に失敗: {msg}', + done: '完了', txSignedNote: '✓ 署名済み。下のコードをオンライン端末から送信してください(例: mempool.space → Broadcast)。', + transactionId: 'トランザクションID', signedTxRaw: '署名済みトランザクション(コード)', copyHex: 'コードをコピー', downloadLabel: 'ダウンロード', + showQrAirgap: 'QRを表示(オフライン転送用)', txBroadcast: '✓ トランザクションを送信しました!', copyTxid: 'トランザクションIDをコピー', viewOnMempool: 'mempool.spaceで見る ↗', + noTxYet: 'まだ取引はありません。', received: '受取', sent: '送金', pendingTag: '⏳ 保留中', awaitingConfirmation: '承認待ち', + feeShort: '手数料 {x}', historyOffline: 'オフラインでは履歴を表示できません。', + status: '状態', confirmed: '承認済み', confirmedTag: '承認済み', pendingInMempool: '保留中(mempool)', block: 'ブロック', date: '日時', + copyId: 'IDをコピー', backToHistory: '← 履歴に戻る', + recoveryPhrase: '復元フレーズ', recoveryWarn: '⚠ この単語を見た人は資金を盗めます。表示前に誰も見ていないことを確認してください。', + bip39Passphrase: 'パスフレーズ', copyPassphrase: 'パスフレーズをコピー', revealRecovery: '復元フレーズを表示', + offlineTransfer: 'オフライン転送', offlineTransferDesc: 'オンライン端末でコインをエクスポートし、オフライン端末(このウォレットとシード)でファイルをインポートすれば、インターネットなしで署名できます。', + exportSnapshot: '⤓ スナップショットを書出', importSnapshot: 'スナップショットを読込…', rescan: '再スキャン', + rescanDesc: '現在のアドレスは自動更新されます。古い(再利用した)アドレスへの入金が表示されない場合のみ再スキャンしてください。', + rescanWallet: 'ウォレットを再スキャン', scanning: 'スキャン中…', language: '言語', + offlineBanner: 'ネットワークに接続できません — オフライン動作中。設定からスナップショットを読み込んでコインを表示できます。', retry: '再試行', + copied: 'コピーしました', snapshotExported: 'スナップショットを書き出しました', importedNCoins: '{n} 件のコインを読み込みました', + unmatchedSuffix: ' · このウォレットにない {n} 件のアドレス', importFailed: '読み込みに失敗: {msg}', + copy: 'コピー', switchUnit: 'BTC / sats を切替', footerMadeBy: '作成者:', footerSourceOn: 'ソースは', +}); + +def('ko', { + appTitle: '비트코인 지갑', home: '홈', logout: '로그아웃', + createNew: '새로 만들기', importExisting: '가져오기', + genIntro: '새 12단어 시드 문구를 생성합니다. 적어 두세요. 이 지갑을 복구하는 유일한 방법입니다.', + generateSeed: '시드 문구 생성', writeDownWarn: '⚠ 이 12단어를 순서대로 종이에 적으세요. 이를 가진 사람은 누구나 코인을 쓸 수 있습니다.', + copyPhrase: '문구 복사', regenerate: '다시 생성', verifyBackup: '백업 확인', skipVerification: '확인 건너뛰기', + confirmBackupIntro: '요청된 단어를 입력해 백업을 확인하세요.', wordN: '{n}번째 단어', + reenterPassphrase: '암호문구 다시 입력', back: '← 뒤로', openWallet: '지갑 열기', + wordsMismatch: '단어가 문구와 일치하지 않습니다. 백업을 확인하세요.', + passphraseMismatch: '암호문구가 입력한 것과 일치하지 않습니다.', + seedPhrase: '시드 문구', seedPlaceholder: '단어를 공백으로 구분해 입력', + optionalPassphrase: '선택 암호문구', leaveBlank: '사용 안 하면 비워 두세요', show: '표시', hide: '숨기기', + invalidSeed: '잘못된 시드 문구 — 철자와 순서를 확인하세요.', + keysNote: '키는 브라우저를 벗어나지 않습니다. 완전 오프라인 작동. 이 페이지를 저장해 로컬에서 열 수 있습니다.', + balance: '잔액', pending: '대기 중', + tabReceive: '받기', tabSend: '보내기', tabHistory: '내역', tabSettings: '설정', + copyAddress: '주소 복사', paymentReceived: '결제가 도착했습니다!', tapToProceed: '아무 곳이나 눌러 계속', + recipient: '받는 사람', recipients: '받는 사람', max: '최대', feeRate: '수수료', + feeEconomy: '절약', feeNormal: '보통', feePriority: '우선', feeCustom: '직접 입력', + selectedRate: '선택한 수수료: {n} sat/vB', coinSelection: '코인 선택', automatic: '자동', manual: '수동', + noCoins: '사용 가능한 코인이 없습니다.', nSelected: '{n}개 선택', addRecipient: '+ 받는 사람 추가', + reviewTx: '거래 검토', remove: '삭제', + enterRecipientAddr: '받는 주소를 입력하세요.', enterAddrForN: '받는 사람 {n}의 주소를 입력하세요.', + enterValidAmtForN: '받는 사람 {n}의 올바른 금액을 입력하세요.', selectCoin: '사용할 코인을 하나 이상 선택하세요.', + networkFee: '네트워크 수수료', offlineSignNote: '오프라인: 지금 서명한 뒤 온라인 기기에서 거래를 전송하세요.', + signBroadcast: '서명 후 전송', signTx: '거래 서명', broadcastFailed: '전송 실패: {msg}', signingFailed: '서명 실패: {msg}', + done: '완료', txSignedNote: '✓ 거래가 서명되었습니다. 아래 코드를 온라인 기기에서 전송하세요(예: mempool.space → Broadcast).', + transactionId: '거래 ID', signedTxRaw: '서명된 거래(코드)', copyHex: '코드 복사', downloadLabel: '다운로드', + showQrAirgap: 'QR 표시(오프라인 전송용)', txBroadcast: '✓ 거래가 전송되었습니다!', copyTxid: '거래 ID 복사', viewOnMempool: 'mempool.space에서 보기 ↗', + noTxYet: '아직 거래가 없습니다.', received: '받음', sent: '보냄', pendingTag: '⏳ 대기 중', awaitingConfirmation: '확인 대기 중', + feeShort: '수수료 {x}', historyOffline: '오프라인에서는 내역을 볼 수 없습니다.', + status: '상태', confirmed: '확인됨', confirmedTag: '확인됨', pendingInMempool: '대기 중(멤풀)', block: '블록', date: '날짜', + copyId: 'ID 복사', backToHistory: '← 내역으로', + recoveryPhrase: '복구 문구', recoveryWarn: '⚠ 이 단어를 보는 사람은 자금을 훔칠 수 있습니다. 표시하기 전에 아무도 보지 않는지 확인하세요.', + bip39Passphrase: '암호문구', copyPassphrase: '암호문구 복사', revealRecovery: '복구 문구 표시', + offlineTransfer: '오프라인 전송', offlineTransferDesc: '온라인 기기에서 코인을 내보낸 뒤, 오프라인 기기(이 지갑과 시드)에서 파일을 가져오면 인터넷 없이 서명할 수 있습니다.', + exportSnapshot: '⤓ 스냅샷 내보내기', importSnapshot: '스냅샷 가져오기…', rescan: '다시 스캔', + rescanDesc: '현재 주소는 자동으로 갱신됩니다. 결제가 예전(재사용) 주소로 가서 보이지 않을 때만 다시 스캔하세요.', + rescanWallet: '지갑 다시 스캔', scanning: '스캔 중…', language: '언어', + offlineBanner: '네트워크에 연결할 수 없음 — 오프라인 작동 중. 설정에서 스냅샷을 가져와 코인을 불러오세요.', retry: '다시 시도', + copied: '복사됨', snapshotExported: '스냅샷을 내보냈습니다', importedNCoins: '{n}개 코인을 가져왔습니다', + unmatchedSuffix: ' · 이 지갑에 없는 주소 {n}개', importFailed: '가져오기 실패: {msg}', + copy: '복사', switchUnit: 'BTC / sats 전환', footerMadeBy: '만든 사람:', footerSourceOn: '소스 공개:', +}); + +def('ar', { + appTitle: 'محفظة بيتكوين', home: 'الرئيسية', logout: 'تسجيل الخروج', + createNew: 'إنشاء جديدة', importExisting: 'استيراد موجودة', + genIntro: 'أنشئ عبارة استرداد جديدة من 12 كلمة. اكتبها. فهي الطريقة الوحيدة لاستعادة هذه المحفظة.', + generateSeed: 'إنشاء العبارة', writeDownWarn: '⚠ اكتب هذه الكلمات الـ12 على ورق بالترتيب. أي شخص يملكها يستطيع إنفاق عملاتك.', + copyPhrase: 'نسخ العبارة', regenerate: 'إعادة الإنشاء', verifyBackup: 'تحقق من النسخة', skipVerification: 'تخطّي التحقق', + confirmBackupIntro: 'أكّد نسختك بإدخال الكلمات المطلوبة.', wordN: 'الكلمة رقم {n}', + reenterPassphrase: 'أعد إدخال كلمة المرور', back: '← رجوع', openWallet: 'فتح المحفظة', + wordsMismatch: 'هذه الكلمات لا تطابق عبارتك. تحقق من نسختك.', + passphraseMismatch: 'كلمة المرور لا تطابق التي أدخلتها.', + seedPhrase: 'عبارة الاسترداد', seedPlaceholder: 'أدخل كلماتك مفصولة بمسافات', + optionalPassphrase: 'كلمة مرور اختيارية', leaveBlank: 'اتركها فارغة إن لم تُستخدم', show: 'إظهار', hide: 'إخفاء', + invalidSeed: 'عبارة استرداد غير صالحة — تحقق من الإملاء والترتيب.', + keysNote: 'المفاتيح لا تغادر متصفحك أبداً. تعمل دون اتصال. احفظ هذه الصفحة وافتحها محلياً.', + balance: 'الرصيد', pending: 'قيد الانتظار', + tabReceive: 'استلام', tabSend: 'إرسال', tabHistory: 'السجل', tabSettings: 'الإعدادات', + copyAddress: 'نسخ العنوان', paymentReceived: 'تم استلام الدفعة!', tapToProceed: 'انقر في أي مكان للمتابعة', + recipient: 'المستلم', recipients: 'المستلمون', max: 'الأقصى', feeRate: 'الرسوم', + feeEconomy: 'اقتصادي', feeNormal: 'عادي', feePriority: 'أولوية', feeCustom: 'مخصص', + selectedRate: 'الرسوم المختارة: {n} sat/vB', coinSelection: 'اختيار العملات', automatic: 'تلقائي', manual: 'يدوي', + noCoins: 'لا توجد عملات متاحة.', nSelected: 'تم اختيار {n}', addRecipient: '+ إضافة مستلم', + reviewTx: 'مراجعة المعاملة', remove: 'إزالة', + enterRecipientAddr: 'أدخل عنوان المستلم.', enterAddrForN: 'أدخل عنواناً للمستلم {n}.', + enterValidAmtForN: 'أدخل مبلغاً صحيحاً للمستلم {n}.', selectCoin: 'اختر عملة واحدة على الأقل للإنفاق.', + networkFee: 'رسوم الشبكة', offlineSignNote: 'دون اتصال: وقّع الآن ثم أرسل المعاملة من جهاز متصل.', + signBroadcast: 'التوقيع والإرسال', signTx: 'توقيع المعاملة', broadcastFailed: 'فشل الإرسال: {msg}', signingFailed: 'فشل التوقيع: {msg}', + done: 'تم', txSignedNote: '✓ تم توقيع المعاملة. أرسل الكود أدناه من جهاز متصل (مثل mempool.space ← Broadcast).', + transactionId: 'معرّف المعاملة', signedTxRaw: 'المعاملة الموقّعة (الكود)', copyHex: 'نسخ الكود', downloadLabel: 'تنزيل', + showQrAirgap: 'إظهار رمز QR (للنقل دون اتصال)', txBroadcast: '✓ تم إرسال المعاملة!', copyTxid: 'نسخ معرّف المعاملة', viewOnMempool: 'عرض على mempool.space ↗', + noTxYet: 'لا توجد معاملات بعد.', received: 'مستلَم', sent: 'مُرسَل', pendingTag: '⏳ قيد الانتظار', awaitingConfirmation: 'بانتظار التأكيد', + feeShort: 'رسوم {x}', historyOffline: 'السجل غير متاح دون اتصال.', + status: 'الحالة', confirmed: 'مؤكدة', confirmedTag: 'مؤكدة', pendingInMempool: 'قيد الانتظار (في mempool)', block: 'الكتلة', date: 'التاريخ', + copyId: 'نسخ المعرّف', backToHistory: '← العودة إلى السجل', + recoveryPhrase: 'عبارة الاسترداد', recoveryWarn: '⚠ أي شخص يرى هذه الكلمات يمكنه سرقة أموالك. تأكد ألا أحد يراقب قبل إظهارها.', + bip39Passphrase: 'كلمة مرور', copyPassphrase: 'نسخ كلمة المرور', revealRecovery: 'إظهار عبارة الاسترداد', + offlineTransfer: 'النقل دون اتصال', offlineTransferDesc: 'صدّر عملاتك على جهاز متصل، ثم استورد الملف على جهاز غير متصل (يحوي هذه المحفظة والعبارة) للتوقيع دون إنترنت.', + exportSnapshot: '⤓ تصدير نسخة', importSnapshot: 'استيراد نسخة…', rescan: 'إعادة الفحص', + rescanDesc: 'يتحدّث عنوانك الحالي تلقائياً. أعد الفحص فقط إذا أُرسلت دفعة إلى عنوان قديم (مُعاد استخدامه) ولم تظهر.', + rescanWallet: 'إعادة فحص المحفظة', scanning: 'جارٍ الفحص…', language: 'اللغة', + offlineBanner: 'تعذّر الوصول إلى الشبكة — تعمل دون اتصال. استورد نسخة من الإعدادات لتحميل العملات.', retry: 'إعادة المحاولة', + copied: 'تم النسخ', snapshotExported: 'تم تصدير النسخة', importedNCoins: 'تم استيراد {n} عملة', + unmatchedSuffix: ' · {n} عنوان ليس في هذه المحفظة', importFailed: 'فشل الاستيراد: {msg}', + copy: 'نسخ', switchUnit: 'تبديل BTC / sats', footerMadeBy: 'صُنع بواسطة', footerSourceOn: 'الكود المصدري على', +}); + +def('ur', { + appTitle: 'بٹ کوائن والیٹ', home: 'ہوم', logout: 'لاگ آؤٹ', + createNew: 'نیا بنائیں', importExisting: 'موجودہ درآمد کریں', + genIntro: 'نیا 12 الفاظ کا سیڈ فقرہ بنائیں۔ اسے لکھ لیں۔ یہی اس والیٹ کو بحال کرنے کا واحد طریقہ ہے۔', + generateSeed: 'سیڈ فقرہ بنائیں', writeDownWarn: '⚠ ان 12 الفاظ کو ترتیب سے کاغذ پر لکھیں۔ جس کے پاس یہ ہوں وہ آپ کے سکے خرچ کر سکتا ہے۔', + copyPhrase: 'فقرہ کاپی کریں', regenerate: 'دوبارہ بنائیں', verifyBackup: 'بیک اپ کی تصدیق', skipVerification: 'تصدیق چھوڑیں', + confirmBackupIntro: 'مطلوبہ الفاظ درج کر کے اپنے بیک اپ کی تصدیق کریں۔', wordN: 'لفظ #{n}', + reenterPassphrase: 'پاس فریز دوبارہ درج کریں', back: '← واپس', openWallet: 'والیٹ کھولیں', + wordsMismatch: 'یہ الفاظ آپ کے فقرے سے میل نہیں کھاتے۔ اپنا بیک اپ دیکھیں۔', + passphraseMismatch: 'یہ پاس فریز آپ کے درج کردہ سے میل نہیں کھاتا۔', + seedPhrase: 'سیڈ فقرہ', seedPlaceholder: 'اپنے الفاظ خالی جگہ سے الگ کر کے لکھیں', + optionalPassphrase: 'اختیاری پاس فریز', leaveBlank: 'استعمال نہ ہو تو خالی چھوڑیں', show: 'دکھائیں', hide: 'چھپائیں', + invalidSeed: 'غلط سیڈ فقرہ — ہجے اور ترتیب دیکھیں۔', + keysNote: 'چابیاں کبھی آپ کے براؤزر سے باہر نہیں جاتیں۔ مکمل آف لائن کام کرتا ہے۔ یہ صفحہ محفوظ کر کے مقامی طور پر کھولیں۔', + balance: 'بیلنس', pending: 'زیرِ التوا', + tabReceive: 'وصول', tabSend: 'بھیجیں', tabHistory: 'تاریخ', tabSettings: 'ترتیبات', + copyAddress: 'پتہ کاپی کریں', paymentReceived: 'ادائیگی موصول ہوئی!', tapToProceed: 'جاری رکھنے کے لیے کہیں بھی ٹیپ کریں', + recipient: 'وصول کنندہ', recipients: 'وصول کنندگان', max: 'زیادہ سے زیادہ', feeRate: 'فیس', + feeEconomy: 'اکنامی', feeNormal: 'عام', feePriority: 'ترجیحی', feeCustom: 'حسبِ ضرورت', + selectedRate: 'منتخب فیس: {n} sat/vB', coinSelection: 'سکوں کا انتخاب', automatic: 'خودکار', manual: 'دستی', + noCoins: 'کوئی سکے دستیاب نہیں۔', nSelected: '{n} منتخب', addRecipient: '+ وصول کنندہ شامل کریں', + reviewTx: 'لین دین کا جائزہ', remove: 'ہٹائیں', + enterRecipientAddr: 'وصول کنندہ کا پتہ درج کریں۔', enterAddrForN: 'وصول کنندہ {n} کا پتہ درج کریں۔', + enterValidAmtForN: 'وصول کنندہ {n} کے لیے درست رقم درج کریں۔', selectCoin: 'خرچ کے لیے کم از کم ایک سکہ منتخب کریں۔', + networkFee: 'نیٹ ورک فیس', offlineSignNote: 'آف لائن: ابھی دستخط کریں، پھر کسی آن لائن آلے سے لین دین نشر کریں۔', + signBroadcast: 'دستخط اور نشر', signTx: 'لین دین پر دستخط', broadcastFailed: 'نشر ناکام: {msg}', signingFailed: 'دستخط ناکام: {msg}', + done: 'مکمل', txSignedNote: '✓ لین دین پر دستخط ہو گئے۔ نیچے دیا کوڈ کسی آن لائن آلے سے نشر کریں (مثلاً mempool.space ← Broadcast)۔', + transactionId: 'لین دین ID', signedTxRaw: 'دستخط شدہ لین دین (کوڈ)', copyHex: 'کوڈ کاپی کریں', downloadLabel: 'ڈاؤن لوڈ', + showQrAirgap: 'QR دکھائیں (آف لائن منتقلی کے لیے)', txBroadcast: '✓ لین دین نشر ہو گیا!', copyTxid: 'لین دین ID کاپی کریں', viewOnMempool: 'mempool.space پر دیکھیں ↗', + noTxYet: 'ابھی کوئی لین دین نہیں۔', received: 'موصول', sent: 'بھیجا گیا', pendingTag: '⏳ زیرِ التوا', awaitingConfirmation: 'تصدیق کے منتظر', + feeShort: 'فیس {x}', historyOffline: 'آف لائن میں تاریخ دستیاب نہیں۔', + status: 'حیثیت', confirmed: 'تصدیق شدہ', confirmedTag: 'تصدیق شدہ', pendingInMempool: 'زیرِ التوا (mempool میں)', block: 'بلاک', date: 'تاریخ', + copyId: 'ID کاپی کریں', backToHistory: '← تاریخ پر واپس', + recoveryPhrase: 'بحالی فقرہ', recoveryWarn: '⚠ جو بھی یہ الفاظ دیکھے آپ کی رقم چرا سکتا ہے۔ ظاہر کرنے سے پہلے یقینی بنائیں کوئی نہیں دیکھ رہا۔', + bip39Passphrase: 'پاس فریز', copyPassphrase: 'پاس فریز کاپی کریں', revealRecovery: 'بحالی فقرہ ظاہر کریں', + offlineTransfer: 'آف لائن منتقلی', offlineTransferDesc: 'کسی آن لائن آلے پر اپنے سکے برآمد کریں، پھر آف لائن آلے (اسی والیٹ اور سیڈ کے ساتھ) پر فائل درآمد کر کے بغیر انٹرنیٹ دستخط کریں۔', + exportSnapshot: '⤓ سنیپ شاٹ برآمد', importSnapshot: 'سنیپ شاٹ درآمد…', rescan: 'دوبارہ اسکین', + rescanDesc: 'آپ کا موجودہ پتہ خودکار اپ ڈیٹ ہوتا ہے۔ صرف تب دوبارہ اسکین کریں جب ادائیگی پرانے (دوبارہ استعمال شدہ) پتے پر آئی ہو اور نظر نہ آ رہی ہو۔', + rescanWallet: 'والیٹ دوبارہ اسکین', scanning: 'اسکین جاری…', language: 'زبان', + offlineBanner: 'نیٹ ورک تک رسائی نہیں — آف لائن کام جاری۔ سکے لوڈ کرنے کے لیے ترتیبات سے سنیپ شاٹ درآمد کریں۔', retry: 'دوبارہ کوشش', + copied: 'کاپی ہو گیا', snapshotExported: 'سنیپ شاٹ برآمد ہو گیا', importedNCoins: '{n} سکے درآمد ہوئے', + unmatchedSuffix: ' · {n} پتے اس والیٹ میں نہیں', importFailed: 'درآمد ناکام: {msg}', + copy: 'کاپی', switchUnit: 'BTC / sats بدلیں', footerMadeBy: 'بنایا از', footerSourceOn: 'سورس کوڈ یہاں', +}); + +def('hi', { + appTitle: 'बिटकॉइन वॉलेट', home: 'होम', logout: 'लॉग आउट', + createNew: 'नया बनाएँ', importExisting: 'मौजूदा आयात करें', + genIntro: 'नया 12-शब्द वाला सीड वाक्यांश बनाएँ। इसे लिख लें। यही इस वॉलेट को पुनः प्राप्त करने का एकमात्र तरीका है।', + generateSeed: 'सीड वाक्यांश बनाएँ', writeDownWarn: '⚠ इन 12 शब्दों को क्रम से कागज़ पर लिखें। जिसके पास ये होंगे वह आपके सिक्के खर्च कर सकता है।', + copyPhrase: 'वाक्यांश कॉपी करें', regenerate: 'फिर बनाएँ', verifyBackup: 'बैकअप जाँचें', skipVerification: 'सत्यापन छोड़ें', + confirmBackupIntro: 'माँगे गए शब्द दर्ज करके अपना बैकअप पुष्टि करें।', wordN: 'शब्द #{n}', + reenterPassphrase: 'पासफ़्रेज़ फिर दर्ज करें', back: '← वापस', openWallet: 'वॉलेट खोलें', + wordsMismatch: 'ये शब्द आपके वाक्यांश से मेल नहीं खाते। अपना बैकअप जाँचें।', + passphraseMismatch: 'यह पासफ़्रेज़ आपके दर्ज किए गए से मेल नहीं खाता।', + seedPhrase: 'सीड वाक्यांश', seedPlaceholder: 'अपने शब्द स्पेस से अलग करके लिखें', + optionalPassphrase: 'वैकल्पिक पासफ़्रेज़', leaveBlank: 'उपयोग न हो तो खाली छोड़ें', show: 'दिखाएँ', hide: 'छिपाएँ', + invalidSeed: 'अमान्य सीड वाक्यांश — वर्तनी और क्रम जाँचें।', + keysNote: 'कुंजियाँ कभी आपके ब्राउज़र से बाहर नहीं जातीं। पूरी तरह ऑफ़लाइन काम करता है। इस पेज को सहेजें और स्थानीय रूप से खोलें।', + balance: 'बैलेंस', pending: 'लंबित', + tabReceive: 'प्राप्त', tabSend: 'भेजें', tabHistory: 'इतिहास', tabSettings: 'सेटिंग्स', + copyAddress: 'पता कॉपी करें', paymentReceived: 'भुगतान प्राप्त हुआ!', tapToProceed: 'आगे बढ़ने के लिए कहीं भी टैप करें', + recipient: 'प्राप्तकर्ता', recipients: 'प्राप्तकर्ता', max: 'अधिकतम', feeRate: 'शुल्क', + feeEconomy: 'किफ़ायती', feeNormal: 'सामान्य', feePriority: 'प्राथमिकता', feeCustom: 'कस्टम', + selectedRate: 'चयनित शुल्क: {n} sat/vB', coinSelection: 'सिक्का चयन', automatic: 'स्वचालित', manual: 'मैनुअल', + noCoins: 'कोई सिक्के उपलब्ध नहीं।', nSelected: '{n} चयनित', addRecipient: '+ प्राप्तकर्ता जोड़ें', + reviewTx: 'लेन-देन की समीक्षा', remove: 'हटाएँ', + enterRecipientAddr: 'प्राप्तकर्ता का पता दर्ज करें।', enterAddrForN: 'प्राप्तकर्ता {n} का पता दर्ज करें।', + enterValidAmtForN: 'प्राप्तकर्ता {n} के लिए मान्य राशि दर्ज करें।', selectCoin: 'खर्च के लिए कम से कम एक सिक्का चुनें।', + networkFee: 'नेटवर्क शुल्क', offlineSignNote: 'ऑफ़लाइन: अभी हस्ताक्षर करें, फिर किसी ऑनलाइन डिवाइस से लेन-देन प्रसारित करें।', + signBroadcast: 'हस्ताक्षर और प्रसारण', signTx: 'लेन-देन पर हस्ताक्षर', broadcastFailed: 'प्रसारण विफल: {msg}', signingFailed: 'हस्ताक्षर विफल: {msg}', + done: 'हो गया', txSignedNote: '✓ लेन-देन पर हस्ताक्षर हो गए। नीचे का कोड किसी ऑनलाइन डिवाइस से प्रसारित करें (जैसे mempool.space → Broadcast)।', + transactionId: 'लेन-देन ID', signedTxRaw: 'हस्ताक्षरित लेन-देन (कोड)', copyHex: 'कोड कॉपी करें', downloadLabel: 'डाउनलोड', + showQrAirgap: 'QR दिखाएँ (ऑफ़लाइन स्थानांतरण के लिए)', txBroadcast: '✓ लेन-देन प्रसारित हुआ!', copyTxid: 'लेन-देन ID कॉपी करें', viewOnMempool: 'mempool.space पर देखें ↗', + noTxYet: 'अभी कोई लेन-देन नहीं।', received: 'प्राप्त', sent: 'भेजा गया', pendingTag: '⏳ लंबित', awaitingConfirmation: 'पुष्टि की प्रतीक्षा', + feeShort: 'शुल्क {x}', historyOffline: 'ऑफ़लाइन में इतिहास उपलब्ध नहीं।', + status: 'स्थिति', confirmed: 'पुष्ट', confirmedTag: 'पुष्ट', pendingInMempool: 'लंबित (mempool में)', block: 'ब्लॉक', date: 'तारीख़', + copyId: 'ID कॉपी करें', backToHistory: '← इतिहास पर वापस', + recoveryPhrase: 'रिकवरी वाक्यांश', recoveryWarn: '⚠ जो भी ये शब्द देखेगा वह आपके फंड चुरा सकता है। दिखाने से पहले सुनिश्चित करें कोई नहीं देख रहा।', + bip39Passphrase: 'पासफ़्रेज़', copyPassphrase: 'पासफ़्रेज़ कॉपी करें', revealRecovery: 'रिकवरी वाक्यांश दिखाएँ', + offlineTransfer: 'ऑफ़लाइन स्थानांतरण', offlineTransferDesc: 'किसी ऑनलाइन डिवाइस पर अपने सिक्के निर्यात करें, फिर ऑफ़लाइन डिवाइस (इसी वॉलेट और सीड के साथ) पर फ़ाइल आयात करके बिना इंटरनेट हस्ताक्षर करें।', + exportSnapshot: '⤓ स्नैपशॉट निर्यात', importSnapshot: 'स्नैपशॉट आयात…', rescan: 'फिर स्कैन करें', + rescanDesc: 'आपका वर्तमान पता स्वतः अपडेट होता है। केवल तभी फिर स्कैन करें जब भुगतान किसी पुराने (पुनः उपयोग) पते पर आया हो और दिख न रहा हो।', + rescanWallet: 'वॉलेट फिर स्कैन करें', scanning: 'स्कैन हो रहा है…', language: 'भाषा', + offlineBanner: 'नेटवर्क उपलब्ध नहीं — ऑफ़लाइन काम जारी। सिक्के लोड करने के लिए सेटिंग्स से स्नैपशॉट आयात करें।', retry: 'पुनः प्रयास', + copied: 'कॉपी हो गया', snapshotExported: 'स्नैपशॉट निर्यात हुआ', importedNCoins: '{n} सिक्के आयात हुए', + unmatchedSuffix: ' · {n} पते इस वॉलेट में नहीं', importFailed: 'आयात विफल: {msg}', + copy: 'कॉपी', switchUnit: 'BTC / sats बदलें', footerMadeBy: 'द्वारा बनाया', footerSourceOn: 'सोर्स यहाँ', +}); + +def('bn', { + appTitle: 'বিটকয়েন ওয়ালেট', home: 'হোম', logout: 'লগ আউট', + createNew: 'নতুন তৈরি', importExisting: 'বিদ্যমান ইম্পোর্ট', + genIntro: 'নতুন ১২-শব্দের সিড ফ্রেজ তৈরি করুন। লিখে রাখুন। এটিই এই ওয়ালেট পুনরুদ্ধারের একমাত্র উপায়।', + generateSeed: 'সিড ফ্রেজ তৈরি করুন', writeDownWarn: '⚠ এই ১২টি শব্দ ক্রম অনুযায়ী কাগজে লিখুন। যার কাছে এগুলো থাকবে সে আপনার কয়েন খরচ করতে পারবে।', + copyPhrase: 'ফ্রেজ কপি করুন', regenerate: 'আবার তৈরি', verifyBackup: 'ব্যাকআপ যাচাই', skipVerification: 'যাচাই এড়িয়ে যান', + confirmBackupIntro: 'অনুরোধ করা শব্দগুলো লিখে আপনার ব্যাকআপ নিশ্চিত করুন।', wordN: 'শব্দ #{n}', + reenterPassphrase: 'পাসফ্রেজ আবার লিখুন', back: '← ফিরুন', openWallet: 'ওয়ালেট খুলুন', + wordsMismatch: 'এই শব্দগুলো আপনার ফ্রেজের সাথে মেলে না। ব্যাকআপ দেখুন।', + passphraseMismatch: 'এই পাসফ্রেজ আপনার দেওয়াটির সাথে মেলে না।', + seedPhrase: 'সিড ফ্রেজ', seedPlaceholder: 'শব্দগুলো স্পেস দিয়ে আলাদা করে লিখুন', + optionalPassphrase: 'ঐচ্ছিক পাসফ্রেজ', leaveBlank: 'ব্যবহার না হলে ফাঁকা রাখুন', show: 'দেখান', hide: 'লুকান', + invalidSeed: 'ভুল সিড ফ্রেজ — বানান ও ক্রম দেখুন।', + keysNote: 'কী কখনো আপনার ব্রাউজার ছাড়ে না। সম্পূর্ণ অফলাইনে চলে। এই পেজ সেভ করে স্থানীয়ভাবে খুলুন।', + balance: 'ব্যালেন্স', pending: 'অপেক্ষমাণ', + tabReceive: 'গ্রহণ', tabSend: 'পাঠান', tabHistory: 'ইতিহাস', tabSettings: 'সেটিংস', + copyAddress: 'ঠিকানা কপি', paymentReceived: 'পেমেন্ট পাওয়া গেছে!', tapToProceed: 'এগিয়ে যেতে যেকোনো জায়গায় ট্যাপ করুন', + recipient: 'প্রাপক', recipients: 'প্রাপকগণ', max: 'সর্বোচ্চ', feeRate: 'ফি', + feeEconomy: 'সাশ্রয়ী', feeNormal: 'সাধারণ', feePriority: 'অগ্রাধিকার', feeCustom: 'কাস্টম', + selectedRate: 'নির্বাচিত ফি: {n} sat/vB', coinSelection: 'কয়েন নির্বাচন', automatic: 'স্বয়ংক্রিয়', manual: 'ম্যানুয়াল', + noCoins: 'কোনো কয়েন নেই।', nSelected: '{n} নির্বাচিত', addRecipient: '+ প্রাপক যোগ করুন', + reviewTx: 'লেনদেন পর্যালোচনা', remove: 'সরান', + enterRecipientAddr: 'প্রাপকের ঠিকানা লিখুন।', enterAddrForN: 'প্রাপক {n}-এর ঠিকানা লিখুন।', + enterValidAmtForN: 'প্রাপক {n}-এর জন্য সঠিক পরিমাণ লিখুন।', selectCoin: 'খরচের জন্য অন্তত একটি কয়েন নির্বাচন করুন।', + networkFee: 'নেটওয়ার্ক ফি', offlineSignNote: 'অফলাইন: এখন সই করুন, তারপর অনলাইন ডিভাইস থেকে লেনদেন সম্প্রচার করুন।', + signBroadcast: 'সই ও সম্প্রচার', signTx: 'লেনদেন সই', broadcastFailed: 'সম্প্রচার ব্যর্থ: {msg}', signingFailed: 'সই ব্যর্থ: {msg}', + done: 'সম্পন্ন', txSignedNote: '✓ লেনদেন সই হয়েছে। নিচের কোড অনলাইন ডিভাইস থেকে সম্প্রচার করুন (যেমন mempool.space → Broadcast)।', + transactionId: 'লেনদেন ID', signedTxRaw: 'সইকৃত লেনদেন (কোড)', copyHex: 'কোড কপি', downloadLabel: 'ডাউনলোড', + showQrAirgap: 'QR দেখান (অফলাইন স্থানান্তরের জন্য)', txBroadcast: '✓ লেনদেন সম্প্রচারিত!', copyTxid: 'লেনদেন ID কপি', viewOnMempool: 'mempool.space-এ দেখুন ↗', + noTxYet: 'এখনো কোনো লেনদেন নেই।', received: 'গৃহীত', sent: 'প্রেরিত', pendingTag: '⏳ অপেক্ষমাণ', awaitingConfirmation: 'নিশ্চিতকরণের অপেক্ষায়', + feeShort: 'ফি {x}', historyOffline: 'অফলাইনে ইতিহাস পাওয়া যায় না।', + status: 'অবস্থা', confirmed: 'নিশ্চিত', confirmedTag: 'নিশ্চিত', pendingInMempool: 'অপেক্ষমাণ (mempool-এ)', block: 'ব্লক', date: 'তারিখ', + copyId: 'ID কপি', backToHistory: '← ইতিহাসে ফিরুন', + recoveryPhrase: 'রিকভারি ফ্রেজ', recoveryWarn: '⚠ যে কেউ এই শব্দগুলো দেখলে আপনার অর্থ চুরি করতে পারে। দেখানোর আগে নিশ্চিত হোন কেউ দেখছে না।', + bip39Passphrase: 'পাসফ্রেজ', copyPassphrase: 'পাসফ্রেজ কপি', revealRecovery: 'রিকভারি ফ্রেজ দেখান', + offlineTransfer: 'অফলাইন স্থানান্তর', offlineTransferDesc: 'অনলাইন ডিভাইসে আপনার কয়েন এক্সপোর্ট করুন, তারপর অফলাইন ডিভাইসে (এই ওয়ালেট ও সিড সহ) ফাইল ইম্পোর্ট করে ইন্টারনেট ছাড়াই সই করুন।', + exportSnapshot: '⤓ স্ন্যাপশট এক্সপোর্ট', importSnapshot: 'স্ন্যাপশট ইম্পোর্ট…', rescan: 'পুনরায় স্ক্যান', + rescanDesc: 'আপনার বর্তমান ঠিকানা স্বয়ংক্রিয়ভাবে আপডেট হয়। শুধু তখনই পুনরায় স্ক্যান করুন যখন পেমেন্ট কোনো পুরোনো (পুনঃব্যবহৃত) ঠিকানায় এসেছে এবং দেখা যাচ্ছে না।', + rescanWallet: 'ওয়ালেট পুনরায় স্ক্যান', scanning: 'স্ক্যান হচ্ছে…', language: 'ভাষা', + offlineBanner: 'নেটওয়ার্কে পৌঁছানো যাচ্ছে না — অফলাইনে চলছে। কয়েন লোড করতে সেটিংস থেকে স্ন্যাপশট ইম্পোর্ট করুন।', retry: 'আবার চেষ্টা', + copied: 'কপি হয়েছে', snapshotExported: 'স্ন্যাপশট এক্সপোর্ট হয়েছে', importedNCoins: '{n}টি কয়েন ইম্পোর্ট হয়েছে', + unmatchedSuffix: ' · {n}টি ঠিকানা এই ওয়ালেটে নেই', importFailed: 'ইম্পোর্ট ব্যর্থ: {msg}', + copy: 'কপি', switchUnit: 'BTC / sats পাল্টান', footerMadeBy: 'তৈরি করেছেন', footerSourceOn: 'সোর্স কোড', +}); + +def('id', { + appTitle: 'Dompet Bitcoin', home: 'Beranda', logout: 'Keluar', + createNew: 'Buat baru', importExisting: 'Impor yang ada', + genIntro: 'Buat frasa benih baru 12 kata. Catatlah. Ini satu-satunya cara memulihkan dompet ini.', + generateSeed: 'Buat frasa benih', writeDownWarn: '⚠ Tulis 12 kata ini di kertas, secara berurutan. Siapa pun yang memilikinya dapat menghabiskan koin Anda.', + copyPhrase: 'Salin frasa', regenerate: 'Buat ulang', verifyBackup: 'Verifikasi cadangan', skipVerification: 'Lewati verifikasi', + confirmBackupIntro: 'Konfirmasi cadangan dengan memasukkan kata yang diminta.', wordN: 'Kata #{n}', + reenterPassphrase: 'Masukkan ulang frasa sandi', back: '← Kembali', openWallet: 'Buka dompet', + wordsMismatch: 'Kata-kata itu tidak cocok dengan frasa Anda. Periksa cadangan Anda.', + passphraseMismatch: 'Frasa sandi tidak cocok dengan yang Anda masukkan.', + seedPhrase: 'Frasa benih', seedPlaceholder: 'masukkan kata-kata dipisahkan spasi', + optionalPassphrase: 'Frasa sandi opsional', leaveBlank: 'kosongkan jika tidak dipakai', show: 'Tampilkan', hide: 'Sembunyikan', + invalidSeed: 'Frasa benih tidak valid — periksa ejaan dan urutan kata.', + keysNote: 'Kunci tidak pernah meninggalkan browser Anda. Berfungsi offline. Simpan halaman ini dan buka secara lokal.', + balance: 'Saldo', pending: 'Tertunda', + tabReceive: 'Terima', tabSend: 'Kirim', tabHistory: 'Riwayat', tabSettings: 'Pengaturan', + copyAddress: 'Salin alamat', paymentReceived: 'Pembayaran diterima!', tapToProceed: 'Ketuk di mana saja untuk lanjut', + recipient: 'Penerima', recipients: 'Penerima', max: 'Maks', feeRate: 'Biaya', + feeEconomy: 'Hemat', feeNormal: 'Normal', feePriority: 'Prioritas', feeCustom: 'Kustom', + selectedRate: 'Biaya dipilih: {n} sat/vB', coinSelection: 'Pemilihan koin', automatic: 'Otomatis', manual: 'Manual', + noCoins: 'Tidak ada koin tersedia.', nSelected: '{n} dipilih', addRecipient: '+ Tambah penerima', + reviewTx: 'Tinjau transaksi', remove: 'Hapus', + enterRecipientAddr: 'Masukkan alamat penerima.', enterAddrForN: 'Masukkan alamat untuk penerima {n}.', + enterValidAmtForN: 'Masukkan jumlah yang valid untuk penerima {n}.', selectCoin: 'Pilih setidaknya satu koin untuk dibelanjakan.', + networkFee: 'Biaya jaringan', offlineSignNote: 'Offline: tanda tangani sekarang, lalu siarkan transaksi dari perangkat online.', + signBroadcast: 'Tandatangani & siarkan', signTx: 'Tandatangani transaksi', broadcastFailed: 'Siaran gagal: {msg}', signingFailed: 'Penandatanganan gagal: {msg}', + done: 'Selesai', txSignedNote: '✓ Transaksi ditandatangani. Siarkan kode di bawah dari perangkat online (mis. mempool.space → Broadcast).', + transactionId: 'ID transaksi', signedTxRaw: 'Transaksi tertandatangani (kode)', copyHex: 'Salin kode', downloadLabel: 'Unduh', + showQrAirgap: 'Tampilkan QR (transfer offline)', txBroadcast: '✓ Transaksi disiarkan!', copyTxid: 'Salin ID transaksi', viewOnMempool: 'Lihat di mempool.space ↗', + noTxYet: 'Belum ada transaksi.', received: 'Diterima', sent: 'Terkirim', pendingTag: '⏳ tertunda', awaitingConfirmation: 'menunggu konfirmasi', + feeShort: 'biaya {x}', historyOffline: 'Riwayat tidak tersedia saat offline.', + status: 'Status', confirmed: 'Terkonfirmasi', confirmedTag: 'terkonfirmasi', pendingInMempool: 'Tertunda (di mempool)', block: 'Blok', date: 'Tanggal', + copyId: 'Salin ID', backToHistory: '← Kembali ke riwayat', + recoveryPhrase: 'Frasa pemulihan', recoveryWarn: '⚠ Siapa pun yang melihat kata-kata ini bisa mencuri dana Anda. Pastikan tidak ada yang melihat sebelum menampilkannya.', + bip39Passphrase: 'Frasa sandi', copyPassphrase: 'Salin frasa sandi', revealRecovery: 'Tampilkan frasa pemulihan', + offlineTransfer: 'Transfer offline', offlineTransferDesc: 'Ekspor koin Anda di perangkat online, lalu impor file di perangkat offline (dengan dompet + benih ini) untuk menandatangani tanpa internet.', + exportSnapshot: '⤓ Ekspor snapshot', importSnapshot: 'Impor snapshot…', rescan: 'Pindai ulang', + rescanDesc: 'Alamat Anda saat ini diperbarui otomatis. Pindai ulang hanya jika pembayaran dikirim ke alamat lama (dipakai ulang) dan tidak muncul.', + rescanWallet: 'Pindai ulang dompet', scanning: 'Memindai…', language: 'Bahasa', + offlineBanner: 'Tidak dapat menjangkau jaringan — bekerja offline. Impor snapshot di Pengaturan untuk memuat koin.', retry: 'Coba lagi', + copied: 'Tersalin', snapshotExported: 'Snapshot diekspor', importedNCoins: '{n} koin diimpor', + unmatchedSuffix: ' · {n} alamat bukan milik dompet ini', importFailed: 'Impor gagal: {msg}', + copy: 'Salin', switchUnit: 'Ganti BTC / sats', footerMadeBy: 'dibuat oleh', footerSourceOn: 'sumber tersedia di', +}); + +def('tr', { + appTitle: 'Bitcoin Cüzdanı', home: 'Ana sayfa', logout: 'Çıkış', + createNew: 'Yeni oluştur', importExisting: 'Mevcudu içe aktar', + genIntro: 'Yeni 12 kelimelik tohum ifadesi oluşturun. Yazın. Bu cüzdanı kurtarmanın tek yolu budur.', + generateSeed: 'Tohum ifadesi oluştur', writeDownWarn: '⚠ Bu 12 kelimeyi sırayla kâğıda yazın. Bunlara sahip olan herkes paranızı harcayabilir.', + copyPhrase: 'İfadeyi kopyala', regenerate: 'Yeniden oluştur', verifyBackup: 'Yedeği doğrula', skipVerification: 'Doğrulamayı atla', + confirmBackupIntro: 'İstenen kelimeleri girerek yedeğinizi onaylayın.', wordN: '{n}. kelime', + reenterPassphrase: 'Parolayı tekrar girin', back: '← Geri', openWallet: 'Cüzdanı aç', + wordsMismatch: 'Bu kelimeler ifadenizle eşleşmiyor. Yedeğinizi kontrol edin.', + passphraseMismatch: 'Bu parola girdiğinizle eşleşmiyor.', + seedPhrase: 'Tohum ifadesi', seedPlaceholder: 'kelimelerinizi boşlukla ayırarak girin', + optionalPassphrase: 'İsteğe bağlı parola', leaveBlank: 'kullanılmıyorsa boş bırakın', show: 'Göster', hide: 'Gizle', + invalidSeed: 'Geçersiz tohum ifadesi — yazımı ve sırayı kontrol edin.', + keysNote: 'Anahtarlar tarayıcınızdan asla çıkmaz. Tamamen çevrimdışı çalışır. Bu sayfayı kaydedip yerel olarak açın.', + balance: 'Bakiye', pending: 'Beklemede', + tabReceive: 'Al', tabSend: 'Gönder', tabHistory: 'Geçmiş', tabSettings: 'Ayarlar', + copyAddress: 'Adresi kopyala', paymentReceived: 'Ödeme alındı!', tapToProceed: 'Devam etmek için herhangi bir yere dokunun', + recipient: 'Alıcı', recipients: 'Alıcılar', max: 'Maks', feeRate: 'Ücret', + feeEconomy: 'Ekonomik', feeNormal: 'Normal', feePriority: 'Öncelikli', feeCustom: 'Özel', + selectedRate: 'Seçilen ücret: {n} sat/vB', coinSelection: 'Coin seçimi', automatic: 'Otomatik', manual: 'Manuel', + noCoins: 'Kullanılabilir coin yok.', nSelected: '{n} seçili', addRecipient: '+ Alıcı ekle', + reviewTx: 'İşlemi gözden geçir', remove: 'Kaldır', + enterRecipientAddr: 'Bir alıcı adresi girin.', enterAddrForN: 'Alıcı {n} için bir adres girin.', + enterValidAmtForN: 'Alıcı {n} için geçerli bir tutar girin.', selectCoin: 'Harcamak için en az bir coin seçin.', + networkFee: 'Ağ ücreti', offlineSignNote: 'Çevrimdışı: şimdi imzalayın, sonra işlemi çevrimiçi bir cihazdan yayınlayın.', + signBroadcast: 'İmzala ve yayınla', signTx: 'İşlemi imzala', broadcastFailed: 'Yayın başarısız: {msg}', signingFailed: 'İmzalama başarısız: {msg}', + done: 'Tamam', txSignedNote: '✓ İşlem imzalandı. Aşağıdaki kodu çevrimiçi bir cihazdan yayınlayın (örn. mempool.space → Broadcast).', + transactionId: 'İşlem kimliği', signedTxRaw: 'İmzalı işlem (kod)', copyHex: 'kod kopyala', downloadLabel: 'İndir', + showQrAirgap: 'QR göster (çevrimdışı aktarım için)', txBroadcast: '✓ İşlem yayınlandı!', copyTxid: 'İşlem kimliği kopyala', viewOnMempool: 'mempool.space’te gör ↗', + noTxYet: 'Henüz işlem yok.', received: 'Alındı', sent: 'Gönderildi', pendingTag: '⏳ beklemede', awaitingConfirmation: 'onay bekleniyor', + feeShort: 'ücret {x}', historyOffline: 'Çevrimdışıyken geçmiş kullanılamaz.', + status: 'Durum', confirmed: 'Onaylandı', confirmedTag: 'onaylandı', pendingInMempool: 'Beklemede (mempool’da)', block: 'Blok', date: 'Tarih', + copyId: 'Kimliği kopyala', backToHistory: '← Geçmişe dön', + recoveryPhrase: 'Kurtarma ifadesi', recoveryWarn: '⚠ Bu kelimeleri gören herkes paranızı çalabilir. Göstermeden önce kimsenin bakmadığından emin olun.', + bip39Passphrase: 'Parola', copyPassphrase: 'Parolayı kopyala', revealRecovery: 'Kurtarma ifadesini göster', + offlineTransfer: 'Çevrimdışı aktarım', offlineTransferDesc: 'Coinlerinizi çevrimiçi bir cihazda dışa aktarın, sonra çevrimdışı cihazda (bu cüzdan + tohum ile) dosyayı içe aktarıp internetsiz imzalayın.', + exportSnapshot: '⤓ Anlık görüntü dışa aktar', importSnapshot: 'Anlık görüntü içe aktar…', rescan: 'Yeniden tara', + rescanDesc: 'Mevcut adresiniz otomatik güncellenir. Yalnızca bir ödeme eski (yeniden kullanılmış) bir adrese geldiyse ve görünmüyorsa yeniden tarayın.', + rescanWallet: 'Cüzdanı yeniden tara', scanning: 'Taranıyor…', language: 'Dil', + offlineBanner: 'Ağa ulaşılamıyor — çevrimdışı çalışılıyor. Coinleri yüklemek için Ayarlar’dan anlık görüntü içe aktarın.', retry: 'Yeniden dene', + copied: 'Kopyalandı', snapshotExported: 'Anlık görüntü dışa aktarıldı', importedNCoins: '{n} coin içe aktarıldı', + unmatchedSuffix: ' · {n} adres bu cüzdanda değil', importFailed: 'İçe aktarma başarısız: {msg}', + copy: 'Kopyala', switchUnit: 'BTC / sats değiştir', footerMadeBy: 'yapan:', footerSourceOn: 'kaynak kodu', +}); + +def('vi', { + appTitle: 'Ví Bitcoin', home: 'Trang chủ', logout: 'Đăng xuất', + createNew: 'Tạo mới', importExisting: 'Nhập ví có sẵn', + genIntro: 'Tạo cụm từ hạt giống 12 từ mới. Hãy ghi lại. Đây là cách duy nhất để khôi phục ví này.', + generateSeed: 'Tạo cụm từ hạt giống', writeDownWarn: '⚠ Ghi 12 từ này ra giấy, theo đúng thứ tự. Bất kỳ ai có chúng đều có thể tiêu coin của bạn.', + copyPhrase: 'Sao chép cụm từ', regenerate: 'Tạo lại', verifyBackup: 'Xác minh sao lưu', skipVerification: 'Bỏ qua xác minh', + confirmBackupIntro: 'Xác nhận bản sao lưu bằng cách nhập các từ được yêu cầu.', wordN: 'Từ #{n}', + reenterPassphrase: 'Nhập lại mật khẩu', back: '← Quay lại', openWallet: 'Mở ví', + wordsMismatch: 'Các từ này không khớp với cụm từ của bạn. Hãy kiểm tra bản sao lưu.', + passphraseMismatch: 'Mật khẩu không khớp với mật khẩu bạn đã nhập.', + seedPhrase: 'Cụm từ hạt giống', seedPlaceholder: 'nhập các từ cách nhau bằng dấu cách', + optionalPassphrase: 'Mật khẩu tùy chọn', leaveBlank: 'để trống nếu không dùng', show: 'Hiện', hide: 'Ẩn', + invalidSeed: 'Cụm từ hạt giống không hợp lệ — kiểm tra chính tả và thứ tự.', + keysNote: 'Khóa không bao giờ rời khỏi trình duyệt của bạn. Hoạt động hoàn toàn ngoại tuyến. Lưu trang này và mở cục bộ.', + balance: 'Số dư', pending: 'Đang chờ', + tabReceive: 'Nhận', tabSend: 'Gửi', tabHistory: 'Lịch sử', tabSettings: 'Cài đặt', + copyAddress: 'Sao chép địa chỉ', paymentReceived: 'Đã nhận thanh toán!', tapToProceed: 'Chạm vào bất kỳ đâu để tiếp tục', + recipient: 'Người nhận', recipients: 'Người nhận', max: 'Tối đa', feeRate: 'Phí', + feeEconomy: 'Tiết kiệm', feeNormal: 'Thường', feePriority: 'Ưu tiên', feeCustom: 'Tùy chỉnh', + selectedRate: 'Phí đã chọn: {n} sat/vB', coinSelection: 'Chọn coin', automatic: 'Tự động', manual: 'Thủ công', + noCoins: 'Không có coin khả dụng.', nSelected: 'Đã chọn {n}', addRecipient: '+ Thêm người nhận', + reviewTx: 'Xem lại giao dịch', remove: 'Xóa', + enterRecipientAddr: 'Nhập địa chỉ người nhận.', enterAddrForN: 'Nhập địa chỉ cho người nhận {n}.', + enterValidAmtForN: 'Nhập số tiền hợp lệ cho người nhận {n}.', selectCoin: 'Chọn ít nhất một coin để chi.', + networkFee: 'Phí mạng', offlineSignNote: 'Ngoại tuyến: ký ngay, sau đó phát giao dịch từ thiết bị có mạng.', + signBroadcast: 'Ký & phát', signTx: 'Ký giao dịch', broadcastFailed: 'Phát thất bại: {msg}', signingFailed: 'Ký thất bại: {msg}', + done: 'Xong', txSignedNote: '✓ Đã ký giao dịch. Phát mã bên dưới từ thiết bị có mạng (vd. mempool.space → Broadcast).', + transactionId: 'ID giao dịch', signedTxRaw: 'Giao dịch đã ký (mã)', copyHex: 'Sao chép mã', downloadLabel: 'Tải xuống', + showQrAirgap: 'Hiện QR (chuyển ngoại tuyến)', txBroadcast: '✓ Đã phát giao dịch!', copyTxid: 'Sao chép ID giao dịch', viewOnMempool: 'Xem trên mempool.space ↗', + noTxYet: 'Chưa có giao dịch.', received: 'Đã nhận', sent: 'Đã gửi', pendingTag: '⏳ đang chờ', awaitingConfirmation: 'đang chờ xác nhận', + feeShort: 'phí {x}', historyOffline: 'Không xem được lịch sử khi ngoại tuyến.', + status: 'Trạng thái', confirmed: 'Đã xác nhận', confirmedTag: 'đã xác nhận', pendingInMempool: 'Đang chờ (trong mempool)', block: 'Khối', date: 'Ngày', + copyId: 'Sao chép ID', backToHistory: '← Về lịch sử', + recoveryPhrase: 'Cụm từ khôi phục', recoveryWarn: '⚠ Bất kỳ ai thấy các từ này đều có thể đánh cắp tiền của bạn. Hãy chắc chắn không ai nhìn trước khi hiển thị.', + bip39Passphrase: 'Mật khẩu', copyPassphrase: 'Sao chép mật khẩu', revealRecovery: 'Hiện cụm từ khôi phục', + offlineTransfer: 'Chuyển ngoại tuyến', offlineTransferDesc: 'Xuất coin trên thiết bị có mạng, rồi nhập tệp trên thiết bị ngoại tuyến (có ví và hạt giống này) để ký không cần internet.', + exportSnapshot: '⤓ Xuất snapshot', importSnapshot: 'Nhập snapshot…', rescan: 'Quét lại', + rescanDesc: 'Địa chỉ hiện tại tự cập nhật. Chỉ quét lại nếu thanh toán được gửi đến địa chỉ cũ (dùng lại) và không hiển thị.', + rescanWallet: 'Quét lại ví', scanning: 'Đang quét…', language: 'Ngôn ngữ', + offlineBanner: 'Không kết nối được mạng — đang ngoại tuyến. Nhập snapshot trong Cài đặt để tải coin.', retry: 'Thử lại', + copied: 'Đã sao chép', snapshotExported: 'Đã xuất snapshot', importedNCoins: 'Đã nhập {n} coin', + unmatchedSuffix: ' · {n} địa chỉ không thuộc ví này', importFailed: 'Nhập thất bại: {msg}', + copy: 'Sao chép', switchUnit: 'Đổi BTC / sats', footerMadeBy: 'làm bởi', footerSourceOn: 'mã nguồn tại', +}); + +def('sw', { + appTitle: 'Pochi ya Bitcoin', home: 'Mwanzo', logout: 'Toka', + createNew: 'Tengeneza mpya', importExisting: 'Leta iliyopo', + genIntro: 'Tengeneza kifungu kipya cha mbegu chenye maneno 12. Kiandike. Ndiyo njia pekee ya kurejesha pochi hii.', + generateSeed: 'Tengeneza kifungu cha mbegu', writeDownWarn: '⚠ Andika maneno haya 12 kwenye karatasi, kwa mpangilio. Yeyote mwenye nayo anaweza kutumia sarafu zako.', + copyPhrase: 'Nakili kifungu', regenerate: 'Tengeneza tena', verifyBackup: 'Thibitisha nakala', skipVerification: 'Ruka uthibitisho', + confirmBackupIntro: 'Thibitisha nakala yako kwa kuingiza maneno yaliyoombwa.', wordN: 'Neno #{n}', + reenterPassphrase: 'Ingiza tena nenosiri', back: '← Rudi', openWallet: 'Fungua pochi', + wordsMismatch: 'Maneno hayo hayalingani na kifungu chako. Angalia nakala yako.', + passphraseMismatch: 'Nenosiri hilo halilingani na ulilolitumia.', + seedPhrase: 'Kifungu cha mbegu', seedPlaceholder: 'andika maneno yako yakitenganishwa kwa nafasi', + optionalPassphrase: 'Nenosiri la hiari', leaveBlank: 'acha tupu kama halitumiki', show: 'Onyesha', hide: 'Ficha', + invalidSeed: 'Kifungu cha mbegu si sahihi — angalia tahajia na mpangilio.', + keysNote: 'Funguo hazitoki kamwe kwenye kivinjari chako. Hufanya kazi nje ya mtandao. Hifadhi ukurasa huu uufungue ndani.', + balance: 'Salio', pending: 'Inasubiri', + tabReceive: 'Pokea', tabSend: 'Tuma', tabHistory: 'Historia', tabSettings: 'Mipangilio', + copyAddress: 'Nakili anwani', paymentReceived: 'Malipo yamepokelewa!', tapToProceed: 'Gusa popote ili kuendelea', + recipient: 'Mpokeaji', recipients: 'Wapokeaji', max: 'Kiwango cha juu', feeRate: 'Ada', + feeEconomy: 'Nafuu', feeNormal: 'Kawaida', feePriority: 'Kipaumbele', feeCustom: 'Maalum', + selectedRate: 'Ada iliyochaguliwa: {n} sat/vB', coinSelection: 'Uchaguzi wa sarafu', automatic: 'Otomatiki', manual: 'Mwenyewe', + noCoins: 'Hakuna sarafu zinazopatikana.', nSelected: '{n} zimechaguliwa', addRecipient: '+ Ongeza mpokeaji', + reviewTx: 'Kagua muamala', remove: 'Ondoa', + enterRecipientAddr: 'Ingiza anwani ya mpokeaji.', enterAddrForN: 'Ingiza anwani ya mpokeaji {n}.', + enterValidAmtForN: 'Ingiza kiasi sahihi kwa mpokeaji {n}.', selectCoin: 'Chagua angalau sarafu moja ya kutumia.', + networkFee: 'Ada ya mtandao', offlineSignNote: 'Nje ya mtandao: tia saini sasa, kisha tangaza muamala kutoka kifaa chenye mtandao.', + signBroadcast: 'Tia saini & tangaza', signTx: 'Tia saini muamala', broadcastFailed: 'Kutangaza kumeshindwa: {msg}', signingFailed: 'Kutia saini kumeshindwa: {msg}', + done: 'Imekamilika', txSignedNote: '✓ Muamala umetiwa saini. Tangaza msimbo ulio hapa chini kutoka kifaa chenye mtandao (mf. mempool.space → Broadcast).', + transactionId: 'Kitambulisho cha muamala', signedTxRaw: 'Muamala uliotiwa saini (msimbo)', copyHex: 'Nakili msimbo', downloadLabel: 'Pakua', + showQrAirgap: 'Onyesha QR (kuhamisha nje ya mtandao)', txBroadcast: '✓ Muamala umetangazwa!', copyTxid: 'Nakili kitambulisho cha muamala', viewOnMempool: 'Tazama kwenye mempool.space ↗', + noTxYet: 'Bado hakuna miamala.', received: 'Imepokelewa', sent: 'Imetumwa', pendingTag: '⏳ inasubiri', awaitingConfirmation: 'inasubiri uthibitisho', + feeShort: 'ada {x}', historyOffline: 'Historia haipatikani ukiwa nje ya mtandao.', + status: 'Hali', confirmed: 'Imethibitishwa', confirmedTag: 'imethibitishwa', pendingInMempool: 'Inasubiri (kwenye mempool)', block: 'Block', date: 'Tarehe', + copyId: 'Nakili kitambulisho', backToHistory: '← Rudi kwenye historia', + recoveryPhrase: 'Kifungu cha kurejesha', recoveryWarn: '⚠ Yeyote anayeona maneno haya anaweza kuiba fedha zako. Hakikisha hakuna anayetazama kabla ya kuyaonyesha.', + bip39Passphrase: 'Nenosiri', copyPassphrase: 'Nakili nenosiri', revealRecovery: 'Onyesha kifungu cha kurejesha', + offlineTransfer: 'Uhamisho nje ya mtandao', offlineTransferDesc: 'Hamisha sarafu zako kwenye kifaa chenye mtandao, kisha leta faili kwenye kifaa kisicho na mtandao (chenye pochi hii na mbegu) ili kutia saini bila intaneti.', + exportSnapshot: '⤓ Hamisha snapshot', importSnapshot: 'Leta snapshot…', rescan: 'Changanua tena', + rescanDesc: 'Anwani yako ya sasa husasishwa kiotomatiki. Changanua tena tu ikiwa malipo yalitumwa kwa anwani ya zamani (iliyotumika tena) na hayaonekani.', + rescanWallet: 'Changanua pochi tena', scanning: 'Inachanganua…', language: 'Lugha', + offlineBanner: 'Imeshindwa kufikia mtandao — inafanya kazi nje ya mtandao. Leta snapshot katika Mipangilio kupakia sarafu.', retry: 'Jaribu tena', + copied: 'Imenakiliwa', snapshotExported: 'Snapshot imehamishwa', importedNCoins: 'Sarafu {n} zimeletwa', + unmatchedSuffix: ' · anwani {n} si za pochi hii', importFailed: 'Kuleta kumeshindwa: {msg}', + copy: 'Nakili', switchUnit: 'Badilisha BTC / sats', footerMadeBy: 'imetengenezwa na', footerSourceOn: 'msimbo unapatikana', +}); + +def('mr', { + appTitle: 'बिटकॉइन वॉलेट', home: 'मुख्यपृष्ठ', logout: 'बाहेर पडा', + createNew: 'नवीन तयार करा', importExisting: 'अस्तित्वातील आयात करा', + genIntro: 'नवीन १२-शब्दांचा सीड वाक्यांश तयार करा. तो लिहून ठेवा. हे वॉलेट परत मिळवण्याचा हाच एकमेव मार्ग आहे.', + generateSeed: 'सीड वाक्यांश तयार करा', writeDownWarn: '⚠ हे १२ शब्द क्रमाने कागदावर लिहा. ज्याच्याकडे हे असतील तो तुमची नाणी खर्च करू शकतो.', + copyPhrase: 'वाक्यांश कॉपी करा', regenerate: 'पुन्हा तयार करा', verifyBackup: 'बॅकअप तपासा', skipVerification: 'पडताळणी वगळा', + confirmBackupIntro: 'मागितलेले शब्द टाकून तुमचा बॅकअप निश्चित करा.', wordN: 'शब्द #{n}', + reenterPassphrase: 'पासफ्रेज पुन्हा टाका', back: '← मागे', openWallet: 'वॉलेट उघडा', + wordsMismatch: 'हे शब्द तुमच्या वाक्यांशाशी जुळत नाहीत. तुमचा बॅकअप तपासा.', + passphraseMismatch: 'हा पासफ्रेज तुम्ही टाकलेल्याशी जुळत नाही.', + seedPhrase: 'सीड वाक्यांश', seedPlaceholder: 'तुमचे शब्द स्पेसने वेगळे करून टाका', + optionalPassphrase: 'ऐच्छिक पासफ्रेज', leaveBlank: 'वापर नसल्यास रिकामे ठेवा', show: 'दाखवा', hide: 'लपवा', + invalidSeed: 'अवैध सीड वाक्यांश — स्पेलिंग व क्रम तपासा.', + keysNote: 'की कधीही तुमचा ब्राउझर सोडत नाहीत. पूर्णपणे ऑफलाइन चालते. हे पान जतन करून स्थानिकरीत्या उघडा.', + balance: 'शिल्लक', pending: 'प्रलंबित', + tabReceive: 'मिळवा', tabSend: 'पाठवा', tabHistory: 'इतिहास', tabSettings: 'सेटिंग्ज', + copyAddress: 'पत्ता कॉपी करा', paymentReceived: 'पेमेंट मिळाले!', tapToProceed: 'पुढे जाण्यासाठी कुठेही टॅप करा', + recipient: 'प्राप्तकर्ता', recipients: 'प्राप्तकर्ते', max: 'कमाल', feeRate: 'शुल्क', + feeEconomy: 'किफायतशीर', feeNormal: 'सामान्य', feePriority: 'प्राधान्य', feeCustom: 'सानुकूल', + selectedRate: 'निवडलेले शुल्क: {n} sat/vB', coinSelection: 'नाणे निवड', automatic: 'स्वयंचलित', manual: 'स्वहस्ते', + noCoins: 'कोणतीही नाणी उपलब्ध नाहीत.', nSelected: '{n} निवडली', addRecipient: '+ प्राप्तकर्ता जोडा', + reviewTx: 'व्यवहार पुनरावलोकन', remove: 'काढा', + enterRecipientAddr: 'प्राप्तकर्त्याचा पत्ता टाका.', enterAddrForN: 'प्राप्तकर्ता {n} चा पत्ता टाका.', + enterValidAmtForN: 'प्राप्तकर्ता {n} साठी वैध रक्कम टाका.', selectCoin: 'खर्चासाठी किमान एक नाणे निवडा.', + networkFee: 'नेटवर्क शुल्क', offlineSignNote: 'ऑफलाइन: आता स्वाक्षरी करा, मग ऑनलाइन उपकरणावरून व्यवहार प्रसारित करा.', + signBroadcast: 'स्वाक्षरी व प्रसारण', signTx: 'व्यवहार स्वाक्षरी', broadcastFailed: 'प्रसारण अयशस्वी: {msg}', signingFailed: 'स्वाक्षरी अयशस्वी: {msg}', + done: 'झाले', txSignedNote: '✓ व्यवहार स्वाक्षरीत झाला. खालील कोड ऑनलाइन उपकरणावरून प्रसारित करा (उदा. mempool.space → Broadcast).', + transactionId: 'व्यवहार ID', signedTxRaw: 'स्वाक्षरीत व्यवहार (कोड)', copyHex: 'कोड कॉपी करा', downloadLabel: 'डाउनलोड', + showQrAirgap: 'QR दाखवा (ऑफलाइन हस्तांतरणासाठी)', txBroadcast: '✓ व्यवहार प्रसारित झाला!', copyTxid: 'व्यवहार ID कॉपी करा', viewOnMempool: 'mempool.space वर पहा ↗', + noTxYet: 'अजून व्यवहार नाहीत.', received: 'मिळाले', sent: 'पाठवले', pendingTag: '⏳ प्रलंबित', awaitingConfirmation: 'पुष्टीची प्रतीक्षा', + feeShort: 'शुल्क {x}', historyOffline: 'ऑफलाइनमध्ये इतिहास उपलब्ध नाही.', + status: 'स्थिती', confirmed: 'पुष्ट', confirmedTag: 'पुष्ट', pendingInMempool: 'प्रलंबित (mempool मध्ये)', block: 'ब्लॉक', date: 'दिनांक', + copyId: 'ID कॉपी करा', backToHistory: '← इतिहासाकडे परत', + recoveryPhrase: 'रिकव्हरी वाक्यांश', recoveryWarn: '⚠ हे शब्द पाहणारा कोणीही तुमचे पैसे चोरू शकतो. दाखवण्यापूर्वी कोणी पाहत नाही याची खात्री करा.', + bip39Passphrase: 'पासफ्रेज', copyPassphrase: 'पासफ्रेज कॉपी करा', revealRecovery: 'रिकव्हरी वाक्यांश दाखवा', + offlineTransfer: 'ऑफलाइन हस्तांतरण', offlineTransferDesc: 'ऑनलाइन उपकरणावर तुमची नाणी निर्यात करा, मग ऑफलाइन उपकरणावर (याच वॉलेट व सीडसह) फाइल आयात करून इंटरनेटशिवाय स्वाक्षरी करा.', + exportSnapshot: '⤓ स्नॅपशॉट निर्यात', importSnapshot: 'स्नॅपशॉट आयात…', rescan: 'पुन्हा स्कॅन', + rescanDesc: 'तुमचा सध्याचा पत्ता आपोआप अद्ययावत होतो. पेमेंट जुन्या (पुन्हा वापरलेल्या) पत्त्यावर आले व दिसत नसेल तरच पुन्हा स्कॅन करा.', + rescanWallet: 'वॉलेट पुन्हा स्कॅन', scanning: 'स्कॅन होत आहे…', language: 'भाषा', + offlineBanner: 'नेटवर्कशी संपर्क नाही — ऑफलाइन काम सुरू. नाणी लोड करण्यासाठी सेटिंग्जमधून स्नॅपशॉट आयात करा.', retry: 'पुन्हा प्रयत्न', + copied: 'कॉपी झाले', snapshotExported: 'स्नॅपशॉट निर्यात झाला', importedNCoins: '{n} नाणी आयात झाली', + unmatchedSuffix: ' · {n} पत्ते या वॉलेटमध्ये नाहीत', importFailed: 'आयात अयशस्वी: {msg}', + copy: 'कॉपी', switchUnit: 'BTC / sats बदला', footerMadeBy: 'द्वारे बनवले', footerSourceOn: 'सोर्स येथे', +}); + +def('te', { + appTitle: 'బిట్కాయిన్ వాలెట్', home: 'హోమ్', logout: 'లాగౌట్', + createNew: 'కొత్తది సృష్టించు', importExisting: 'ఉన్నదాన్ని దిగుమతి', + genIntro: 'కొత్త 12-పదాల సీడ్ ఫ్రేజ్ సృష్టించండి. దాన్ని రాసుకోండి. ఈ వాలెట్ను తిరిగి పొందడానికి ఇదే ఏకైక మార్గం.', + generateSeed: 'సీడ్ ఫ్రేజ్ సృష్టించు', writeDownWarn: '⚠ ఈ 12 పదాలను వరుసగా కాగితంపై రాయండి. ఇవి ఉన్నవారు మీ నాణేలను ఖర్చు చేయగలరు.', + copyPhrase: 'ఫ్రేజ్ కాపీ', regenerate: 'మళ్లీ సృష్టించు', verifyBackup: 'బ్యాకప్ ధృవీకరించు', skipVerification: 'ధృవీకరణ దాటవేయి', + confirmBackupIntro: 'అడిగిన పదాలను నమోదు చేసి మీ బ్యాకప్ను ధృవీకరించండి.', wordN: 'పదం #{n}', + reenterPassphrase: 'పాస్ఫ్రేజ్ మళ్లీ నమోదు చేయి', back: '← వెనక్కి', openWallet: 'వాలెట్ తెరువు', + wordsMismatch: 'ఈ పదాలు మీ ఫ్రేజ్తో సరిపోలడం లేదు. మీ బ్యాకప్ చూడండి.', + passphraseMismatch: 'ఈ పాస్ఫ్రేజ్ మీరు నమోదు చేసినదానితో సరిపోలడం లేదు.', + seedPhrase: 'సీడ్ ఫ్రేజ్', seedPlaceholder: 'మీ పదాలను ఖాళీలతో వేరు చేసి నమోదు చేయండి', + optionalPassphrase: 'ఐచ్ఛిక పాస్ఫ్రేజ్', leaveBlank: 'వాడకపోతే ఖాళీగా ఉంచండి', show: 'చూపు', hide: 'దాచు', + invalidSeed: 'చెల్లని సీడ్ ఫ్రేజ్ — స్పెల్లింగ్, వరుస చూడండి.', + keysNote: 'కీలు మీ బ్రౌజర్ను ఎప్పుడూ వదలవు. పూర్తిగా ఆఫ్లైన్లో పనిచేస్తుంది. ఈ పేజీని సేవ్ చేసి స్థానికంగా తెరవండి.', + balance: 'బ్యాలెన్స్', pending: 'పెండింగ్', + tabReceive: 'స్వీకరించు', tabSend: 'పంపు', tabHistory: 'చరిత్ర', tabSettings: 'సెట్టింగ్లు', + copyAddress: 'చిరునామా కాపీ', paymentReceived: 'చెల్లింపు అందింది!', tapToProceed: 'కొనసాగడానికి ఎక్కడైనా నొక్కండి', + recipient: 'గ్రహీత', recipients: 'గ్రహీతలు', max: 'గరిష్ఠం', feeRate: 'ఫీజు', + feeEconomy: 'ఆర్థిక', feeNormal: 'సాధారణ', feePriority: 'ప్రాధాన్యం', feeCustom: 'అనుకూల', + selectedRate: 'ఎంచుకున్న ఫీజు: {n} sat/vB', coinSelection: 'నాణెం ఎంపిక', automatic: 'స్వయంచాలక', manual: 'మాన్యువల్', + noCoins: 'నాణేలు అందుబాటులో లేవు.', nSelected: '{n} ఎంచుకోబడ్డాయి', addRecipient: '+ గ్రహీతను జోడించు', + reviewTx: 'లావాదేవీ సమీక్ష', remove: 'తొలగించు', + enterRecipientAddr: 'గ్రహీత చిరునామా నమోదు చేయండి.', enterAddrForN: 'గ్రహీత {n} చిరునామా నమోదు చేయండి.', + enterValidAmtForN: 'గ్రహీత {n} కోసం సరైన మొత్తాన్ని నమోదు చేయండి.', selectCoin: 'ఖర్చు చేయడానికి కనీసం ఒక నాణెం ఎంచుకోండి.', + networkFee: 'నెట్వర్క్ ఫీజు', offlineSignNote: 'ఆఫ్లైన్: ఇప్పుడే సంతకం చేయండి, తర్వాత ఆన్లైన్ పరికరం నుండి లావాదేవీని ప్రసారం చేయండి.', + signBroadcast: 'సంతకం & ప్రసారం', signTx: 'లావాదేవీపై సంతకం', broadcastFailed: 'ప్రసారం విఫలం: {msg}', signingFailed: 'సంతకం విఫలం: {msg}', + done: 'పూర్తయింది', txSignedNote: '✓ లావాదేవీపై సంతకం అయింది. కింది కోడ్ను ఆన్లైన్ పరికరం నుండి ప్రసారం చేయండి (ఉదా. mempool.space → Broadcast).', + transactionId: 'లావాదేవీ ID', signedTxRaw: 'సంతకం చేసిన లావాదేవీ (కోడ్)', copyHex: 'కోడ్ కాపీ', downloadLabel: 'డౌన్లోడ్', + showQrAirgap: 'QR చూపు (ఆఫ్లైన్ బదిలీ కోసం)', txBroadcast: '✓ లావాదేవీ ప్రసారమైంది!', copyTxid: 'లావాదేవీ ID కాపీ', viewOnMempool: 'mempool.space లో చూడండి ↗', + noTxYet: 'ఇంకా లావాదేవీలు లేవు.', received: 'అందింది', sent: 'పంపబడింది', pendingTag: '⏳ పెండింగ్', awaitingConfirmation: 'ధృవీకరణ కోసం వేచి', + feeShort: 'ఫీజు {x}', historyOffline: 'ఆఫ్లైన్లో చరిత్ర అందుబాటులో లేదు.', + status: 'స్థితి', confirmed: 'ధృవీకరించబడింది', confirmedTag: 'ధృవీకరించబడింది', pendingInMempool: 'పెండింగ్ (mempool లో)', block: 'బ్లాక్', date: 'తేదీ', + copyId: 'ID కాపీ', backToHistory: '← చరిత్రకు తిరిగి', + recoveryPhrase: 'రికవరీ ఫ్రేజ్', recoveryWarn: '⚠ ఈ పదాలు చూసిన ఎవరైనా మీ నిధులను దొంగిలించగలరు. చూపే ముందు ఎవరూ చూడటం లేదని నిర్ధారించుకోండి.', + bip39Passphrase: 'పాస్ఫ్రేజ్', copyPassphrase: 'పాస్ఫ్రేజ్ కాపీ', revealRecovery: 'రికవరీ ఫ్రేజ్ చూపు', + offlineTransfer: 'ఆఫ్లైన్ బదిలీ', offlineTransferDesc: 'ఆన్లైన్ పరికరంలో మీ నాణేలను ఎగుమతి చేసి, ఆఫ్లైన్ పరికరంలో (ఈ వాలెట్, సీడ్తో) ఫైల్ను దిగుమతి చేసి ఇంటర్నెట్ లేకుండా సంతకం చేయండి.', + exportSnapshot: '⤓ స్నాప్షాట్ ఎగుమతి', importSnapshot: 'స్నాప్షాట్ దిగుమతి…', rescan: 'మళ్లీ స్కాన్', + rescanDesc: 'మీ ప్రస్తుత చిరునామా స్వయంచాలకంగా అప్డేట్ అవుతుంది. చెల్లింపు పాత (మళ్లీ వాడిన) చిరునామాకు వచ్చి కనిపించకపోతేనే మళ్లీ స్కాన్ చేయండి.', + rescanWallet: 'వాలెట్ మళ్లీ స్కాన్', scanning: 'స్కాన్ అవుతోంది…', language: 'భాష', + offlineBanner: 'నెట్వర్క్ చేరలేకపోయింది — ఆఫ్లైన్లో పని. నాణేలు లోడ్ చేయడానికి సెట్టింగ్లలో స్నాప్షాట్ దిగుమతి చేయండి.', retry: 'మళ్లీ ప్రయత్నించు', + copied: 'కాపీ అయింది', snapshotExported: 'స్నాప్షాట్ ఎగుమతి అయింది', importedNCoins: '{n} నాణేలు దిగుమతి అయ్యాయి', + unmatchedSuffix: ' · {n} చిరునామాలు ఈ వాలెట్లో లేవు', importFailed: 'దిగుమతి విఫలం: {msg}', + copy: 'కాపీ', switchUnit: 'BTC / sats మార్చు', footerMadeBy: 'తయారు చేసినవారు', footerSourceOn: 'సోర్స్ ఇక్కడ', +}); + +def('ta', { + appTitle: 'பிட்காயின் வாலட்', home: 'முகப்பு', logout: 'வெளியேறு', + createNew: 'புதிது உருவாக்கு', importExisting: 'உள்ளதை இறக்கு', + genIntro: 'புதிய 12-சொல் சீட் சொற்றொடரை உருவாக்கவும். எழுதி வைக்கவும். இந்த வாலட்டை மீட்க இதுவே ஒரே வழி.', + generateSeed: 'சீட் சொற்றொடரை உருவாக்கு', writeDownWarn: '⚠ இந்த 12 சொற்களை வரிசையாக காகிதத்தில் எழுதவும். இவை உள்ளவர் உங்கள் நாணயங்களைச் செலவழிக்கலாம்.', + copyPhrase: 'சொற்றொடரை நகலெடு', regenerate: 'மீண்டும் உருவாக்கு', verifyBackup: 'காப்பை சரிபார்', skipVerification: 'சரிபார்ப்பைத் தவிர்', + confirmBackupIntro: 'கேட்கப்பட்ட சொற்களை உள்ளிட்டு உங்கள் காப்பை உறுதிப்படுத்துங்கள்.', wordN: 'சொல் #{n}', + reenterPassphrase: 'கடவுச்சொற்றொடரை மீண்டும் உள்ளிடு', back: '← பின்', openWallet: 'வாலட்டைத் திற', + wordsMismatch: 'இந்த சொற்கள் உங்கள் சொற்றொடருடன் பொருந்தவில்லை. உங்கள் காப்பை சரிபார்க்கவும்.', + passphraseMismatch: 'இந்த கடவுச்சொற்றொடர் நீங்கள் உள்ளிட்டதுடன் பொருந்தவில்லை.', + seedPhrase: 'சீட் சொற்றொடர்', seedPlaceholder: 'உங்கள் சொற்களை இடைவெளியால் பிரித்து உள்ளிடவும்', + optionalPassphrase: 'விருப்பக் கடவுச்சொற்றொடர்', leaveBlank: 'பயன்படுத்தாவிட்டால் காலியாக விடவும்', show: 'காட்டு', hide: 'மறை', + invalidSeed: 'தவறான சீட் சொற்றொடர் — எழுத்துப்பிழை, வரிசையைச் சரிபார்க்கவும்.', + keysNote: 'விசைகள் உங்கள் உலாவியை விட்டு வெளியேறாது. முழுவதும் ஆஃப்லைனில் இயங்கும். இந்தப் பக்கத்தைச் சேமித்து உள்ளூரில் திறக்கவும்.', + balance: 'இருப்பு', pending: 'நிலுவையில்', + tabReceive: 'பெறு', tabSend: 'அனுப்பு', tabHistory: 'வரலாறு', tabSettings: 'அமைப்புகள்', + copyAddress: 'முகவரியை நகலெடு', paymentReceived: 'பணம் பெறப்பட்டது!', tapToProceed: 'தொடர எங்கேனும் தட்டவும்', + recipient: 'பெறுநர்', recipients: 'பெறுநர்கள்', max: 'அதிகபட்சம்', feeRate: 'கட்டணம்', + feeEconomy: 'சிக்கனம்', feeNormal: 'சாதாரண', feePriority: 'முன்னுரிமை', feeCustom: 'தனிப்பயன்', + selectedRate: 'தேர்ந்த கட்டணம்: {n} sat/vB', coinSelection: 'நாணயத் தேர்வு', automatic: 'தானியங்கி', manual: 'கைமுறை', + noCoins: 'நாணயங்கள் எதுவும் இல்லை.', nSelected: '{n} தேர்ந்தது', addRecipient: '+ பெறுநரைச் சேர்', + reviewTx: 'பரிவர்த்தனையை மதிப்பாய்வு', remove: 'அகற்று', + enterRecipientAddr: 'பெறுநர் முகவரியை உள்ளிடவும்.', enterAddrForN: 'பெறுநர் {n}-இன் முகவரியை உள்ளிடவும்.', + enterValidAmtForN: 'பெறுநர் {n}-க்கு சரியான தொகையை உள்ளிடவும்.', selectCoin: 'செலவழிக்க குறைந்தது ஒரு நாணயத்தைத் தேர்ந்தெடுக்கவும்.', + networkFee: 'நெட்வொர்க் கட்டணம்', offlineSignNote: 'ஆஃப்லைன்: இப்போது கையொப்பமிடவும், பிறகு ஆன்லைன் சாதனத்திலிருந்து பரிவர்த்தனையை ஒளிபரப்பவும்.', + signBroadcast: 'கையொப்பம் & ஒளிபரப்பு', signTx: 'பரிவர்த்தனையில் கையொப்பம்', broadcastFailed: 'ஒளிபரப்பு தோல்வி: {msg}', signingFailed: 'கையொப்பம் தோல்வி: {msg}', + done: 'முடிந்தது', txSignedNote: '✓ பரிவர்த்தனை கையொப்பமிடப்பட்டது. கீழுள்ள குறியீட்டை ஆன்லைன் சாதனத்திலிருந்து ஒளிபரப்பவும் (எ.கா. mempool.space → Broadcast).', + transactionId: 'பரிவர்த்தனை ID', signedTxRaw: 'கையொப்பமிட்ட பரிவர்த்தனை (குறியீடு)', copyHex: 'குறியீடு நகலெடு', downloadLabel: 'பதிவிறக்கு', + showQrAirgap: 'QR காட்டு (ஆஃப்லைன் பரிமாற்றத்திற்கு)', txBroadcast: '✓ பரிவர்த்தனை ஒளிபரப்பப்பட்டது!', copyTxid: 'பரிவர்த்தனை ID நகலெடு', viewOnMempool: 'mempool.space இல் பார் ↗', + noTxYet: 'இன்னும் பரிவர்த்தனைகள் இல்லை.', received: 'பெறப்பட்டது', sent: 'அனுப்பப்பட்டது', pendingTag: '⏳ நிலுவையில்', awaitingConfirmation: 'உறுதிப்படுத்தலுக்காகக் காத்திருக்கிறது', + feeShort: 'கட்டணம் {x}', historyOffline: 'ஆஃப்லைனில் வரலாறு கிடைக்காது.', + status: 'நிலை', confirmed: 'உறுதியானது', confirmedTag: 'உறுதியானது', pendingInMempool: 'நிலுவையில் (mempool இல்)', block: 'பிளாக்', date: 'தேதி', + copyId: 'ID நகலெடு', backToHistory: '← வரலாற்றுக்கு', + recoveryPhrase: 'மீட்பு சொற்றொடர்', recoveryWarn: '⚠ இந்த சொற்களைப் பார்க்கும் எவரும் உங்கள் பணத்தைத் திருடலாம். காட்டும் முன் யாரும் பார்க்கவில்லை என உறுதி செய்யவும்.', + bip39Passphrase: 'கடவுச்சொற்றொடர்', copyPassphrase: 'கடவுச்சொற்றொடரை நகலெடு', revealRecovery: 'மீட்பு சொற்றொடரைக் காட்டு', + offlineTransfer: 'ஆஃப்லைன் பரிமாற்றம்', offlineTransferDesc: 'ஆன்லைன் சாதனத்தில் நாணயங்களை ஏற்றுமதி செய்து, ஆஃப்லைன் சாதனத்தில் (இந்த வாலட் மற்றும் சீடுடன்) கோப்பை இறக்கி இணையம் இல்லாமல் கையொப்பமிடவும்.', + exportSnapshot: '⤓ ஸ்னாப்ஷாட் ஏற்று', importSnapshot: 'ஸ்னாப்ஷாட் இறக்கு…', rescan: 'மீண்டும் ஸ்கேன்', + rescanDesc: 'உங்கள் தற்போதைய முகவரி தானாகப் புதுப்பிக்கப்படும். பணம் பழைய (மறுபயன்பாட்டு) முகவரிக்கு வந்து தெரியவில்லை எனில் மட்டுமே மீண்டும் ஸ்கேன் செய்யவும்.', + rescanWallet: 'வாலட்டை மீண்டும் ஸ்கேன்', scanning: 'ஸ்கேன் செய்கிறது…', language: 'மொழி', + offlineBanner: 'நெட்வொர்க்கை அடைய முடியவில்லை — ஆஃப்லைனில் இயங்குகிறது. நாணயங்களை ஏற்ற அமைப்புகளில் ஸ்னாப்ஷாட்டை இறக்கவும்.', retry: 'மீண்டும் முயல்', + copied: 'நகலெடுக்கப்பட்டது', snapshotExported: 'ஸ்னாப்ஷாட் ஏற்றப்பட்டது', importedNCoins: '{n} நாணயங்கள் இறக்கப்பட்டன', + unmatchedSuffix: ' · {n} முகவரிகள் இந்த வாலட்டில் இல்லை', importFailed: 'இறக்குமதி தோல்வி: {msg}', + copy: 'நகலெடு', switchUnit: 'BTC / sats மாற்று', footerMadeBy: 'உருவாக்கியவர்', footerSourceOn: 'மூலம்', +}); + +// --- runtime -------------------------------------------------------------- +const SUPPORTED = new Set(LANGS.map(([c]) => c)); +const LANG_KEY = 'btc-wallet-lang'; + +function detect() { + try { + const saved = localStorage.getItem(LANG_KEY); + if (saved && SUPPORTED.has(saved)) return saved; + } catch {} + try { + for (const l of navigator.languages || [navigator.language || 'en']) { + const code = String(l).toLowerCase().split('-')[0]; + if (SUPPORTED.has(code)) return code; + } + } catch {} + return 'en'; +} + +let lang = detect(); + +export const getLang = () => lang; + +export function setLang(l) { + if (!SUPPORTED.has(l)) return; + lang = l; + try { + localStorage.setItem(LANG_KEY, l); + } catch {} +} + +export function t(key, vars) { + let s = (STR[lang] && STR[lang][key]) ?? STR.en[key] ?? key; + if (vars) for (const k in vars) s = s.replaceAll('{' + k + '}', vars[k]); + return s; +} diff --git a/src/style.css b/src/style.css index dce2d88..d913d76 100644 --- a/src/style.css +++ b/src/style.css @@ -198,6 +198,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si width: 18px; height: 18px; border: 2px solid var(--line-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; display: inline-block; vertical-align: middle; } +.spinner.sm { width: 13px; height: 13px; border-width: 2px; } @keyframes spin { to { transform: rotate(360deg); } } .toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); @@ -230,6 +231,14 @@ hr.divider { margin: 16px 0; } .linklike { background: none; border: 0; padding: 0; color: var(--accent-ink); font-weight: 600; cursor: pointer; } .linklike:hover { text-decoration: underline; background: none; } +/* ---- how it works ---- */ +.lead { font-size: 16px; line-height: 1.5; color: var(--ink); } +.hiw-tech { border-top: 1px solid var(--line); padding-top: 14px; } +.hiw-tech > summary { font-weight: 650; cursor: pointer; list-style: none; color: var(--accent-ink); display: flex; align-items: center; gap: 7px; } +.hiw-tech > summary::-webkit-details-marker { display: none; } +.hiw-tech > summary::before { content: '▸'; font-size: 12px; transition: transform .15s; } +.hiw-tech[open] > summary::before { transform: rotate(90deg); } + @media (max-width: 420px) { .words { grid-template-columns: repeat(2, 1fr); } .balance .amt { font-size: 28px; } diff --git a/src/wallet.js b/src/wallet.js index ba22af2..7152d31 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -70,6 +70,7 @@ export class Wallet { this.scanning = false; this.loaded = false; // true once a scan/snapshot has populated balances once + this.historyLoading = false; // history is still being fetched in the background this._refreshing = false; // a scan/refresh is in flight this.nextReceiveIndex = 0; this.nextChangeIndex = 0; @@ -206,10 +207,11 @@ export class Wallet { const cs = info.chain_stats || {}; const ms = info.mempool_stats || {}; const used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0; + const hasMempool = (ms.tx_count || 0) > 0; // Balance straight from chain_stats — no need to fetch /utxo just to total. const confirmed = (cs.funded_txo_sum || 0) - (cs.spent_txo_sum || 0); const pending = (ms.funded_txo_sum || 0) - (ms.spent_txo_sum || 0); - found.push({ chain, index: i, address, used, confirmed, pending }); + found.push({ chain, index: i, address, used, confirmed, pending, hasMempool }); this.addrMap.set(address, { chain, index: i }); if (used) { gap = 0; @@ -336,8 +338,11 @@ export class Wallet { try { const prevBal = `${this.confirmed}|${this.pending}|${this.nextReceiveIndex}|${this.nextChangeIndex}`; this.addrMap = new Map(); - this.receive = await this.scanChain(0); - this.change = await this.scanChain(1); + // Probe both chains at once — they're independent, so interleaving lets + // their per-hit delays overlap instead of running back-to-back. + const [receive, change] = await Promise.all([this.scanChain(0), this.scanChain(1)]); + this.receive = receive; + this.change = change; this.nextReceiveIndex = firstUnused(this.receive); this.nextChangeIndex = firstUnused(this.change); this._recomputeBalanceFromChains(); @@ -347,14 +352,23 @@ export class Wallet { const balanceChanged = `${this.confirmed}|${this.pending}|${this.nextReceiveIndex}|${this.nextChangeIndex}` !== prevBal; if (!silent || balanceChanged || !this.loaded) { + // Balance + receive address are already known from the chain_stats above. + // Show them right away so the wallet looks ready, then keep loading the + // heavier UTXO set and full history in the background. + if (!silent) { + this.loaded = true; + this.historyLoading = true; + this.emit(); + } if (!silent || !this.feeRates) this.feeRates = await this.api.feeRates(); await this.refreshUtxos(); - await this.refreshHistory(); + await this.refreshHistory(!silent); } this.loaded = true; this.saveCache(); } finally { this._refreshing = false; + this.historyLoading = false; // clear even if an earlier step threw if (!silent) { this.scanning = false; this.emit(); @@ -369,7 +383,13 @@ export class Wallet { } async refreshUtxos() { - const used = this.usedAddresses(); + // An address whose confirmed balance is 0 and which has no mempool activity + // is fully spent and settled — it cannot hold any UTXOs, so skip the /utxo + // round-trip for it. In a wallet with history most addresses are spent, so + // this avoids the bulk of the requests (and the 429s that come with them). + const used = this.usedAddresses().filter( + (a) => (a.confirmed || 0) > 0 || a.hasMempool + ); const lists = await pool(used, (a) => this.api.addressUtxos(a.address)); const utxos = []; used.forEach((a, idx) => { @@ -418,17 +438,35 @@ export class Wallet { }); } - async refreshHistory() { + // progressive: emit after each address resolves so the History tab fills in + // as transactions arrive (foreground load) instead of all at once at the end. + async refreshHistory(progressive = false) { const used = this.usedAddresses(); - const lists = await pool(used, (a) => this.api.addressTxs(a.address)); - const byId = new Map(); - for (const list of lists) { - for (const tx of list) { - if (!byId.has(tx.txid)) byId.set(tx.txid, this._txSummary(tx)); - } + this.historyLoading = true; + const seen = new Set(); // txids found in this pass (a tx can touch >1 address) + try { + await pool(used, async (a) => { + const list = await this.api.addressTxs(a.address); + let added = false; + for (const tx of list) { + if (seen.has(tx.txid)) continue; + seen.add(tx.txid); + const summary = this._txSummary(tx); + const at = this.txs.findIndex((t) => t.txid === tx.txid); + if (at >= 0) this.txs[at] = summary; // refresh confirmations/fee + else { this.txs.push(summary); added = true; } + } + if (added && progressive) { + this._sortTxs(); + this.emit(); + } + }); + // Drop anything that no longer appears in any address's history. + this.txs = this.txs.filter((t) => seen.has(t.txid)); + this._sortTxs(); + } finally { + this.historyLoading = false; } - this.txs = [...byId.values()]; - this._sortTxs(); } // --- balances ----------------------------------------------------------- |
