aboutsummaryrefslogtreecommitdiffstats
path: root/src/i18n.js
blob: 2d2c9764a7518517745389c66cb1191a0cf2c382 (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
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
import { assetUrl } from './asset-url.js';
// 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: {
    arkHistoryIncomplete: 'History incomplete',
    arkDetailsNotSynced: 'Payment details not synced',
    arkHistoryIncompleteHelp: 'These coins have been used, but the payment details and any remaining change have not synced to this device. Open the wallet that made the payments and let it sync.',
    arkOriginalCoins: 'Original coin total',
    arkDetectedAt: 'Detected on this device',
    appTitle: 'coinos',
    home: 'Home',
    lockWallet: 'Lock wallet',
    accounts: 'Accounts',
    addWallet: 'Add account',
    signInAnother: 'Sign into another account',
    signInAnotherNote: 'Add a different identity (Nostr, passkey or Google) without signing out. Switch between them here anytime.',
    switchIdentityTitle: 'Switch account',
    switchIdentityDesc: 'Accounts already signed in on this device. Tap one to switch. Nothing is logged out.',
    signInNew: 'Sign into a new account',
    identityNoKey: 'Not opened on this device yet',
    clearAll: 'Delete all',
    logoutForget: 'Logout & forget all data',
    entropyToggle: 'Entropy',
    entropySubmit: 'Submit',
    entropyHint: 'The seed comes from exactly what you type. Entering the same text again recreates the same wallet, on any device.',
    entropyWarn: '⚠ Anyone who can guess this text can take your money. Use plenty of real-world randomness: 100+ dice rolls, or the order of a fully shuffled deck of cards.',
    entropyPlaceholder: '4 2 6 6 1 3 5 2 …',
    logoutPopBlurb: 'Your accounts stay saved on this device.',
    logoutOne: 'Log out of {name}',
    logoutOneBlurbLogin: 'Only this account leaves. The others stay signed in. You can sign back in with the same key anytime.',
    logoutOneBlurbSeed: 'Only this account leaves. The others stay signed in. It is removed from this device, so make sure you have its recovery phrase.',
    logoutAll: 'Log out of all accounts',
    clearAllWarn: '⚠ This removes every account from this device. Any account whose recovery phrase or key you have not backed up will be lost for good.',
    removeWalletTitle: 'Remove account',
    removeWalletWarn: '⚠ Remove “{name}” from this device? It will be gone from this browser. Make sure you have its recovery phrase. Other accounts 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: 'Account {n}',
    watchLabelN: 'Watch {n}',
    saveDevice: 'Save on this device',
    forgetDevice: 'Saved · forget',
    savedTag: 'saved',
    setPassword: 'Set a password (optional)',
    setPasswordDesc: 'Encrypts this account on this device. Leave it blank to save without a password. You will need it to unlock saved accounts later.',
    passwordOptional: 'Password (optional)',
    changePassword: 'Change password',
    changePasswordDesc: 'Re-encrypt your saved-to-device accounts 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 accounts to continue.',
    profUsername: 'Username',
    profUploadPic: 'Upload photo',
    profDisplayName: 'Display name',
    profPublicKey: 'Nostr public key',
    profKeyLink: 'Public key',
    followHistTitle: 'Follow list',
    followHistDesc: 'You follow {n} people. If another app replaced your follow list, an older copy may still be on a relay that keeps history. Restoring adds everyone from it back; nobody is removed.',
    followHistLook: 'Look for older copies',
    followHistPeople: '{n} people',
    followHistRestore: 'Restore {n}',
    followHistNone: 'No older copy has anyone your list is missing.',
    relaysTitle: "Relays",
    relaysDesc: "Where your posts go and where other apps look for you. The lists are signed with your key, so every Nostr app you use follows them.",
    relaysPublic: "Posts and profile. Read: where replies and mentions reach you. Write: where your posts are published.",
    relaysInbox: "Direct message inbox: where people send you private messages.",
    relaysRead: "Read",
    relaysWrite: "Write",
    relaysNoneYet: "Not published yet. These are the defaults; save to publish them.",
    relaysDown: "unreachable",
    relaysBadUrl: "That isn’t a relay address (wss://…).",
    relaysNeedBoth: "Keep at least one read relay and one write relay.",
    relaysNeedDm: "Keep at least one inbox relay.",
    relaysUnchanged: "Nothing changed.",
    relaysWrongIdentity: "Your signer belongs to a different account. Switch to it and try again.",
    relaysChangedElsewhere: "Another app just updated your relay lists. They’ve been reloaded; make your changes again.",
    relaysSaved: "Relay lists saved",
    relaysSave: "Save",
    relaysSaving: "Saving…",
    relaysReload: "Reload",
    followHistNothing: 'Everyone in that copy is already in your list.',
    followHistRestored: 'Restored {n} follows.',
    profFollowing: 'Following',
    profFollowers: 'Followers',
    profFollowsNone: 'Not following anyone yet.',
    profFollowersNone: 'No followers found.',
    profFollowersPartial: 'Showing {n} of {total} followers.',
    showMore: 'Show more',
    profFetchFailed: 'Couldn’t load the latest profile.',
    profNotFound: 'No profile details found on the available relays.',
    profUnameInvalid: 'Usernames can only use letters, numbers, dots, dashes and underscores.',
    lockSaveDesc: 'The accounts already saved on this device are protected by a password. Enter it to save this account with them and lock.',
    lockAction: 'Lock',
    password: 'Password',
    confirmPassword: 'Confirm password',
    pwMismatch: 'Passwords do not match.',
    pwWrong: 'Wrong password.',
    pwNeededToLock: 'Enter a password, or choose "Not now".',
    lockPwTitle: 'Protect this device?',
    lockPwDesc: 'Set a password to lock your wallet. Anyone using this device will need it to spend your money.',
    lockPwSkip: 'Not now',
    unlockSaved: 'Unlock saved accounts',
    unlockSavedDesc: 'Enter your password to open the accounts saved on this device.',
    unlock: 'Unlock',
    useAnotherWallet: 'Use another account',
    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',
    passWhatsThis: 'What’s this?',
    passInfo: 'An optional 13th word: if someone gets hold of your 12 seed words, they still can’t open your wallet without this one. It’s not a login password and it doesn’t encrypt anything: each passphrase opens its own separate wallet, so to recover your funds you’ll need your seed words and this passphrase, exactly as written.',
    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: the keys to your coins. Anyone who has them can spend; if you lose them, the coins are gone for good. Write them down on paper before you put money in.',
    hiwBasics2: 'Your keys never leave your device. Coinos runs servers that ferry payments and messages, but they can’t touch your money. The app itself is one page. You can save it and open it offline.',
    hiwBasics3: 'It uses standard Bitcoin formats: the same twelve words restore your Savings in Sparrow, Electrum, or any other standard wallet, even if this site disappears.',
    hiwLnTitle: 'Spending and Savings',
    hiwLn1: 'Your money can sit in two accounts. 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, free or nearly 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.',
    hiwGiftsTitle: 'Gifts',
    hiwGifts1: 'You can gift bitcoin with a link: whoever opens it claims the coins into their own wallet, with no app and no signup, and it works even for someone brand new to bitcoin. Until a gift is claimed, you can cancel it and take the coins back.',
    hiwStandaloneTitle: 'Keep a copy',
    hiwStandalone1: 'The whole wallet fits in one file. Download it, keep it on a hard drive or USB stick, and open it in any browser. It runs from there with no server and no install, and your twelve words bring your money with you.',
    hiwStandaloneLink: 'Download the standalone 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',
    msgAttachFailed: 'Attachment unavailable',
    msgAttach: 'Attach a picture',
    msgAttachTooBig: 'That file is too big (20 MB max)',
    msgUploadFailed: 'Upload failed. Check your connection and try again',
    mediaUploadTooBig: 'That file is too big for the available media hosts',
    mediaUploadUnsupported: 'That file format is not supported',
    msgPhoto: 'Photo',
    searchPeopleFor: 'Search people for {q}',
    msgEmojiSearch: 'Search emoji…',
    msgEmojiMore: 'More reactions',
    msgEmojiNone: 'No match',
    msgEmojiAddPack: 'Add an emoji pack',
    msgEmojiPackHint: 'Paste a pack link or naddr…',
    msgEmojiPackAdd: 'Add',
    msgEmojiPackHave: 'Added',
    msgEmojiPackRemove: 'Remove this pack',
    msgEmojiPackShare: 'Copy a link to this pack',
    msgEmojiPackAdded: 'Emoji pack added: {name}',
    msgEmojiPackBad: "Couldn't find an emoji pack there",
    msgEmojiPackLink: 'Emoji pack',
    msgEdited: '(edited)',
    msgReact: 'React',
    msgZap: 'Zap',
    zapTallyTitle: '{n} sats zapped',
    close: 'Close',
    playVideo: 'Play',
    videoUnmute: 'Unmute',
    videoTapToPlay: "Tap to play",
    videoNoPicture: "This browser can’t show this video’s picture",
    videoOpenFile: "Open the file",
    videoLargeTitle: "Videos",
    videoLargeToggle: "Autoplay large videos",
    videoLargeHelp: "Videos over {mb} MB or above 1080p wait for a tap, so the feed doesn’t eat your data. The limit is lower on a slow or data-saving connection.",
    videoMute: 'Mute',
    openInYouTube: 'Open on YouTube',
    accountsThisOne: 'signed in',
    worthThen: 'Worth then',
    worthNow: 'Worth now',
    worthNowInline: '{v} now',
    currencyLabel: 'Show amounts in',
    feedTitle: 'Feed',
    feedFollowingName: 'Following',
    feedNew: 'New feed',
    feedNewName: 'New feed',
    feedEdit: 'Edit feed',
    feedName: 'Name',
    feedNameHint: 'e.g. Bitcoin builders',
    feedPeople: 'People',
    feedFollowsToggle: 'Everyone I follow',
    feedListsToggle: 'Everyone on my lists and packs',
    feedInterestsToggle: 'Posts from anyone on hashtags I follow',
    feedInterestsHelp: 'Your topic feeds\' hashtags and the interests on your nostr profile.',
    feedAddPerson: 'Add someone…',
    feedTopics: 'Topics',
    feedTopicsHint: '#bitcoin #nostr',
    feedTopicsHelp: 'Posts tagged with any of these. With people set as well, only their posts on these topics.',
    feedDelete: 'Delete feed',
    feedSaveAdhoc: 'Save feed',
    feedSaved: 'Feed saved',
    feedPostFab: 'Post',
    mediaShrinking: 'Shrinking video for streaming',
    mediaUploading: 'Uploading',
    feedNoQuery: 'Add people or topics to this feed.',
    feedOfTitle: '{name}’s timeline',
    feedOfLoading: 'Reading who they follow…',
    feedNoFollowsOf: 'They follow nobody yet.',
    feedShare: 'Share this feed',
    feedMenu: 'Feed options',
    feedSharedName: 'Shared feed',
    feedPackName: 'Follow pack',
    profTheirFeed: 'Their timeline',
    feedFirehose: 'Everything',
    feedPopular: 'Popular',
    askSignInTitle: 'Sign in to join in',
    askSignInBody: 'Reading is free. To zap, reply, react or follow, sign in or create an account.',
    askSignInCreate: 'Create a new account',
    askNotNow: 'Not now',
    feedLangLabel: 'Language',
    mediaAll: "All",
    mediaImages: "Images",
    mediaVideos: "Videos",
    mediaArticles: "Articles",
    mediaNoArticles: "No articles in this feed yet.",
    mediaNoImages: "No pictures in this feed yet.",
    mediaNoVideos: "No vertical videos in this feed yet.",
    feedRelays: 'Relays',
    feedRelaysFormHelp: 'Read this feed from these relays only. Leave empty for the usual ones.',
    feedAllToggle: 'Every post on these relays (a firehose)',
    feedAllCurated: 'Only popular posts: hide bots and posts nobody reacted to',
    feedAllNeedsRelays: 'Name the relays to read everything from.',
    feedRelaysHint: 'Changing these relays changes where posts come from, not which people or topics the feed follows.',
    feedRelayUsual: 'usual',
    feedRelaysOutbox: 'Right now, each person’s posts come from the relays they post to.',
    feedRelayPh: 'wss://relay.example.com',
    feedRelayAdd: 'Add',
    feedRelayOther: 'Other…',
    feedRelayPick: 'Add a relay…',
    feedRelayBad: 'That is not a relay address.',
    feedRelaysDefault: 'Back to the usual relays',
    feedRelaysDone: 'Done',
    feedSumFollows: 'Everyone you follow',
    feedSumFollowsPlus: 'Everyone you follow + {n} more',
    feedSumPeople: '{n} people',
    feedPacks: 'Follow packs',
    posTitle: 'Point of sale',
    posDesc: 'Charge a customer a set amount, with an optional tip prompt. Bills and tips are counted separately.',
    posOpen: 'Open',
    posEnterAmount: 'Enter an amount first',
    posNeedsSpending: 'Point of sale takes Lightning into Spending. Connect first',
    posNoteHint: 'Note (optional)',
    posCharge: 'Charge',
    posAskTip: 'Ask the customer for a tip',
    posTipChoices: 'Tip choices',
    posYourBill: 'Your bill',
    posAddTip: 'Add a tip?',
    posTipCustom: 'Other',
    posNoTip: 'No tip',
    posTotalLine: 'Total {n}',
    posContinue: 'Continue',
    posScanToPay: 'Scan to pay',
    posBreakdown: 'Bill {bill} + tip {tip} {u}',
    posWaiting: 'Waiting for payment…',
    posPaid: 'Paid',
    posNewSale: 'New sale',
    posInvoiceFailed: 'Could not create an invoice',
    posInvoiceExpired: 'That invoice expired before it was paid',
    posMemo: 'Bill {bill}, tip {tip}',
    posSalesToday: 'Today',
    posBills: 'Bills',
    posTips: 'Tips',
    posTotal: 'Total',
    posTipWord: 'tip',
    posVoid: 'cancelled',
    posAwaiting: 'awaiting payment',
    posMakingInvoice: 'Creating the invoice…',
    posBackspace: 'Delete',
    posNoDetail: 'No payment record for this sale yet',
    posNoteLabel: 'Note',
    posModeToggle: 'Start in point of sale on this device',
    posLinkHint: 'The till can also be bookmarked or added to the home screen: {url}',
    linkHandlerTitle: 'Open links here',
    linkHandlerDesc: 'Make this app the one your browser opens for nostr links (what njump calls "Your default web app") and bitcoin: links.',
    linkHandlerNostr: 'nostr links',
    linkHandlerBitcoin: 'bitcoin: links',
    linkHandlerAsked: 'Your browser is asking. Allow it to finish',
    linkHandlerRefused: 'Your browser would not register this here',
    feedAddPack: 'Search packs, or paste a following.space link…',
    feedPacksHelp: 'A pack is a list someone curates. Its members are read live, so the feed follows the list as it changes.',
    feedPackMembers: '{n} members',
    feedPackUntitled: 'Untitled pack',
    feedShareList: 'Keep as a nostr list',
    feedShareListHelp: 'Published under your key as a follow set (NIP-51), so other nostr apps show the same list. Untick to remove the list from the relays; the feed stays here.',
    feedPrivateMember: 'Private member, added in another app',
    feedListPeople: '{n} people',
    postCopyLink: 'Copy link',
    postShare: 'Share',
    listAddTo: 'Add to a list',
    listPickTitle: 'Lists',
    listPickEmpty: 'No lists yet. A feed of people is a list.',
    listNew: 'New list…',
    profLists: 'Lists',
    searchTopicRow: 'Posts on this topic',
    searchTopicFor: 'Posts tagged #{q}',
    alertsTitle: 'Notifications',
    alertsSub: 'Zaps, reactions and replies to your posts',
    alertsNew: '{n} new',
    alertsEmpty: 'Nothing yet. Zaps, reactions and replies to your posts will show up here.',
    alertZap: 'zapped you {sats}',
    alertReact: 'reacted {emoji}',
    alertBoost: 'boosted your post',
    alertReply: 'replied to you',
    alertMention: 'mentioned you',
    alertNoteGone: 'Not a post your relays have — perhaps a private message',
    postWho: 'Who reacted',
    feedFollow: 'Follow',
    feedFollowing: 'Following',
    feedFollowed: 'Following them',
    feedUnfollowed: 'Unfollowed',
    feedFollowingN: 'Posts from {n} people you follow',
    feedFollowing1: 'Posts from the one person you follow',
    feedNoFollowsShort: 'Follow people to fill this',
    feedNoFollows: "You don't follow anyone yet. Their posts will show up here.",
    feedFindPeople: 'Find people',
    feedEmpty: 'Nothing posted yet.',
    feedLoadingMore: 'Loading more posts…',
    feedEnd: 'You’re all caught up.',
    feedAttach: 'Attach a picture or video',
    postLike: 'Like',
    postBoost: 'Boost',
    postBoosted: 'Boosted',
    postBoostedAlready: 'You already boosted that',
    postQuote: 'Quote',
    postMore: 'More',
    postDiscardTitle: 'Discard this draft?',
    postDiscardBody: 'You’ll lose this draft and any attachments.',
    postKeepEditing: 'Keep editing',
    postDiscard: 'Discard draft',
    postMute: 'Mute',
    postUnmute: 'Unmute',
    postMuted: 'Muted. Their posts are hidden',
    postUnmuted: 'Unmuted',
    postBlock: 'Block and unfollow',
    postMuteThread: 'Mute this thread',
    postUnmuteThread: 'Unmute this thread',
    postReport: 'Report…',
    postEdit: 'Edit',
    translateBtn: 'Translate',
    translating: 'Translating…',
    translated: 'Translated',
    translatedFrom: 'Translated from {lang}',
    translateShowOriginal: 'Show original',
    translateSame: 'This post is already in your language',
    translateNothing: 'There are no words in this post to translate',
    translateFailed: 'Couldn’t translate this post',
    translateTitle: 'Translation',
    translateAutoToggle: 'Translate posts into {lang} automatically',
    translateHelp: 'Posts that look like they’re in another language are shown in yours, with a way back to the original. Translations come from your browser when it can, otherwise from coinos’s own LibreTranslate server — posts are public, but the text passes through it.',
    postEditTitle: 'Edit post',
    postEditHelp: 'The edit is published beside the original (kind 1010). Apps that read edits show the new text; others keep showing what you first wrote.',
    postEditedTag: 'edited',
    postEditedToast: 'Post edited',
    postDelete: 'Delete',
    postDeleteTitle: 'Delete this post?',
    postDeleteHelp: 'A deletion request goes to the relays (NIP-09). Most honour it and apps hide the post, but anyone who already copied it may keep it.',
    postDeletedToast: 'Post deleted',
    postDeletedNote: 'This post was deleted by its author.',
    postNotYours: 'This post was signed by a key that isn’t signed in here',
    reportTitle: 'Report this post',
    reportHelp: 'A report is public (NIP-56). People who follow you see this post folded; the post is hidden for you.',
    reportSpam: 'Spam',
    reportImpersonation: 'Impersonation',
    reportNudity: 'Nudity',
    reportProfanity: 'Harassment or abuse',
    reportIllegal: 'Illegal content',
    reportMalware: 'Scam or malware',
    reportOther: 'Something else',
    reportSent: 'Reported',
    foldReported1: 'Reported by {name}',
    foldReportedN: 'Reported by {n} people you follow',
    foldShow: 'Show',
    foldSensitive: 'Sensitive content',
    foldSensitiveWhy: 'Sensitive: {why}',
    modSensitiveToggle: 'Fold posts marked sensitive',
    modSensitiveHelp: 'Posts their authors marked with a content warning (NIP-36) stay behind it until you tap Show, and are left out of picture and video grids. Popular never shows them, nor posts from accounts that say they post adult content.',
    quoteHidden: 'Hidden post',
    profMutedBanner: 'Muted. Their posts are hidden everywhere else',
    modTitle: 'Muted and hidden',
    modDesc: 'Your mute list (NIP-51) follows you to other nostr apps, and theirs applies here.',
    modPeople: 'Muted people',
    modNoPeople: 'Nobody muted',
    modWords: 'Hidden words and hashtags',
    modWordHint: 'Add a word, phrase or #hashtag',
    modAdd: 'Add',
    modThreads: '{n} muted threads',
    modSpamToggle: 'Hide repeated posts from strangers',
    modSpamHelp: 'A post of 60+ characters posted again and again is spam; someone you don\'t follow who does it five times is hidden for the session. Posts stuffed with more than ten hashtags are hidden too.',
    modReportsToggle: 'Fold posts reported by people you follow',
    modReportsHelp: 'Their reports (NIP-56) are read from the relays. A flagged post is shown collapsed, with who flagged it; tap to see it anyway.',
    postBlocked: 'Blocked',
    zapSending: 'Sending your zap…',
    msgReply: 'Reply',
    mediaOpenPost: 'Open the post',
    profNewPost: 'New post',
    profYourProfile: 'Your profile',
    profComposePh: "What's on your mind?",
    profPostBtn: 'Post',
    profPosted: 'Posted',
    publicChatTitle: '{name} community',
    publicChatDesc: 'Live chat: read along, sign in to join the conversation',
    publicChatSignIn: 'Sign in to chat',
    publicChatOpenApp: 'Open coinos',
    publicChatLoading: 'Loading messages…',
    msgDelete: 'Delete message',
    msgNoIdentity: "Your nostr signer isn't connected. Reconnect it to send as yourself",
    msgSendFailed: 'No relay accepted the message',
    msgLockedChat: 'Your wallet is locked, so it can’t sign messages.',
    noteRefLink: 'view note',
    noteRefLoading: 'Fetching note…',
    noteRefNotFound: "Couldn't find that note on the relays.",
    addrLoading: 'Fetching…',
    addrNotFound: "Couldn't find that on the relays.",
    addrArticle: 'Article',
    addrRead: '{min} min read',
    addrList: 'A list by {name}',
    msgSignerOff: "Your nostr signer isn't connected, so you can't post as yourself yet.",
    msgAmberDms: 'Signed in with Amber, messages sent to you can’t be opened here, only ones you sent from this device.',
    msgAmberConnect: 'Connect Amber',
    postRepostedBy: '{name} reposted',
    gifBtnTitle: 'Add a GIF',
    gifSearchPh: 'Search GIFs',
    gifFrom: 'GIFs from',
    gifNone: 'No GIFs found.',
    gifDown: 'GIF search isn’t available right now.',
    gifLimited: 'Too many searches. Try again in a minute.',
    msgReconnect: 'Reconnect',
    msgDmsTitle: 'Messages',
    msgCommunitiesTitle: 'Communities',
    msgNewDm: 'New message',
    msgNoDms: 'No conversations yet.',
    msgShowAllDms: 'Show all {n} conversations',
    msgShowFewerDms: 'Show fewer',
    msgShowAllCommunities: 'Show all {n} communities',
    msgNoDmsYet: 'No messages yet. Say hi!',
    msgDmPlaceholder: 'Message',
    msgDmEncrypted: 'Private message · end-to-end encrypted',
    msgNewGroup: 'New group',
    msgGroupName: 'Group name',
    msgGroupPeople: 'Add people by name or npub…',
    msgGroupCreate: 'Create group',
    msgGroupNeedPeople: 'Add at least one person',
    msgGroupChecking: 'checking…',
    msgGroupCantJoin: "can't join group chats yet",
    msgGroupSomeCant: "{names} can't join group chats yet",
    msgGroupInvite: '{name} invited you to {group}',
    msgGroupUnnamed: 'Group chat',
    msgGroupDecline: 'Decline',
    msgGroupInfo: 'Private group · end-to-end encrypted',
    msgGroupLeave: 'Leave group',
    msgGroupRemove: 'Remove',
    msgGroupRename: 'Rename',
    msgGroupGone: 'You are no longer in this group',
    msgGroupAttachment: 'Attachment',
    msgGroupFailed: "Couldn't update the group. Try again",
    msgGroupSetup: 'Your signer is needed once to set up group chats',
    msgGroupProto: 'Group type',
    msgGroupProtoWn: 'Marmot (MLS)',
    msgGroupProtoCd: 'Cordn',
    msgGroupProtoCdHint: 'Cordn groups work with Amethyst and the Cordn app; messages travel through a coordinator server that cannot read them.',
    msgCordnInfo: 'Cordn group · end-to-end encrypted · delivered by a coordinator',
    msgCordnNoFiles: 'Attachments are not available in Cordn groups yet',
    msgRouteTitle: 'Send messages by',
    msgRouteAuto: 'Automatic',
    msgRouteAutoHint: 'Reply the way they last wrote',
    msgRouteWn: 'Marmot (MLS)',
    msgRouteWnHint: 'Encrypted with forward secrecy · works with White Noise',
    msgRouteDm: 'Nostr DM',
    msgRouteDmHint: 'Reaches any Nostr app that reads private messages',
    msgSysAdded: '{a} added {b}',
    msgSysJoined: '{b} joined',
    msgSysRemoved: '{a} removed {b}',
    msgSysLeft: '{b} left',
    msgSysAdminAdded: '{b} is now an admin',
    msgSysAdminRemoved: '{b} is no longer an admin',
    msgSysRenamed: '{a} named the group {name}',
    msgSysDisbanded: '{a} ended the group',
    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.",
    spendingRenewingPayment: 'Your Spending coins are renewing. You can pay once the renewal finishes.',
    zapBoardBtn: 'Move sats into Spending',
    onbBoardTitle: 'Top up Spending',
    onbBoardBody: 'Move some sats from Savings into Spending. Payments there are instant, with no mining fees.',
    topUpFromSavings: 'Top up from Savings',
    arkRenewedHistory: 'Renewed',
    arkDepthNotice: 'Unilateral exit costs about {fee} sats.',
    arkDepthFree: 'free',
    arkDepthBtn: 'Manage',
    arkExpiredNotice: '{amount} has passed its expiry. The server can still renew it. Until then it can\u2019t be spent or exited.',
    arkDepthRenewed: 'Renewal requested',
    arkDepthRenewBtn: 'Renew now',
    arkCoinsTitle: 'Spending coins',
    arkCoinsNone: 'No coins in Spending yet.',
    arkCoinsIntro: 'Your Spending balance lives in {n} coins totalling {total}. Each can be pulled on-chain without the server’s help, at a cost that grows with the coin’s history.',
    arkCoinsIntroOne: 'Your Spending balance lives in a single coin of {total}. It can be pulled on-chain without the server’s help, at a cost that grows with the coin’s history.',
    arkCoinsColAmount: 'Amount',
    arkCoinsColExpiry: 'Expiry',
    arkCoinsColExit: 'Exit cost',
    arkCoinsColRenew: 'Renewal',
    arkCoinsChipRenewTitle: 'This coin’s share of the renewal fee. The fee is priced on all the coins together and rounded up once, so tiny shares add almost nothing',
    arkCoinsChipRenewTiny: '< 1 sat',
    arkCoinsChipExpTitle: 'Time until this coin needs renewal',
    arkCoinsChipExitTitle: 'What pulling this coin on-chain by itself would cost',
    arkFeeChain: 'On-chain fee',
    arkFeeService: 'Service fee',
    arkCoinsExpired: 'expired',
    noteForeignKind: 'A kind {kind} event this app doesn’t show.',
    zapRowText: 'zapped {n} sats',
    pollVoter: '{n} vote',
    pollVoters: '{n} votes',
    pollMulti: 'Pick any',
    pollEnded: 'Poll closed',
    pollEndsAt: 'Closes {when}',
    arkExpiredDust: '{amount} has expired and is too small to renew. You can forget it from the Manage page.',
    arkCoinForget: 'Forget',
    arkCoinForgetTitle: 'Give this coin up: it has expired and is too small to renew or exit.',
    arkCoinForgotten: 'Coin forgotten',
    arkCoinsRenewing: 'renewing',
    arkCoinsIntroRenewing: 'Your Spending balance of {total} is being renewed: the coins below are in a round and come back as one fresh coin. Each can be pulled on-chain without the server’s help, at a cost that grows with the coin’s history.',
    arkRenewingTitle: 'Renewal in progress',
    arkRenewingLine: '{amount} sats → one fresh coin{fee}',
    arkRenewElsewhere: 'Started on another device, which finishes it the next time it’s open.',
    arkRenewingFee: ', fee {fee} sats',
    arkRenewStageQueued: 'Joining the next round. The wallet submits it on its next sync.',
    arkRenewStageRound: 'In a round. The fresh coin arrives once the round’s transaction has a confirmation, about a block from now, the next time this wallet syncs.',
    arkRenewStageFinishing: 'Finishing: signing the fresh coin.',
    arkRenewStageRetry: 'The last check with the server didn’t go through; the wallet keeps trying.',
    arkRenewingHistory: 'Renewing',
    arkRenewingOpen: 'See the coins',
    arkCoinsExitTitle: 'Unilateral exit',
    arkCoinsCoopTitle: 'Co-operative exit',
    arkOffboardBadAddress: 'Enter a valid on-chain address for this network.',
    arkCoinsExitBtn: 'Exit now',
    arkExitConfirmTitle: 'Start a unilateral exit?',
    arkExitConfirmBody: 'This brings your Spending coins to Savings on-chain without the server, by publishing each coin’s transaction history. Nothing needs the server’s cooperation (that is the point), but it is slow and it costs mining fees.',
    arkExitConfirmCoins: 'Coins',
    arkExitConfirmAmount: 'Amount',
    arkExitConfirmFee: 'Mining fees (about, paid from Savings)',
    arkExitConfirmSavings: 'Savings available for fees',
    arkExitConfirmWait: 'The funds arrive in Savings only once each coin’s timelock has run out, hours to days. An exit can’t be stopped once it has started.',
    arkExitConfirmGo: 'Start the exit',
    arkCoinsExitDesc: 'If the server vanished today, publishing every transaction in each coin’s history would cost about {fee} sats in mining fees. After a renewal it would be one fresh coin, one hop: about {after} sats.',
    arkCoinsRenewTitle: 'Renewal',
    arkCoinsRenewDesc: 'Your wallet renews coins automatically once they are close to expiring and renewal is free. Renewing early is an option if you would rather pay a small fee now to keep your exit cost low should the coinos server ever stop responding when you want to bring your funds on-chain. A renewal replaces your coins with a single fresh one in the next round ({every}), which flattens their history. The fee depends on how close the coin is to expiry. The closer, the cheaper:',
    arkCoinsRenewDescBase: 'Your wallet renews coins automatically once they are close to expiring, when renewal is cheapest. Renewing early is an option if you would rather pay a little more now to keep your exit cost low should the coinos server ever stop responding when you want to bring your funds on-chain. A renewal replaces your coins with a single fresh one in the next round ({every}), which flattens their history. Every renewal costs a flat {base} toward the round’s on-chain fee, plus a share that depends on how close the coin is to expiry. The closer, the cheaper:',
    arkCoinsRoundEvery: 'rounds run every {n} minutes',
    arkCoinsRoundEveryOne: 'rounds run every minute',
    arkCoinsRenewNowFee: 'Renew now for {fee}',
    arkCoinsRenewSome: 'Renew {n} selected for {fee}',
    arkCoinsRenewNone: 'Select coins to renew',
    arkCoinsSelectAll: 'Select all',
    arkCoinsSpanDay: '1 day',
    arkCoinsSpanDays: '{n} days',
    arkCoinsSpanHour: '1 hour',
    arkCoinsSpanHours: '{n} hours',
    arkCoinsSchedOver: 'more than {span} left',
    arkCoinsSchedBetween: '{a} to {b} left',
    arkCoinsSchedFinal: 'less than {span} left',
    arkOffboardQuote: 'Fee about {fee} sats right now ({chain} mining + {service} coinos), so it needs at least {min} sats.',
    arkOffboardQuoting: 'Checking the fee…',
    arkOffboardBelowMin: 'Your {n} sats are below the {min}-sat minimum for sending on-chain right now. Send them over Lightning instead: no mining fees.',
    arkOffboardAmtBelowMin: 'Sending on-chain needs at least {min} sats right now (the fee is about {fee}).',
    arkUniUneconomical: 'A trustless on-chain exit would cost about {fee} sats in mining fees, more than the {n} sats it would move, so it isn’t offered. For a balance this size, send it out over Lightning instead: no mining fees at all.',
    arkUniFallbackBody: 'The server didn’t cooperate, but your money can still move without it: your whole Spending balance exits to Savings on-chain. It takes about a day, and roughly {fee} sats of mining fees will be paid from Savings. Funds land in Savings. Send them on from there once it completes.',
    arkUniFallbackShort: 'Savings holds {have} sats. About {need} sats are needed for the mining fees. Top up Savings first.',
    arkUniFallbackBtn: 'Exit to Savings ({fee} sats in fees)',
    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',
    profBanner: 'Cover photo URL',
    profUploadBanner: 'Upload cover',
    profSaved: 'Profile published',
    hats: 'Hats',
    hatShopSupport: 'Support Coinos, Buy a Hat!',
    footerSupport: 'Show your support',
    hatShopTitle: 'Hat shop',
    hatShopBlurb: 'Support Coinos and wear the proof on your avatar.',
    hatShopOffline: 'The hat stand is unreachable right now.',
    hatBeanie: 'Toque',
    hatParty: 'Party hat',
    hatTrucker: 'Trucker cap',
    hatCowboy: 'Cowboy hat',
    hatFedora: 'Fedora',
    hatBowler: 'Bowler',
    hatWizard: 'Wizard hat',
    hatTop: 'Top hat',
    hatConstruction: 'Hard hat',
    hatCrown: 'The Crown',
    hatKnight: 'Knight\u2019s helm',
    hatCrownWho: "Adam's. Sorry.",
    hatCrownTaken: 'not for sale',
    hatOwned: 'In your hat rack',
    hatWear: 'Wear',
    hatWearing: 'Wearing ✓',
    hatConfirmTap: 'Tap to confirm',
    hatYours: '🎩 The {name} is yours!',
    hatNoFunds: 'You need {sats} sats in Spending for that hat',
    hatNeedSpending: 'Open your Spending balance first',
    hatMainnetOnly: 'Hats are sold on mainnet and Mutinynet only',
    profNotesTitle: 'Latest posts',
    profNotesNone: 'No recent posts found.',
    profTabPosts: 'Posts',
    profTabReplies: 'Replies',
    profRepliesNone: 'No recent replies found.',
    profReplyTag: 'reply',
    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, or search',
    msgDiscoverTitle: 'Suggested communities',
    msgPopularCommunities: 'Popular communities',
    msgBrowseCommunities: 'Browse all',
    msgHubNoSuggestions: 'No new communities to suggest right now.',
    msgHubLoading: 'Finding communities…',
    msgHubFailed: 'Could not load suggestions right now.',
    msgJoined: 'joined',
    msgNamePlaceholder: 'Community name',
    msgBadInvite: 'Not a valid invite link',
    msgInviteLoading: 'Fetching invite…',
    msgInviteNotFound: 'Invite not found on its relays',
    syncStorageTitle: 'Sync storage',
    syncStorageHow: 'Wallet state too large for a relay event is encrypted and stored as a blob on these Blossom servers (one per line). Any Blossom server works. Run your own or use another provider.',
    syncStorageSave: 'Save servers',
    syncStorageReset: 'Use default',
    syncStorageSaved: 'Sync storage servers saved',
    syncStorageInvalid: 'Enter at least one https:// server',
    syncStoragePending: '{n} snapshot(s) waiting to upload',
    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',
    msgLeaveConfirmShort: 'Leave?',
    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, so 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.',
    giftSettling: '{n} of your balance is still settling. Try again in a moment.',
    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',
    giftCoinLabel: 'Coin',
    giftClaimTxLabel: 'Claim',
    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',
    giftRevokedTag: 'revoked',
    giftCopyLinkAgain: 'Copy gift link',
    copyLink: 'Copy link',
    claimBody: 'Claim it into a brand-new wallet that only you control.',
    claimBodyExisting: 'Claim it into your account \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 accounts:',
    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: 'Account name',
    walletSettings: 'Account settings',
    securityTitle: 'Security',
    walletSettingsDesc: 'Name, recovery phrase, public key, and auto-logout for this account.',
    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 accounts 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.',
    receiveAddressTab: 'Savings',
    receiveLnTab: 'Lightning',
    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 {n} confirmations.',
    arkBoardedNoteOne: '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.',
    arkBoardPaused: "Moving money from Savings into Spending is paused for now while the Ark server gets a security fix. Everything else works as usual.",
    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.',
    arkRenewWarn: 'Your Spending balance ({amount}) is too small to renew itself and will expire around {date}. Receiving any amount, or spending it, keeps it alive.',
    arkRenewForecast: 'Coins renew in ~{when} for {fee}.',
    arkRenewForecastNow: 'Coins renew shortly for {fee}.',
    arkBoardStuck: 'Moving money hit a snag and will keep retrying: {error}',
    arkRenewWarnPush: 'Your Spending balance is too small to renew itself and expires around {date}. Receive any amount or spend it to keep it alive.',
    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: 'Paused: the next step needs about {need} sats in Savings for its mining fee. Top up Savings and it resumes by itself.',
    arkExitFeeShort: 'Savings can’t cover the exit’s mining fees (about {need} sats needed, {have} available). Top up Savings first.',
    preimageLabel: 'Preimage',
    copyPreimage: 'Copy preimage',
    arkExitSummary: 'Moving {n} sats to Savings on-chain, about {eta} until it lands.',
    arkExitViewTx: 'View on block explorer',
    arkExitSoon: 'soon',
    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.',
    arkExitingHistory: 'Moving to Savings',
    arkExitPendingTag: 'in progress',
    arkNoSpendableMidExit: 'No spendable balance. Your coins are on their way to Savings in an on-chain exit.',
    arkExitInFlightLink: 'Exit in progress ({n} sats). View details',
    arkExitCoopHeld: 'Your balance is in the unilateral exit below. While that exit hasn’t touched the chain yet, cancelling it frees the coins for a cooperative 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!',
    arkZapNoPostZap: 'Can’t zap this post: its author has no Lightning address or Ark address.',
    arkZapLnDown: 'Can’t pay Lightning right now: Spending isn’t connected. Try again in a moment.',
    arkZapNoArkGift: 'They haven’t set up an Ark address yet. Send the zap as an Ark gift locked to their nostr key instead. They’ll get a DM with the claim link, and only they can open it.',
    arkZapGiftBtn: 'Send locked gift',
    arkZapGiftHint: 'Instant Ark gift only their nostr key can claim. You can revoke it from the gift card while unclaimed.',
    arkZapGiftSentTitle: 'Gift sent!',
    giftDmText: '🎁 Someone sent you {amount} on coinos! Claim it here: {link}\nIf you’re asked for a claim code, use: {code}',
    giftDmSending: 'Sending them the claim link by DM…',
    giftDmSent: 'Claim link sent to them by DM ✓',
    giftDmFailed: 'Couldn’t reach them by DM. Copy the link and claim code and pass them along yourself.',
    giftLockedTo: 'Locked to',
    giftCodeHint: 'Enter the claim code from your DM to unlock this gift.',
    claimCodePlaceholder: 'Claim code',
    claimCodeWrong: 'That code doesn’t unlock this gift.',
    claimWithExtension: 'Unlock with nostr extension',
    orEnterCode: 'or enter your claim code',
    extWrongAccount: 'Your extension is signed in to a different nostr account.',
    extNoNip44: 'Your extension doesn’t support NIP-44 decryption.',
    extDecryptFailed: 'Couldn’t decrypt with the extension.',
    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',
    arkServerTitle: 'Ark server',
    arkServerDesc: 'Spending runs on an Ark server. coinos runs one; Second runs another; or point at your own. Coins are held per server. Switch back any time to reach what a server holds.',
    arkServerCustomUrl: 'Ark server URL (https://…)',
    arkServerCustomEsplora: 'Esplora API URL for that server',
    arkServerCustomNeeded: 'Enter both the Ark server and its Esplora URL first',
    arkServerSwitchWarn: '{amount} sats are held with the current server. They stay there (switch back to reach them) but can’t be spent from the new one. To move them, send or exit them first.',
    arkServerSwitchGo: 'Switch server',
    arkCmpShow: 'Compare the servers',
    arkCmpHide: 'Hide the comparison',
    arkCmpRunBy: 'Run by',
    arkCmpRunByCoinos: 'coinos',
    arkCmpRunBySecond: 'Second, the makers of Ark',
    arkCmpRunByOther: 'someone else',
    arkCmpLifetime: 'Coin lifetime before renewal',
    arkCmpRounds: 'Rounds',
    arkCmpEvery: 'every {n} min',
    arkCmpRenew: 'Renewal fee',
    arkCmpLnSend: 'Lightning send fee',
    arkCmpLnRecv: 'Lightning receive fee',
    arkCmpExit: 'Cooperative exit fee',
    arkCmpBoard: 'Boarding',
    arkCmpBoardVal: 'min {min} sats, {n} confirmations',
    arkCmpMaxCoin: 'Largest coin',
    arkCmpFree: 'free',
    arkCmpFreeNear: 'free within {span} of expiry',
    arkCmpMin: 'min {n} sats',
    arkCmpNote: 'Fees graded by expiry cost more the earlier a coin is renewed or exited. Figures come from each server’s own published info, read just now. Either server pays Lightning invoices and takes Lightning payments to your coinos name.',
    arkServerSwitched: 'Ark server switched',
    arkOtherServerReceipt: '{amount} sats arrived with {server}. Switch to that server in Settings → Advanced to spend them.',
    arkForeignBip21: 'Their Ark address is on a different Ark server, so the on-chain address is paid instead.',
    arkForeignServer: 'That Ark address belongs to a different Ark server, so it can’t be paid from here directly. Ask for a Lightning invoice, or pay their coinos name or npub. Those fall back to Lightning by themselves.',
    arkLnFromSavingsTitle: 'Lightning pays from Spending',
    arkLnFromSavingsBody: 'Lightning payments leave your Spending balance, not Savings. Spending holds {have}.',
    arkLnFromSavingsShort: 'Spending can’t cover this one. Move funds over from Savings first. That takes an on-chain confirmation, so ask for a fresh invoice afterwards.',
    arkLnUseSpending: 'Switch to Spending and continue',
    arkLnMoveFirst: 'Move Savings into Spending first',
    arkLnFee: 'Fee',
    arkLnPaying: 'Paying invoice…',
    arkLnRetrying: 'Route failed. Retrying with a bigger fee budget ({n}/{of})…',
    arkLnPayFailed: 'Payment failed',
    arkRenewConfirmTitle: "Renew these coins?",
    arkRenewConfirmBody: "Your wallet renews coins by itself, for free, when they are close to expiring. Renewing now is optional.",
    arkRenewConfirmFee: "Renewal fee",
    arkRenewConfirmLeft: "Left to spend meanwhile",
    arkRenewConfirmAll: "This is your whole Spending balance. You won’t be able to pay or send from Spending until the renewal confirms.",
    arkRenewConfirmWait: "Coins being renewed can’t be spent until the round’s transaction confirms on the Bitcoin network: usually about ten minutes, sometimes an hour or more. A renewal can’t be cancelled once it has started.",
    arkRenewViewTx: "View the round’s transaction",
    arkLnRenewing: "Nothing was sent. {sats} sats are being renewed and can’t be spent until the renewal confirms on the Bitcoin network: usually about ten minutes, sometimes an hour or more. Try again then.",
    arkLnShort: "Not enough in Spending: this payment needs {need} sats with its fee, and {have} are available.",
    arkLnRefunded: 'Payment failed. Your money is back in Spending.',
    arkLnPaidTitle: 'Paid!',
    or: 'or',
    nlSignIn: 'Sign in',
    feedOneNew: '1 new post',
    feedNNew: '{n} new posts',
    feedOneNewWord: 'new post',
    feedNNewWord: 'new posts',
    composePreview: 'Preview',
    composePreviewHide: 'Hide preview',
    composePreviewTag: 'Preview',
    composePreviewEmpty: 'Nothing to preview yet.',
    onbWelcomeTitle: 'Welcome to coinos',
    onbWelcomeBody: 'Bitcoin you actually hold. Your wallet is twelve words. Nobody else ever has them, and they work in any wallet, anywhere.',
    onbStart: 'Create a new wallet',
    onbStartHint: "Takes a minute: write down twelve words and it's yours",
    onbOrSignIn: 'or',
    onbHaveSeed: 'Have an existing seed or entropy to import?',
    nlGooglePrev: 'Signed in with Google here before? Use the previous sign-in server',
    onbCreateInstead: 'Create a new wallet instead',
    onbBackupTitle: 'Write down your recovery phrase',
    onbBackupBody: "Signing in was the easy part, but the sign-in isn't your wallet. These twelve words are. They're the only way back in if you lose access to that account, and nobody can reissue them for you.",
    onbBackupDone: "I've written it down",
    onbBackupLater: 'Later',
    backupReminder: 'Your recovery phrase is still unwritten.',
    backupReminderGo: 'Write it down',
    onbSeedTitle: 'Set up your wallet',
    onbSigninTitle: 'Sign in',
    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',
    onbMigrateDoor: 'Migrate a coinos account',
    onbMigrateIntro: "Moving from the original coinos.io? Your balance and username come with you. First, set up the wallet they'll move into and write down its twelve words: they're yours alone, and the only way back in. Then we'll take you to coinos.io to finish the move.",
    onbMigrateCreate: 'Create my new wallet',
    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}…',
    onbSpendTitle: 'Set up a spending account',
    onbSpendBody: 'Claim a username and get your own payment address. People can send you bitcoin instantly, with just your name.',
    onbNameWait: 'Getting things ready…',
    onbNotNow: 'Not now',
    onbAvatarTitle: 'Choose your look',
    onbAvatarBody: 'Pick a cover and an avatar, or upload your own. You can change them anytime.',
    onbCover: 'Cover image',
    onbCoverPrev: 'Previous cover',
    onbCoverNext: 'Next cover',
    onbCoverUpload: 'Upload a cover',
    onbCoverYours: 'Your photo',
    onbUpload: 'Upload a photo',
    onbUploadFailed: 'Upload failed. Try another picture',
    onbProfileSkipped: 'Couldn’t publish your profile. Set it later from Edit profile',
    onbContinue: 'Continue',
    onbSkipAvatar: 'Continue',
    onbDoneTitle: "You're all set!",
    onbDoneBody: 'Your spending account is set up. Your address is live and the coinos community chat is waiting for you.',
    onbEnter: 'Enter coinos',
    spendSetup: 'Set up a spending account',
    nlConnectApp: '📱 Connect a signer app',
    nlOpenSigner: 'Open signer app',
    nlWaitingSigner: 'Waiting for your signer to connect…',
    nlTitle: 'Log in with Nostr',
    nlSignInNostr: 'Sign in with Nostr',
    nlDesc: 'Use your Nostr account to open your wallet, on any device, with the same account.',
    nlExtension: 'Use browser extension',
    nlAmber: 'Sign in with Amber (Android)',
    nlAmberWait: 'Waiting for your signer app… approve there, then come back here.',
    nlAmberPasteHint: 'The answer is read from the clipboard when you return. If nothing happens, paste it here:',
    nlAmberPastePh: 'Paste the signer’s answer',
    nlAmberPasteBtn: 'Use it',
    nlAmberBadPaste: 'That doesn’t look like the answer to this request.',
    nlGoogle: 'Sign in with Google',
    nlPasskey: 'Sign in with a passkey',
    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.',
    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',
    nlLinkTitle: 'Nostr account',
    nlLinkDesc: 'Log in with your own Nostr account instead. Chat and your payment address will use its identity, and logging in with it on another device opens this wallet.',
    nlBuiltinDesc: 'This wallet has a built-in Nostr account, derived from its seed. Chat and your payment address already use it. You don’t need to link anything.',
    nlExportBtn: 'Export Nostr key',
    nlExportWarn: 'This key opens this wallet. Anyone who has it can spend your money, read your chats, and change your payment address. Guard it like your recovery phrase.',
    nlExportReveal: 'Show the key',
    nlExportedNote: 'Logging in with Nostr using this key opens this wallet.',
    nlChangeBtn: 'Use a different Nostr account',
    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.",
    nlReconnectTitle: 'Reconnect your signer',
    nlAmberUpgradeTitle: 'Read your messages',
    nlAmberUpgradeBody: 'Amber can only open messages here when it’s connected as a remote signer. Tap below, approve in Amber (allow decrypting), and your messages will load. Signing works as before.',
    nlReconnectBody: 'Your Nostr signer isn’t connected on this device. Reconnect to keep acting as {npub}…',
    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: 'Change username',
    namesCustomHow: 'Pick a new name for your payment address. Your old address stops working.',
    namesMerchantShow: 'I\u2019m a merchant \u2014 show point-of-sale tools',
    namesMerchantHide: 'Hide point-of-sale tools',
    namesPos: 'Point of sale',
    namesPosHow: 'A coinos POS terminal takes payments to this address. Create a token and enter it as the API token when you flash the terminal at coinos.io/flash. The terminal can ring up sales and see them paid; it can never spend.',
    namesPosCreate: 'Create POS token',
    namesPosCopy: 'Copy token',
    namesPosRevoke: 'Revoke tokens',
    namesPosRevoked: 'POS tokens revoked',
    namesPosLink: 'Tap-to-pay link',
    namesPosLinkHow: 'What the terminal broadcasts over NFC. Any lightning wallet that scans it pays the sale the terminal just rang up, or asks for an amount. Print it as a sticker if you like.',
    namesPosCopyLnurl: 'Copy LNURL',
    namesPosCopyLink: 'Copy link',
    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',
    namesRule: 'Letters and numbers only',
    namesResolving: 'Looking up {name}…',
    namesSendTo: 'Send to {name}',
    namesClaim: 'Claim',
    namesClaimed: 'You are now {name}',
    namesCopy: 'Copy address',
    namesMoreOpen: 'More options',
    namesMoreTitle: 'More ways to get paid',
    namesMoreBolt11: 'Lightning invoice (BOLT 11)',
    namesMoreBolt11How: 'A one-time invoice for a specific amount',
    namesMoreBolt12: 'Lightning offer (BOLT 12)',
    namesMoreBolt12How: 'A reusable offer. Mining pools like Ocean pay to one',
    namesMoreArk: 'Ark address',
    namesMoreArkHow: 'Get paid directly from any Ark wallet',
    namesMoreClink: 'Nostr offer (CLINK)',
    namesMoreClinkHow: 'A static code Nostr wallets can pay',
    namesMoreOnchain: 'Bitcoin address (on-chain)',
    namesMoreOnchainHow: 'Switch to Savings for a regular on-chain address',
    namesInvAmountPh: 'Amount (sats)',
    namesInvMemoPh: 'Description (optional)',
    namesInvCreate: 'Create invoice',
    namesInvNew: 'New invoice',
    namesInvBadAmount: 'Enter an amount in sats',
    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:',
    msgMarkAllRead: 'Mark all read',
    namesOwnNode: 'Deliver to your own node',
    namesOwnNodeMenuHow: 'Run a Lightning node? Point your address at it. Payments land there instead of in this wallet.',
    namesOwnNodeHow: 'Run your own Lightning node? Paste the CLINK offer code (noffer1…) from a service beside it. Lightning payments to your address will be delivered straight to your node instead of into this wallet.',
    namesOwnNodeActive: 'Lightning payments to your address are delivered to your own node:',
    namesOwnNodeClear: 'Deliver to this wallet again',
    namesOwnNodeSet: 'Delivery updated. Payments go to your node',
    namesOwnNodeCleared: 'Payments land in this wallet again',
    namesOwnNodeNoRelay: 'That offer code names no relay',
    namesOwnLnoHow: 'Or paste your node’s BOLT12 offer (lno1…). It goes into your address’s DNS record, so senders fetch invoices straight from your node.',
    namesOwnLnoActive: 'Your address’s DNS record carries your own node’s offer:',
    namesOwnLnoBad: 'That doesn’t look like a BOLT12 offer',
    bolt12Currency: 'This offer is priced in {currency}, which this wallet can’t pay',
    clinkBadInvoice: 'The recipient’s service returned an invoice that doesn’t match',
    clinkFixedAmount: 'This offer has a fixed price',
    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',
    nwcNoPushHere: 'This browser can\u2019t receive push messages.',
    nwcNoPermission: 'Notifications weren\u2019t allowed.',
    upNoDistributor: 'To be reached while this app is closed you need a push app installed alongside it: ntfy is the usual one (F\u2011Droid or the Play Store). Install it, open it once, then reopen coinos. Until then, payments and messages arrive whenever coinos is open.',
    upWaiting: 'Waiting for {app} to hand over a push address \u2014 reopen coinos in a moment and it should be here.',
    nwcNoPushService: 'This browser has no push service, so it can\u2019t be woken while closed. On Android that\u2019s usually a phone without Google Play Services \u2014 Firefox brings its own push and works there. Zaps are still answered whenever coinos is open.',
    nwcOn: 'On',
    nwcOff: 'Off',
    nwcBackgroundOn: 'Requests will be answered in the background',
    nwcBackgroundOff: 'Background answering turned off',
    nwcPaidTitle: 'Payment sent',
    nwcPaidBody: '-{amount} sats via {name}',
    settingsNotifications: 'Notifications',
    searchUsers: 'Search',
    spendingName: 'Spending',
    manageAccounts: 'Accounts',
    lockedToast: 'Locked',
    logout: 'Log out',
    lockedTitle: 'Locked',
    lockedDesc: 'Your wallets are locked on this device. Enter your password to open them again.',
    settingsWalletDesc: 'Name, keys, backup',
    settingsPaymentsDesc: 'Zaps and auto-withdraw',
    settingsNetworkDesc: 'Servers and explorers',
    settingsNostrDesc: 'Identity and connections',
    settingsNotifDesc: 'What buzzes this device',
    settingsAdvancedDesc: 'Network, rarities, rescues',
    settingsPosDesc: 'Till mode, tips and the /pos link',
    unlockForLabel: 'Stay unlocked',
    unlockFor5m: '5 minutes',
    unlockFor30m: '30 minutes',
    unlockFor1d: '1 day',
    unlockForever: 'Until I lock it',
    viewWithoutPassword: 'View without password',
    arkExitSendTitle: 'Send on-chain',
    arkExitSendNote: 'Leaves your Spending balance and arrives on-chain. Network fees come off what arrives.',
    savingsName: 'Savings',
    renameWallet: 'Rename',
    addWalletSeed: 'Seed phrase',
    addWalletNewSeed: 'New seed phrase',
    addWalletShareSeed: 'Same seed as {name}',
    addWalletShareNote: 'Backs up under the existing phrase, but with its own separate coins, history, and identity.',
    addWalletGo: 'Continue',
    zapSentShort: 'Zapped {n}',
    zapSetupTitle: 'Set your zap amount',
    zapSetupDesc: 'One tap on ⚡ sends this amount instantly: no form, no confirmation. Change it any time by holding ⚡, or in Settings → Payments.',
    zapSetupSave: 'Save and zap',
    zapSettingsDesc: 'This is what one tap on ⚡ sends: no form, no confirmation. Tap again while one is still on its way to send another.',
    zapAmountSaved: 'One tap now zaps {n} sats',
    zapSoundToggle: 'Play a sound when you zap',
    zapDefaultLabel: 'One-tap zap amount',
    zapDefaultHint: 'What a single tap of the ⚡ button sends.',
    threadNoReplies: 'No replies yet.',
    threadReplyHint: 'Write a reply…',
    threadReplySend: 'Reply',
    threadReplied: 'Reply published',
    threadRootMissing: 'The original post could not be found.',
    searchUsersHint: 'Name, npub, address, or #topic…',
    searchUsersEmpty: 'Find anyone on nostr and open their profile.',
    searchNoResults: 'No one found.',
    notifTitle: 'Notifications',
    notifEnable: 'Enable notifications on this device',
    notifEnabled: 'Notifications enabled',
    notifOnDevice: 'Notifications are on for this device.',
    notifPayRecv: 'Payments received',
    notifDm: 'Direct messages',
    notifMention: 'Replies and mentions on nostr',
    notifZap: 'Zaps on your posts',
    notifReaction: 'Reactions to your posts',
    notifChatHint: 'Community chat is opt-in per room. Turn it on from a room’s menu.',
    notifPaySent: 'Payments sent by connected apps',
    notifPaySentHint: 'A heads-up when a connected app (a nostr client zapping, for example) spends from this wallet while it’s closed.',
    nwcNotifHint: 'You’ll be asked to allow notifications. The connection uses them to answer when Coinos is closed.',
    nwcNotifNeeded: 'Without notifications the connection couldn’t answer when Coinos is closed, so it wasn’t added. Allow notifications and try again.',
    nwcNotifBlocked: 'Notifications are blocked for this site. Allow them in your browser settings, then add the connection again.',
    nwcNoPush: 'This browser can’t receive push notifications, so a connection wouldn’t work while Coinos is closed.',
    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.',
    sendNameHint: 'Sent instantly, no fee.',
    arkOwnAddrBoard: "That's your own Spending address. Moving from Savings instead.",
    arkLnExceedsSpending: 'That invoice asks for {need} but Spending holds {have}.',
    arkPayFromSpending: 'Ark payments come from your Spending balance. Switch to Spending to send this.',
    arkPayTo: 'To',
    arkNoFee: 'none',
    arkSendBtn: 'Send',
    arkSentTitle: 'Sent!',
    arkBoarded: 'Boarded to Ark',
    dateLabel: 'Date',
    detailsLabel: 'Details',
    arkVtxoId: 'Coin (VTXO)',
    arkVtxoInputs: 'Coins spent',
    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',
    giftChainWarn: '⚠ This is a bearer link: whoever opens it can claim the gift, so share it privately. It funds a fresh on-chain wallet when claimed.',
    giftRetired: 'This gift link uses an older format that’s no longer safe to claim. Ask the sender to void it and create a new one.',
    lnAutopayOffer: 'Auto-pay {name} up to {n} next time',
    lnAutopaySetLabel: 'Auto-pay {name} next time, up to:',
    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(assetUrl('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;
}