aboutsummaryrefslogtreecommitdiffstats
path: root/src/i18n.js
blob: 91fdcf7eeb34cba74073aff67700b948edcb9ce0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
// 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: 'coinos',
    home: 'Home',
    lockWallet: 'Lock wallet',
    accounts: 'Wallets',
    addWallet: 'Add wallet',
    clearAll: 'Clear all',
    clearAllWarn: '⚠ This removes every wallet from this device. Any wallet whose recovery phrase or key you have not backed up will be lost for good.',
    removeWalletTitle: 'Remove wallet',
    removeWalletWarn: '⚠ Remove “{name}” from this device? It will be gone from this browser — make sure you have its recovery phrase. Other wallets aren’t affected.',
    rename: 'Rename',
    remove: 'Remove',
    watchOnly: 'Watch-only',
    watchOnlyTag: 'watch-only',
    watchOnlyAdd: 'Add watch-only',
    watchOnlyDesc: 'Watch any wallet from its account public key. You can receive and view balance and history, but not send.',
    watchOnlyNote: 'This is a watch-only wallet. You can receive and view, but you cannot send.',
    loadSeedTitle: 'Load recovery phrase',
    loadSeedDesc: "Enter this wallet's recovery phrase (or private key) to spend from it — it must match this wallet.",
    loadSeedBtn: 'Load recovery phrase',
    seedPlaceholder: 'Recovery phrase or xprv',
    bip39PassphraseOpt: 'BIP39 passphrase (optional)',
    alsoSaveDevice: 'Also save to this device',
    enterSeedToSpend: 'Enter your recovery phrase.',
    seedMismatch: "That recovery phrase doesn't match this wallet.",
    seedNeedsPrivate: "That's a public key — enter the recovery phrase or private key to spend.",
    seedInvalid: 'Not a valid recovery phrase or key.',
    watchOnlySendTitle: 'Load your seed to send',
    watchOnlySendDesc: "You're viewing this wallet without its seed, so you can't spend yet. Load your recovery phrase to send.",
    watchXpubLabel: 'Account xpub or zpub',
    labelOptional: 'Label (optional)',
    walletLabelN: 'Wallet {n}',
    watchLabelN: 'Watch {n}',
    saveDevice: 'Save on this device',
    forgetDevice: 'Saved · forget',
    savedTag: 'saved',
    setPassword: 'Set a password (optional)',
    setPasswordDesc: 'Encrypts this wallet on this device — leave it blank to save without a password. You will need it to unlock saved wallets later.',
    passwordOptional: 'Password (optional)',
    changePassword: 'Change password',
    changePasswordDesc: 'Re-encrypt your saved-to-device wallets with a new password. Leave the new password blank to remove it.',
    currentPassword: 'Current password',
    pwChanged: 'Password changed',
    enterPassword: 'Enter your password',
    enterPasswordDesc: 'Unlock your saved wallets to continue.',
    password: 'Password',
    confirmPassword: 'Confirm password',
    pwTooShort: 'Use at least 8 characters.',
    pwMismatch: 'Passwords do not match.',
    pwWrong: 'Wrong password.',
    pwNeededToLock: 'Enter a password, or choose "Not now".',
    lockPwTitle: 'Protect this device?',
    lockPwDesc: 'Your wallets stay saved here when you lock. Without a password anyone holding this device can open them again — set one and it will be asked for every time.',
    lockPwSkip: 'Not now',
    unlockSaved: 'Unlock saved wallets',
    unlockSavedDesc: 'Enter your password to open the wallets saved on this device.',
    unlock: 'Unlock',
    useAnotherWallet: 'Use another wallet',
    save: 'Save',
    publicKey: 'Public key',
    publicKeyDesc: 'Share this to watch this wallet read-only on another device or app. It reveals your addresses, not your keys.',
    copyKey: 'Copy key',
    showPublicKey: 'Show public key',
    createNew: 'Create new',
    importExisting: 'Import existing',
    genIntro: 'Generate a new 12-word seed phrase. Write it down. It’s 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',
    cancel: 'Cancel',
    paste: 'Paste',
    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',
    passphrase: 'Passphrase',
    show: 'Show',
    hide: 'Hide',
    invalidSeed: 'Invalid seed phrase — check the spelling and word order.',
    importLabel: 'Recovery phrase or key',
    importPlaceholder: 'Your seed words, or an xpub / xprv',
    invalidImport: 'That is not a valid recovery phrase, xpub, or xprv.',
    importedKey: 'Imported key',
    importedKeyNote: 'This wallet was imported from an extended private key, so there is no recovery phrase. Back up that key itself to keep the wallet.',
    keysNote: 'Keys never leave your browser. Works fully offline. Save this page and open it from your filesystem.',
    howItWorks: 'How it works',
    installApp: 'Install app',
    hiwBasicsTitle: 'The basics',
    hiwBasics1: 'When you make a wallet, the page generates twelve random words which are the keys to the wallet. Anyone who has them can spend the coins in the wallet. If you lose them, the coins are gone for good. Write them down on paper before you put money in.',
    hiwBasics2: 'There’s no server and no account. The whole app is one page that runs in your browser. You can save the page and use it offline.',
    hiwBasics3: 'It follows the standard Bitcoin formats, so the same twelve words will restore your wallet in Sparrow, Electrum, or any other standard wallet even if this site disappears.',
    hiwLnTitle: 'Spending and Savings',
    hiwLn1: 'Your money sits in two places. Savings is ordinary Bitcoin on the blockchain — safest for money you’re keeping, but each move costs a mining fee and waits for a confirmation. Spending sends and arrives instantly and free, and is still only yours: it runs on Ark, a newer layer on top of Bitcoin where every coin stays locked to your key, and it speaks Lightning, so you can pay and be paid by any bitcoin app or shop.',
    hiwLn2: 'Move money either way under your balance — a small fee each direction, free inside Spending. The wallet handles all the upkeep on its own (just open the app now and then), and every coin in Spending carries a pre-signed escape route: even if the coinos server vanished, you could pull your money back on-chain yourself.',
    hiwGiftsTitle: 'Gifts',
    hiwGifts1: 'You can gift bitcoin with a link: whoever opens it claims the coins into their own wallet — no app, no signup. On-chain gifts lock the coins until claimed and can be cancelled or reclaimed; Ark gifts claim instantly and free, even into a brand-new empty wallet.',
    moveMoney: 'Move money',
    moveToSpending: 'Into Spending',
    moveToSpendingDesc: 'Make money instantly spendable, with no fees to send it.',
    moveToSaving: 'Into Savings',
    moveToSavingDesc: 'Park money in ordinary on-chain Bitcoin.',
    moveToSavingBtn: 'Move to Savings',
    spendingLabel: 'Spending',
    savingLabel: 'Savings',
    balanceSwitch: 'Switch to {name}',
    movingLabel: 'Moving…',
    balance: 'Balance',
    pending: 'Pending',
    tabReceive: 'Receive',
    tabSend: 'Send',
    tabHistory: 'History',
    tabSettings: 'Settings',
    tabMessages: 'Chat',
    msgSend: 'Send',
    msgEmpty: 'No messages yet — say hi!',
    msgPlaceholder: 'Message #{channel}',
    msgPlaceholderPlain: 'Message…',
    msgMembers: '{n} members',
    msgEncrypted: 'End-to-end encrypted',
    msgAddPerson: 'Add person',
    msgNewChannel: 'New channel',
    msgAdmin: 'admin',
    msgEdited: '(edited)',
    msgDelete: 'Delete message',
    msgNoIdentity: "Your nostr signer isn't connected — reconnect it to send as yourself",
    msgSendFailed: 'No relay accepted the message',
    msgSignerOff: "Your nostr signer isn't connected, so you can't post as yourself yet.",
    msgReconnect: 'Reconnect',
    msgDmsTitle: 'Messages',
    msgCommunitiesTitle: 'Communities',
    msgNewDm: 'New message',
    msgNoDms: 'No conversations yet.',
    msgNoDmsYet: 'No messages yet — say hi!',
    msgDmPlaceholder: 'Message',
    msgDmEncrypted: 'Private message · end-to-end encrypted',
    msgYouPrefix: 'You:',
    msgOpen: 'Open',
    msgNpubPlaceholder: 'npub…',
    msgSearchPlaceholder: 'Name or npub…',
    zapNoBalance: "There's not enough in Spending to send a zap — move some money into Spending first.",
    msgPushOffer: 'Get notified when you receive payments and messages, even with coinos closed.',
    msgPushEnable: 'Enable notifications',
    msgPushOn: 'Notifications on',
    msgNotifyOn: 'Notifications on for this community — tap to mute',
    msgNotifyOff: 'Muted — tap to be notified about this community',
    msgOnline: 'online',
    msgNOnline: '{n} online',
    msgSeenJustNow: 'last seen just now',
    msgSeenMins: 'last seen {n} min ago',
    msgSeenHours: 'last seen {n}h ago',
    msgSeenDays: 'last seen {n}d ago',
    msgSeenRecently: 'last seen recently',
    msgTyping: '{name} is typing…',
    msgTyping2: '{a} and {b} are typing…',
    msgTypingMany: '{n} people are typing…',
    msgPushFailed: 'Could not enable notifications',
    msgChannelPlaceholder: 'New channel name',
    msgOwnerOnly: 'Only the community owner can do that',
    profEdit: 'Edit profile',
    profPay: '⚡ Pay',
    profName: 'Name',
    profAbout: 'About',
    profPicture: 'Picture URL',
    profSaved: 'Profile published',
    msgSearching: 'Searching…',
    msgNoMatches: 'No matches',
    msgBadNpub: 'Not a valid npub',
    msgSignerNoDm: 'This signer cannot encrypt DMs — log in again',
    msgJoin: 'Join',
    msgCreate: 'Create',
    msgInvitePlaceholder: 'Paste an invite link',
    msgNamePlaceholder: 'Community name',
    msgBadInvite: 'Not a valid invite link',
    msgInviteLoading: 'Fetching invite…',
    msgInviteNotFound: 'Invite not found on its relays',
    msgInviteRevoked: 'This invite has been revoked',
    msgInviteExpired: 'This invite has expired',
    msgInviteTo: 'You are invited to {name}',
    msgInviteFounder: 'Founded by {npub}',
    msgDirectInvite: '{from} invited you to {name}',
    msgDismiss: 'Dismiss',
    msgInviteTitle: 'Invite people',
    msgCopyInvite: 'Copy invite link',
    msgLinkCopied: 'Invite link copied',
    msgInviteSent: 'Invite sent',
    msgLeave: 'Leave community',
    msgLeaveConfirm: 'Really leave? Rejoining needs a new invite',
    nostrSettings: 'Nostr',
    settingsWallet: 'Wallet',
    settingsPayments: 'Payments',
    settingsNetwork: 'Network',
    advancedSettings: 'Advanced',
    recoveryDesc: 'The twelve words that back up this wallet. Anyone who sees them can steal your funds.',
    receiveOfflineSpending: "You're offline — your payment address needs the network. You can still receive to Savings with an on-chain address.",
    receiveUseSavings: 'Switch to Savings',
    arkOffboardAmtPlaceholder: 'Amount',
    arkSplitting: 'Carving out that exact amount — this can take a minute…',
    arkSplitTimeout: 'The split is taking longer than expected — your balance is intact; try again shortly.',
    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.',
    unconfirmedInputWarn: 'This payment spends bitcoin that hasn’t confirmed yet, so it won’t confirm until that earlier transaction does.',
    signBroadcast: 'Sign & broadcast',
    signTx: 'Sign transaction',
    signExport: 'Sign & export (broadcast elsewhere)',
    scanQr: 'Scan QR',
    scanHint: 'Point the camera at a QR code',
    scanNoCamera: 'Could not access the camera.',
    scanNoDecoder: 'QR scanning needs a connection the first time it’s used.',
    scanUnrecognized: 'Couldn’t read a Bitcoin address from that QR.',
    broadcastScanned: 'Broadcast transaction?',
    broadcastNow: 'Broadcast',
    bumpFee: 'Bump fee',
    bumpConfirm: 'Replace with a higher fee?',
    replaceTx: 'Replace',
    bumpInsufficient: 'Not enough funds to bump at this rate. Try a lower rate.',
    scanOffline: 'You’re offline — reconnect to broadcast this transaction.',
    broadcastFailed: 'Broadcast failed: {msg}',
    broadcastTimeout: 'Timed out reaching the network. The payment was not sent — try again.',
    signingFailed: 'Signing failed: {msg}',
    done: 'Done',
    txSignedNote: 'Signed, but not sent yet. To actually send it, broadcast the code below from any online device (e.g. mempool.space → Broadcast). It will not send on its own.',
    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: 'Block explorer ↗',
    noTxYet: 'No transactions yet.',
    loadingHistory: 'Loading transactions…',
    received: 'Received',
    sent: 'Sent',
    pendingTag: '⏳ pending',
    stuckTag: '⚠ stuck',
    stuckNote: 'Fee too low — may not confirm',
    stuckIncomingNote: '⚠ This payment’s fee is below the current network floor, so it may not confirm. Only the sender can speed it up (bump or re-send).',
    stuckOutgoingNote: '⚠ This transaction’s fee is below the current network floor, so it may not confirm on its own. Bump the fee below to rebroadcast it at a higher rate.',
    awaitingConfirmation: 'awaiting confirmation',
    feeShort: 'fee {x}',
    historyOffline: 'Transaction history is unavailable in offline mode.',
    status: 'Status',
    confirmed: 'Confirmed',
    confirmedTag: 'confirmed',
    block: 'Block',
    date: 'Date',
    copyId: 'Copy ID',
    backToHistory: '← Back to history',
    recoveryPhrase: 'Recovery phrase',
    giftWelcome: 'You’ve been sent Bitcoin',
    giftBackedUp: 'I’ve written it down',
    giftLink: 'Gift some Bitcoin',
    giftLinkDesc: 'Presign an amount as a link. Whoever opens it claims the Bitcoin into a brand-new wallet that only they control.',
    giftLinkWarn: '⚠ Anyone who opens this link can claim the amount, so share it only with the person you are gifting. Keep amounts small.',
    giftLinkReveal: 'Create gift link',
    giftAmountLabel: 'Amount to gift',
    giftAmountInvalid: 'Enter at least {n}.',
    giftMinNote: 'Minimum {n}.',
    giftAllNote: 'Gifts your whole balance; the recipient pays the claim fee.',
    giftExceedsBalance: 'That’s more than your balance.',
    giftNeedsHeadroom: 'Gift up to {n}, or tap Max to gift your whole balance.',
    giftAnother: 'Create another',
    giftForYou: 'A gift for you',
    claimGift: 'Claim',
    dismiss: 'Dismiss',
    giftCopyCode: 'Copy claim code',
    giftQrTooLong: 'Too large for a QR code — share the link below instead.',
    giftConsolidateExplain: 'This gift would bundle {count} coins, making it costly for the recipient to claim. Splitting one coin first (~{fee}) gives them a clean single-input gift.',
    giftConsolidateFirst: 'Split into one coin first',
    giftUseManyCoins: 'Use {count} coins anyway',
    giftSplitExplain: 'This gift would lock {lock} until it’s claimed ({change} of it comes back to you then). You can split a right-sized coin first for about {fee} so only the gift amount stays locked.',
    giftSplitFirst: 'Split a coin first',
    giftLockWhole: 'Lock the whole coin ({n})',
    giftSplitFailed: 'Could not split the coin.',
    giftChecking: 'Opening your gift…',
    giftTakenTitle: 'Already claimed',
    giftTakenBody: 'Sorry, this gift has already been claimed.',
    createWalletAnyway: 'Create a wallet anyway',
    goToHome: 'Go to home',
    newWalletTitle: 'Your new wallet',
    newWalletBody: 'Back up your recovery phrase to keep it safe.',
    giftHistoryTitle: 'Gift link',
    giftUnclaimedTag: 'unclaimed',
    giftClaimedTag: 'claimed',
    giftView: 'Gift link',
    giftCodeLabel: 'Claim code (sent via DM)',
    giftCancel: 'Cancel',
    viewAllGifts: 'View all gifts ({n})',
    prevPage: '‹ Prev',
    nextPage: 'Next ›',
    pageXofY: 'Page {x} of {y}',
    lockedInGifts: 'Locked in gifts',
    giftReserved: 'Unclaimed gift links ({n})',
    giftReclaim: 'Reclaim',
    giftReclaimPrompt: 'Reclaim frees this coin for your next payment, but the link stays claimable until you spend it. Pay a network fee to revoke it right now instead?',
    giftReclaimed: 'Coin reclaimed',
    giftReclaimedTag: 'still claimable',
    giftRevoke: 'Revoke now',
    giftRevokeConfirm: 'Revoke this gift now? This spends the coin back into your wallet and disables the link.',
    giftRevoked: 'Gift revoked',
    copyLink: 'Copy link',
    claimBody: 'Claim it into a brand-new wallet that only you control.',
    claimBodyExisting: 'Claim it into your wallet \u201c{name}\u201d.',
    claimIntoPrompt: 'Choose where to receive it.',
    claimNewWallet: 'Receive into a new wallet',
    giftClaimedContext: 'This transaction delivered your gift',
    claimIntoExisting: 'Receive into one of your wallets:',
    claimOr: 'or',
    goToWallet: 'Go to wallet',
    claimBtn: 'Claim it',
    claimFeeNote: '~{n} fee',
    claimFailed: 'This gift has already been claimed or is no longer available.',
    claimNotVisibleBody: 'The coins funding this gift aren\u2019t visible from this wallet\u2019s network/data source. Make sure this wallet uses the same network and data source as the sender, then try again.',
    claimedTitle: 'Claimed!',
    claimedBody: 'Your Bitcoin is on its way. Back up your recovery phrase to keep it safe.',
    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',
    revealWords: 'Show the words',
    offlineTransfer: 'Offline transfer',
    offlineTransferDesc: 'Export your coins on an online device, then import the file on an offline device to sign without internet.',
    exportSnapshot: '⤓ Export snapshot',
    importSnapshot: 'Import snapshot…',
    rescan: 'Rescan',
    rescanDesc: 'Your current address updates automatically. Rescan an individual address only if a payment was sent to an old (reused) one and isn’t showing.',
    rescanWallet: 'Rescan wallet',
    rescanAddresses: 'Rescan an address',
    rescanAddrDesc: 'Tap Rescan next to the address a payment was sent to. Only that address is re-checked.',
    rescanOne: 'Rescan',
    rescanDone: 'Address rescanned',
    rescanFailed: 'Rescan failed',
    unusedTag: 'unused',
    scanning: 'Scanning…',
    explorer: 'Block explorer',
    explorerDesc: 'The server this wallet queries for balances and history. Point it at your own node for maximum privacy.',
    explorerUrl: 'Server URL',
    explorerUrlHint: 'Esplora / electrs REST base, e.g. https://mempool.space/api',
    walletName: 'Wallet name',
    walletSettings: 'Wallet settings',
    walletSettingsDesc: 'Name, recovery phrase, public key, and auto-logout for this wallet.',
    autolockTitle: 'Log out automatically',
    autolockDesc: "After the app has been in the background this long, log this wallet out — it's removed from this device (including from Save to device), so keep its recovery phrase. Other wallets aren't affected. The timer never runs while the app is open and focused.",
    autolockNever: 'Never',
    autolock1m: 'After 1 minute',
    autolock5m: 'After 5 minutes',
    autolock1h: 'After 1 hour',
    autolock1d: 'After 1 day',
    dataSource: 'Data source',
    dataSourceDesc: 'Where Coinos reads the chain and how it learns about incoming payments.',
    modeCoinos: 'coinos (instant)',
    modeCoinosDesc: "The default. Chain data and instant payments come from coinos's own Bitcoin node, with public servers as automatic backup.",
    modeExplorer: 'Block explorer',
    modeExplorerDesc: 'Reads from your chosen block explorer and polls for payments. A bit slower.',
    modeElectrum: 'Electrum server',
    backendElectrumDesc: 'Data and instant payments over one Electrum WebSocket. Point it at your own server for a fully self-hosted wallet.',
    electrumUrl: 'Electrum server (wss://)',
    electrumUrlHint: 'A WebSocket Electrum server, e.g. wss://your-node:50004',
    sourceUrl: 'Server URL',
    sourceUrlHint: 'wss:// for an Electrum server, or an Esplora REST base (https://…/api). The type is detected from the URL.',
    detectedElectrum: 'Electrum — data + instant payment notifications.',
    detectedEsplora: 'Block explorer (Esplora) — data only; polls for payments.',
    silentPaymentNote: 'Silent payment',
    receiveAddressTab: 'Savings',
    receiveLnTab: 'Lightning',
    receiveSpTab: 'Silent payment',
    arkReceiveIntro: 'Receive instantly and free — straight into Spending.',
    arkTitle: 'Spending balance',
    arkDesc: 'Your Spending balance sends and receives instantly with no mining fees, and stays yours — you can move it back to Savings whenever you like.',
    arkServerUrl: 'Server URL',
    arkEsploraUrl: 'Esplora URL (chain source)',
    arkWatchOnly: 'Spending needs the wallet keys — load the seed to use it.',
    arkConnecting: 'Connecting…',
    arkConnectBtn: 'Connect',
    arkNotConnected: 'Not connected — pick a server in Settings.',
    arkBalance: 'Spending',
    arkBoarding: 'Moving…',
    arkBoardHistory: 'Moved to Spending',
    arkVtxos: 'Coins (VTXOs)',
    arkPendingActions: '{n} operation(s) in progress',
    arkBoardBtn: 'Move to Spending',
    arkBoardFeeNote: 'Fee {fee} sats — {net} sats will arrive in Spending.',
    arkBoardFeeLabel: 'Fee',
    arkBoardedTitle: 'On its way!',
    arkBoardedNote: 'Ready to spend after 1 confirmation.',
    arkBoardIntro: 'Move money from Savings into Spending:',
    arkBoardAvailable: '{n} in Savings',
    arkBoardNoFunds: 'Needs at least {n} sats in Savings.',
    arkRefreshBtn: 'Refresh / consolidate {n} coin(s)',
    arkOffboardBtn: 'Move {n} to Savings',
    giftSourceChain: 'On-chain',
    giftSourceArk: 'Spending',
    giftArkNote: 'Sent from Spending — the link claims instantly, with no fee.',
    giftArkOutstanding: '{n} unclaimed instant gift(s)',
    giftArkRevoked: 'Gift swept back into Spending.',
    claimArkFree: 'instant, no fee',
    arkGiftWrongNet: 'This gift lives on {net} — switch the network in Settings, then open the link again.',
    arkOffboardedTitle: 'On its way!',
    arkOffboardedNote: 'Arrives in Savings after 1 confirmation.',
    arkOffboarded: 'Moved to Savings',
    arkOffboardHistory: 'Moved to Savings',
    arkExitBtn: 'Move to Savings without the server',
    arkExitStarted: 'Unilateral exit started for {n} coin(s) — broadcasting the exit chain.',
    arkExitNoFeeCoin: 'This needs a little in Savings to pay the mining fees.',
    arkExitChainStatus: 'Moving {n} sats — step {done} of {total}…',
    arkExitTimelockStatus: 'Exiting {n} sats — {blocks} block(s) of timelock left{eta}',
    arkExitClaimingStatus: 'Exiting {n} sats — almost done, awaiting confirmation',
    arkExitRetrying: 'hit a snag — retrying automatically',
    arkExitCancel: 'Cancel this exit',
    arkExitLink: 'Move to Savings',
    arkExitPageTitle: 'Move money to Savings',
    arkExitPageIntro: 'Two ways to move your Spending balance into Savings.',
    arkCoopTitle: 'Cooperative (recommended)',
    arkCoopDesc: 'The normal way: one transaction into Savings, ready after a single confirmation, with a small fee taken from the amount.',
    arkUniTitle: 'Unilateral exit',
    arkUniDesc: 'Your escape hatch: claim the money yourself without anyone’s help. Takes about a day and costs mining fees, but works even if the server disappears.',
    arkExitNoBalance: 'Nothing in Spending to move.',
    arkZapTitle: 'Send a zap',
    awTitle: 'Auto-withdraw',
    awDesc: 'When Spending goes over an amount you choose, send it onward automatically — to an address, a payment name, a lightning address, or an npub.',
    awDest: 'Send to',
    awDestPh: 'name@domain, npub, bc1…, ark1…',
    awThreshold: 'When Spending reaches (sats)',
    awKeep: 'Keep behind (sats)',
    awTurnOn: 'Turn on auto-withdraw',
    awTurnOff: 'Turn off',
    awOn: 'Auto-withdraw is on',
    awOff: 'Auto-withdraw is off',
    awBadDest: "That destination isn't one we can pay",
    awLowThreshold: 'Use a threshold of at least 1,000 sats',
    awKeepTooHigh: 'Keep less behind than the threshold',
    awOutOfRange: "The amount is outside the destination's limits",
    awSent: 'Auto-withdrew {n}',
    awLast: 'Last sent {n} · {when}',
    awLastError: 'Last attempt failed: {why}',
    zapTitle: 'Send a zap',
    zapFinding: 'Finding the best way to pay them…',
    arkZapLookup: 'Looking them up…',
    arkZapNoArk: 'No way to pay this account was found — no Ark address, Lightning address, or payment record.',
    arkZapHint: 'Instant and free, with a zap receipt on nostr.',
    arkZapCommentPh: 'Comment (optional)',
    arkZapBtn: 'Zap',
    arkZapSentTitle: 'Zapped!',
    arkOpsBoard: '{n} deposit(s) confirming',
    arkOpsSend: '{n} send(s) finishing up',
    arkOpsRefresh: '{n} refresh(es) waiting for the next round',
    arkOpsOffboard: '{n} withdrawal(s) confirming',
    arkOpsLnPay: '{n} Lightning payment(s) in flight',
    arkOpsLnRecv: '{n} Lightning invoice(s) outstanding',
    arkLnPayVia: 'Instant, from your Spending balance',
    arkLnFee: 'Fee',
    arkLnPaying: 'Paying invoice…',
    arkLnPayFailed: 'Payment failed',
    arkLnRefunded: 'Payment failed — your money is back in Spending.',
    arkLnPaidTitle: 'Paid!',
    or: 'or',
    nlSignIn: 'Sign in with Nostr',
    onbWelcomeTitle: 'Welcome to coinos',
    onbStart: 'Get started',
    onbHaveWallet: 'I already have a wallet',
    onbSigninTitle: 'Sign in with Nostr',
    onbLegacyTitle: 'Move your coinos.io account',
    onbLegacyBody: 'The original coinos. We can move your balance over and point your username here — your old account keeps working, it just stops receiving payments.',
    onbLegacyGo: 'Move my coinos.io account',
    onbLegacyNoAddr: "Your payment address isn't ready yet — try again in a moment.",
    onbLegacyDone: "I'm done — continue",
    onbHaveCoinos: 'Migrate an existing coinos account',
    migrateClaimed: 'Welcome back — {name} is yours here now.',
    migrateClaimFailed: "Your balance moved, but {name} isn't free yet. Try claiming it in a minute.",
    migrateWorking: 'Claiming {name}…',
    onbNameTitle: 'Claim your username',
    onbNameBody: 'Your payment address — people can send you bitcoin with just this.',
    onbNameWait: 'Getting things ready…',
    onbSkipName: 'Skip for now',
    onbAvatarTitle: 'Choose your look',
    onbAvatarBody: 'Pick a punk or upload your own picture. You can change it anytime.',
    onbUpload: 'Upload a photo',
    onbUploadFailed: 'Upload failed — try a punk for now',
    onbContinue: 'Continue',
    onbSkipAvatar: 'Continue with my default',
    onbDoneTitle: "You're all set!",
    onbDoneBody: 'Your wallet is ready, your address is live, and the coinos community chat is waiting for you.',
    onbEnter: 'Enter coinos',
    tourAvatar: 'Your avatar lives here — profile, messages, settings, and logout are all behind it.',
    tourBalance: 'Two balances: Spending is instant and free, Savings is on-chain. Tap to switch.',
    tourTabs: 'Receive, send, and your history. Tip: swipe left and right to move between them.',
    tourNext: 'Next',
    tourReplayTitle: 'Tour',
    tourReplayDesc: 'A quick walk through the home screen — where your profile, balances, and tabs live.',
    tourReplay: 'Take the tour',
    tourSkip: 'Skip',
    nlConnectApp: '📱 Connect a signer app',
    nlOpenSigner: 'Open signer app',
    nlWaitingSigner: 'Waiting for your signer to connect…',
    nlTitle: 'Log in with Nostr',
    nlDesc: 'Use your Nostr account to open your wallet — on any device, with the same account.',
    nlExtension: 'Use browser extension',
    nlKeyOrBunker: 'nsec, private key, or bunker://…',
    nlGo: 'Log in',
    nlHint: 'Works with Amber, Amethyst and other remote signers (paste their bunker:// address), or a browser extension. A pasted key is used once and never stored.',
    nlApprove: 'Approve the request in your signer →',
    nlUnrecognized: 'That doesn’t look like a private key or a bunker:// address.',
    nlOpenedDerived: 'Wallet opened from your Nostr key',
    nlOpenedRestored: 'Your wallet was restored',
    nlNewTitle: 'Create a wallet for this account',
    nlNewDesc: 'This Nostr account has no wallet yet. We’ll make one and save it, encrypted to your account, so you can open it anywhere you use this account.',
    nlBackupWarning: 'Your wallet will be recoverable by anyone who controls this Nostr account — treat that account like a key to your money.',
    nlCreateBtn: 'Create my wallet',
    nlCreated: 'Wallet created and linked',
    nlLinkTitle: 'Nostr account',
    nlLinkDesc: 'Link this wallet to your Nostr account so you can open it on other devices by logging in with Nostr.',
    nlLinkedDesc: 'This wallet opens when you log in with this Nostr account.',
    nlConnected: 'connected',
    nlDisconnected: 'not connected',
    nlReconnectDesc: "Your signer isn't connected, so this app can't sign as you. Reconnect it to post and update your profile — your wallet and balance aren't affected.",
    nlWrongAccount: 'That signer is a different Nostr account. Reconnect with {npub}.',
    nlLinked: 'Wallet linked to your Nostr account',
    nlNeedSeed: 'This wallet has no recovery phrase loaded.',
    nlAlreadyLinked: 'That Nostr account already has a different wallet.',
    namesAutoFailed: 'Couldn’t set your address up automatically ({why}). Pick one below.',
    namesSettingUp: 'Setting up your payment address…',
    namesTitle: 'Payment address',
    receiveNameTab: 'Spending',
    namesCustom: 'Choose a custom name',
    namesCustomHow: 'Swap the generated username for one you like. Your old address stops working.',
    namesDesc: 'Claim your payment address — like [email protected]. People can pay you just by typing it, no codes to copy or QR to scan.',
    namesPlaceholder: 'username',
    namesClaim: 'Claim',
    namesClaimed: 'You are now {name}',
    namesCopy: 'Copy address',
    namesOfferOpen: 'Need a Lightning offer (BOLT 12)?',
    namesOfferTitle: 'Lightning offer',
    namesOfferMemoPh: 'Label (e.g. Ocean payout)',
    namesOfferCreate: 'Create',
    namesOfferNoMemo: 'No label',
    namesOfferQr: 'QR',
    namesRelease: 'Release',
    namesReleased: 'Name released',
    namesNeedArk: 'Your wallet is still connecting — try again in a moment',
    namesNotFound: 'No payment record found for {name}',
    namesNoUsableInstruction: 'That address exists, but this wallet has no way to pay it',
    namesZapCode: 'Get zapped on Nostr',
    namesZapCodeHow: 'Paste this code into your Nostr app profile and zaps land straight in this wallet:',
    namesZapCodeCopy: 'Copy code',
    namesOwnDomain: 'Use your own domain',
    namesOwnDomainHow: 'Have your own domain? Add this line to its DNS records (DNSSEC must be enabled) and your address works there too:',
    nwcTitle: 'Nostr Wallet Connect',
    nwcDesc: 'Let another app pay from this wallet over nostr — it never sees your keys, only spends within the limits you set, and you can cut it off at any time.',
    nwcTabWarning: 'Works while Coinos is open — turn on background answering below to keep working when it isn’t.',
    nwcAdd: 'Add a connection',
    nwcBackground: 'Answer while closed',
    nwcOn: 'On',
    nwcOff: 'Off',
    nwcBackgroundOn: 'Requests will be answered in the background',
    nwcBackgroundOff: 'Background answering turned off',
    nwcAppName: 'App name (e.g. Damus)',
    nwcMaxPayment: 'Max per payment (sats)',
    nwcDailyBudget: 'Daily budget (sats)',
    nwcCreate: 'Create connection',
    nwcShow: 'Show connection string',
    nwcCopy: 'Copy connection string',
    nwcRevoke: 'Revoke',
    nwcRevoked: 'Revoked {name}',
    nwcLimits: 'Max {max}/payment · {day}/day · {spent} spent today',
    arkLnFundsPending: 'Your money is settling from a previous payment — retrying…',
    lnZapOffer: 'Zap {addr}',
    arkLnInvoiceIntro: 'Or take a Lightning payment, straight into Spending.',
    arkLnPaidHistory: 'Sent',
    arkLnReceivedHistory: 'Received',
    arkLnCancelInvoice: 'Cancel invoice',
    arkExited: 'Moved to Savings',
    arkExitHistory: 'Moved to Savings',
    arkRefreshStarted: 'Refresh submitted — completes with the next round.',
    arkSendTitle: 'Send',
    arkSendHint: 'Ark address — sent instantly off-chain, no mining fee.',
    arkPayTo: 'To',
    arkNoFee: 'none — instant off-chain send',
    arkSendBtn: 'Send',
    arkSentTitle: 'Sent!',
    arkBoarded: 'Boarded to Ark',
    dateLabel: 'Date',
    detailsLabel: 'Details',
    arkVtxoId: 'Coin (VTXO)',
    lnReceiveIntro: 'Receive over Lightning — paid into this wallet on-chain, non-custodial.',
    lnReceiveAwaiting: 'Share this invoice. When it’s paid, the sats land here automatically.',
    lnReceiveWatching: 'Waiting for payment…',
    lnReceived: 'Received!',
    lnCreateInvoice: 'Create invoice',
    copyInvoice: 'Copy invoice',
    amountSatsLabel: 'Amount (sats)',
    destPlaceholder: 'Address or Lightning invoice',
    lnQuoting: 'Getting a quote…',
    lnPayTitle: 'Pay over Lightning',
    lnPayAmount: 'Amount',
    lnPayBoltzFee: 'Swap fee',
    lnPayNetworkFee: 'Network fee',
    lnPayTotal: 'Total',
    lnPayConfirm: 'Pay',
    lnPaySentTitle: 'Lightning payment sent',
    lnPaySentBody: 'The swap provider pays the invoice once the on-chain lockup confirms.',
    lnZapTitle: 'Zap over Lightning',
    lnZapReviewTitle: 'Zap over Lightning',
    lnZapFallback: 'Zap over Lightning instead',
    lnZapResolving: 'Finding their Lightning address…',
    lnZapRequesting: 'Requesting a Lightning invoice…',
    lnZapNoAddress: 'This nostr user hasn’t published a Lightning address.',
    lnZapRange: 'Enter {min}–{max} sats',
    lnZapHintNostr: 'Sent over Lightning, with a public zap receipt on nostr.',
    lnZapHintPlain: 'Sent over Lightning (this address doesn’t support zap receipts).',
    lnZapSentBody: 'Your zap is on its way — the swap provider pays it once the on-chain lockup confirms.',
    lnZapFailed: 'Couldn’t complete the zap.',
    language: 'Language',
    darkMode: 'Dark mode',
    lightMode: 'Light mode',
    offlineBanner: "Can't reach the network — working offline. Import a snapshot in Settings (under Advanced) 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',
  },
};

// --- locale loading -------------------------------------------------------
// Only English is bundled; other languages are fetched on demand from
// locales/<code>.json (emitted by the build) and cached, so each visitor only
// downloads the language they use. t() falls back to English until it loads.
const _inflight = new Map();
export function loadLocale(code) {
  if (code === 'en' || STR[code] || !SUPPORTED.has(code)) return Promise.resolve();
  if (_inflight.has(code)) return _inflight.get(code);
  const p = fetch('locales/' + code + '.json')
    .then((r) => (r.ok ? r.json() : null))
    .then((d) => { if (d) STR[code] = d; })
    .catch(() => {})
    .finally(() => _inflight.delete(code));
  _inflight.set(code, p);
  return p;
}

// --- runtime --------------------------------------------------------------
const SUPPORTED = new Set(LANGS.map(([c]) => c));
const LANG_KEY = 'btc-wallet-lang';

function detect() {
  try {
    const q = new URLSearchParams(location.search).get('lang');
    if (q && SUPPORTED.has(q)) return q;
  } catch {}
  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;
}