aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/createExceptions.js
blob: 26baeb7e1948d1c6b15d7e2113e33d4437bccc67 (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
BitcoinCore = require("@asoltys/bitcoin-core");
fs = require("fs");
config = require("./config");
bc = new BitcoinCore(config.bitcoin);
l = require("pino")();
require("./db");

console.log(db.Payment);

(async () => {
  payments = (
    await db.Payment.findAll({
      attributes: ["hash"],
    })
  ).map((p) => p.hash);
  const transactions = await bc.listTransactions("*", 1000);
  var stream = fs.createWriteStream("exceptions", {
    flags: "a",
    emitClose: true,
  });
  transactions.map((tx) => {
    if (!payments.includes(tx.txid)) {
      l.info("writing", tx.txid);
      stream.write(tx.txid + "\n");
    }
  });
  stream.end();
  stream.on("close", () => {
    l.info("done");
    process.exit();
  });
})();
47 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 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788
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'],
  ['it', 'Italiano'],
  ['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: {
    mediaSeek: 'Seek',
    mediaMomentDrop: 'About the whole post, not this moment',
    mediaCommentAt: 'Comment at',
    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',
    passkeyLockBtn: 'Lock with fingerprint or face',
    passkeyLockDesc: 'Your saved wallets are encrypted with a passkey kept in your phone’s secure hardware (or your password manager). Opening them takes your fingerprint or face. Your recovery words still restore them if the passkey is ever lost.',
    passkeyLockOnNote: 'Locked with your fingerprint or face (a passkey).',
    passkeyLockOff: 'Turn off fingerprint lock',
    passkeyLockOnToast: 'Fingerprint lock on',
    passkeyLockOffToast: 'Fingerprint lock off',
    passkeyLockFailed: 'Couldn’t use a passkey here: {msg}',
    passkeyUnlock: 'Unlock with fingerprint or face',
    passkeyLockedDesc: 'Your wallets are locked on this device. Use your fingerprint or face to open them.',
    passkeyEnterDesc: 'Use your fingerprint or face to open the wallets saved on this device.',
    passkeyLostHint: 'Lost the passkey? Your recovery words still restore every wallet.',
    lockPwOrPassword: 'or set a password:',
    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',
    historyExportTitle: 'Transaction history',
    historyExportDesc: 'Download every payment in Spending and Savings as a CSV file, for a spreadsheet or your accountant.',
    historyExportBtn: 'Export CSV',
    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',
    mediaUploadUnauthorized: 'The media server denied this upload. Check that your Nostr identity is allowed to upload there.',
    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',
    clipsTall: 'Tall only',
    clipsWide: 'Wide only',
    clipsAll: 'All shapes',
    clipsToggleTitle: 'Which clips to show: the ones that fit the screen, or all',
    videoTapToPlay: "Tap to play",
    videoPlay: "Play",
    videoPause: "Pause",
    pagerFetchingVideo: "Fetching the next video…",
    pagerFetchingPictures: "Fetching more pictures…",
    pagerCaughtUp: "That’s everything for now",
    pagerPrevPost: 'Previous post',
    pagerNextPost: 'Next post',
    pagerPrevPic: 'Previous picture',
    pagerNextPic: 'Next picture',
    videoNoPicture: "This browser can’t show this video’s picture",
    videoOpenFile: "Open the file",
    wnDevicesTitle: 'Group invitations',
    wnDevicesHelp: 'White Noise and other Marmot apps invite one of your devices, and only that device gets the group. Keep the invitations coming to the device you actually use.',
    wnDeviceHere: 'This device can be invited.',
    wnDeviceRetired: 'This device no longer takes invitations; another one of yours does.',
    wnDeviceNone: 'This device has no invitation key published yet.',
    wnDevicesOthers: '{n} other invitation key(s) of yours are published:',
    wnDevicesNoOthers: 'No other device of yours is published.',
    wnDeviceUnknown: 'another app',
    wnOnlyHere: 'Invite this device only',
    wnOnlyHereDone: 'Done. {n} other invitation key(s) retired.',
    wnOnlyHereFail: 'Couldn’t retire the other keys. Try again.',
    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',
    noteReplyingTo: 'Replying to',
    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: "Notes",
    mediaConversations: "Conversations",
    mediaImages: "Images",
    mediaVideos: "Videos",
    mediaLive: "Live",
    mediaArticles: "Articles",
    mediaNoArticles: "No articles in this feed yet.",
    mediaNoImages: "No pictures in this feed yet.",
    mediaNoVideos: "No vertical videos in this feed yet.",
    mediaNoLive: "Nobody in this feed is live right now.",
    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}',
    relayLinkOpen: 'See what’s posted on this relay',
    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.',
    liveBadge: 'LIVE',
    livePlanned: 'SOON',
    liveEnded: 'ENDED',
    livePast: 'Past streams',
    liveReport: 'Report this stream',
    liveReportTitle: 'Report this stream',
    liveReportDesc: 'coinos carries this stream. Tell us what’s wrong and we’ll look at it right away.',
    liveReason_csam: 'Sexual content involving a minor',
    liveReason_sexual: 'Sexual content',
    liveReason_violence: 'Violence or gore',
    liveReason_illegal: 'Something else illegal',
    liveReason_spam: 'Spam or a scam',
    liveReason_other: 'Something else',
    liveReportTextPh: 'Anything we should know (optional)',
    liveReportSend: 'Send report',
    liveReported: 'Reported. Thank you.',
    liveTakeDown: 'Take down',
    liveTakeDownAsk: 'Take this stream down? It ends now, and its recording is kept from everyone (preserved for reporting).',
    liveBanAsk: 'Also stop this account from streaming again?',
    liveTakenDown: 'Taken down.',
    liveTakenDownBanned: 'Taken down, and the account can’t stream again.',
    liveRemoved: 'This stream was taken down.',
    liveDelete: 'Delete recording',
    liveDeleteAsk: 'Delete this stream’s recording? Nobody will be able to watch it again.',
    liveDeleted: 'Recording deleted.',
    liveDeletedSay: 'The streamer deleted this recording.',
    goLiveNewName: 'Your name can go live a day after you claimed it: from {when}.',
    goLiveBanned: 'This account can’t go live on coinos.',
    liveWatchRecording: 'Watch the recording',
    liveStaleMine: '“{title}” still says live, but nothing is going out.',
    liveNotYours: 'Only the account that started this stream can end it.',
    liveRecordingBadge: 'RECORDING',
    liveUntitled: 'Live stream',
    liveWatching: '{n} watching',
    liveWatch: 'Watch',
    liveZap: 'Zap',
    liveChat: 'Chat',
    liveChatEmpty: 'No messages yet.',
    liveChatHint: 'Say something',
    liveSend: 'Send',
    liveZapped: 'zapped {n} sats',
    liveSomeone: 'Someone',
    liveOffline: 'The stream isn’t playing right now.',
    liveNoPlayer: 'This browser can’t play the stream.',
    liveOver: 'This stream has ended.',
    liveStartsAt: 'Starts {when}',
    liveNotFound: 'Couldn’t find that stream.',
    goLive: 'Go live',
    goLiveCamera: 'Camera',
    goLiveScreen: 'Screen',
    goLiveFlip: 'Flip',
    goLiveTitlePh: 'What’s your stream about?',
    goLiveDefaultTitle: '{name} is live',
    goLiveStart: 'Go live',
    goLiveStarting: 'Starting…',
    goLiveAsking: 'Asking for the camera…',
    goLiveNoCamera: 'The camera isn’t available. Check that the browser may use it.',
    goLiveNoScreen: 'Screen sharing was cancelled or isn’t available.',
    goLiveNeedsName: 'Going live needs a coinos name: claim one in Settings first.',
    goLiveRefused: 'The live server didn’t take the stream.',
    goLiveHint: 'Your stream is public: anyone can watch it and its recording, kept for a week. Keep this page open while you’re live.',
    goLiveEnd: 'End',
    goLiveEndAsk: 'End your stream?',
    goLiveEnded: 'Your stream has ended.',
    goLiveReconnecting: 'Reconnecting…',
    goLiveBehind: 'Your connection is falling behind',
    goLiveLost: 'Lost the connection to the live server.',
    goLiveRestarted: 'The stream had to start over.',
    callVoice: 'Voice call',
    callVideo: 'Video call',
    callIncomingVoice: 'Voice call',
    callIncomingVideo: 'Video call',
    callIncomingFrom: '{name} is calling',
    callCalling: 'Calling…',
    callConnecting: 'Connecting…',
    callAccept: 'Answer',
    callDecline: 'Decline',
    callHangUp: 'Hang up',
    callMute: 'Mute',
    callUnmute: 'Unmute',
    callCamOff: 'Turn camera off',
    callCamOn: 'Turn camera on',
    goLiveToCamera: 'Back to camera',
    callShare: 'Share screen',
    callStopShare: 'Stop sharing',
    callAddVideo: 'Turn on camera',
    callFlip: 'Switch camera',
    callMinimize: 'Minimize',
    callEnded: 'Call ended',
    callNoAnswer: 'No answer',
    callDeclined: 'Declined',
    callBusy: 'They’re on another call',
    callFailed: 'Couldn’t connect',
    callNoPermission: 'The microphone or camera isn’t allowed',
    callMissed: 'Missed call',
    callAnsweredElsewhere: 'Answered on another device',
    callDeclinedElsewhere: 'Declined on another device',
    callUnsupported: 'This browser can’t make calls.',
    notifCall: 'Calls',
    callLogVoice: 'voice call',
    callLogVideo: 'video call',
    callLogOut: 'Outgoing {what}',
    callLogIn: 'Incoming {what}',
    callLogMissed: 'Missed {what}',
    callLogDeclined: 'Declined {what}',
    callLogTheyDeclined: 'Outgoing {what} · Declined',
    callLogNoAnswer: 'Outgoing {what} · No answer',
    callLogCancelled: 'Cancelled {what}',
    callLogBusy: 'Outgoing {what} · Busy',
    callLogFailed: '{what} · Couldn’t connect',
    callLogUnrung: 'This didn’t ring: calls ring from people you follow or have written to.',
    callLogUnrungWas: 'This didn’t ring: you didn’t follow them then.',
    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',
    translateTo: 'Translate posts into',
    translateHelpDeepl: 'Posts that look like they’re in another language are translated by DeepL with your own key. The key stays on this device; coinos’s server passes each request on to DeepL (DeepL doesn’t accept requests from browsers) and never keeps it.',
    deeplKeyPh: 'DeepL API key (optional)',
    deeplSave: 'Use DeepL',
    deeplSaved: 'Translations now come from DeepL',
    deeplOn: 'Using your DeepL key {key}',
    deeplRemove: 'Remove',
    deeplBadKey: 'That DeepL key wasn’t accepted',
    deeplQuota: 'Your DeepL key is out of quota for this month',
    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',
    mediaPicGone: 'This picture couldn’t be loaded',
    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.',
    zapLocked: 'Your wallet is locked, so it can’t zap. Unlock it to send sats.',
    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',
    arkCoinsHeld: 'held by Coinos',
    arkCoopSelected: 'Only the {n} coins ticked above will move.',
    arkCoopSelectedOne: 'Only the coin ticked above will move.',
    arkCoopSelectedAmt: '{n} selected coins · {amount}',
    arkCoopSelectedAmtOne: '1 selected coin · {amount}',
    arkCustodyTitle: 'Custody strategy',
    safeBootTitle: 'Coinos opened in safe mode',
    safeBootBody: 'The app was closed {n} times in a row before it finished opening, so automatic connections are paused this time. Your coins and keys are untouched. Last step reached: {phase}.',
    safeBootOpen: 'Open normally',
    safeBootCopy: 'Copy report',
    safeBootCopied: 'Copied',
    arkCustodyDesc: 'Each coin expires about {days} days after it was made. Until then you can bring it on-chain yourself, without Coinos; after that, Coinos keeps honoring it. Renewing before expiry keeps your own exit, for a fee, so it is a choice:',
    arkCustodyCustodial: 'Allow coins to expire',
    arkCustodyCustodialDesc: 'Nothing renews and no renewal fees are paid. A coin keeps its own exit until it expires. After that, paying still works, but bringing it on-chain needs Coinos’s cooperation. The one exception: a coin whose payment history has grown very long is renewed free of the base fee, so it stays spendable.',
    arkCustodySelf: 'Minimize renewal cost',
    arkCustodySelfDesc: 'Coins renew in the last hours before they expire, when renewal is cheapest, so you can always exit on your own without Coinos. Roughly once every {days} days per coin. Coins too small to be worth the fee are left alone.',
    arkCustodyExit: 'Minimize exit cost',
    arkCustodyExitDesc: 'Every transfer adds a hop to a coin’s history, and a unilateral exit pays one on-chain transaction per hop. This renews a coin before it expires, and also as soon as its history is {hops} hops deep, so bringing any coin on-chain stays close to a fresh coin’s price. Only for coins worth it: never more than a tenth of a coin on its renewal.',
    arkCustodyCostNone: 'no fees',
    arkCustodyCostSelf: '{base} per renewal',
    arkCustodyCostExit: '{base} near expiry, {pct} + {base} when early',
    arkCustodySaved: 'Custody choice saved',
    arkCustodyNotHere: 'This server does not honor expired coins, so they renew automatically here.',
    arkExitNoneExitable: 'Every coin has expired, so there is nothing a unilateral exit can take on-chain: Coinos holds them now. Renewing restores your own exit; withdrawing with Coinos’s cooperation works as before.',
    arkCoinsRenewDescCustodial: 'With “Allow coins to expire”, nothing renews on its own. A renewal replaces the selected coins with a single fresh one and restores your own exit for them. Every renewal costs a flat {base} toward its on-chain fee, plus a share that depends on how close each coin is to expiry. The closer, the cheaper:',
    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: 'Exit',
    arkCoinsCoopTitle: 'Withdraw',
    arkCoinsWithdrawBtn: 'Withdraw {amount}',
    arkCoinsExitBtnAmt: 'Exit {amount}',
    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',
    profTabStreams: 'Streams',
    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',
    mediaServersTitle: 'Media servers',
    mediaServersHow: 'Blossom servers for chat attachments and post media, including videos. Enter up to 6 HTTPS server addresses, one per line. Custom servers replace the public upload defaults. Saved on this device; existing uploads stay where they are.',
    mediaServersDefault: 'Using public media servers',
    mediaServersSaved: 'Media servers saved',
    mediaServersInvalid: 'Enter 1–6 HTTPS server addresses without paths, credentials, queries or fragments.',
    mediaServersSaveFailed: 'Could not save media servers on this device.',
    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',
    onbGiftNameTitle: 'Now pick a username',
    onbGiftNameBody: 'Claim a name and get your own payment address, so people can send you bitcoin with just your name. Then choose a picture and a cover.',
    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.',
    nlAmberTap: 'Your signer app has another request to answer. Open it to continue.',
    nlAmberOpen: 'Open signer app',
    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',
    namesInvKeepOpen: 'Keep coinos open until this is paid. If it\u2019s paid while coinos is closed, the money goes back to the sender. It expires in an hour.',
    namesInvExpired: 'This invoice has expired. Make a new one when you\u2019re ready to be paid.',
    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.',
    nwcWorkerLog: 'Background activity log',
    nwcWorkerLogHint: 'What the wallet did while it was closed: wake-ups, payments, notifications. Copy it when reporting a problem.',
    nwcWorkerLogEmpty: 'Nothing recorded yet',
    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',
    nwcLimitsPer: 'Max {max}/payment · {budget} a {per} · {spent} {spentWhen}',
    nwcSpentDay: 'spent today',
    nwcSpentWeek: 'spent this week',
    nwcSpentMonth: 'spent this month',
    nwcBudget: 'Budget (sats)',
    nwcPeriod: 'Budget resets',
    nwcPerDay: 'Every day',
    nwcPerWeek: 'Every week',
    nwcPerMonth: 'Every month',
    nwcDay: 'day',
    nwcWeek: 'week',
    nwcMonth: 'month',
    nwcPeriodHint: 'Resets at midnight UTC: at the start of the day, the week (Monday) or the month.',
    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',
    lnAuthTitle: 'Log in with Lightning',
    lnAuthAskLogin: '{domain} is asking you to log in with your wallet.',
    lnAuthAskRegister: '{domain} is asking you to create an account with your wallet.',
    lnAuthAskLink: '{domain} is asking to link your wallet to your account there.',
    lnAuthAskAuth: '{domain} is asking you to confirm it’s you, with your wallet.',
    lnAuthHow: 'Your wallet signs their challenge with a key it keeps for this site alone. No payment is made, and nothing else about you is shared.',
    lnAuthGo: 'Log in',
    lnAuthGoRegister: 'Create account',
    lnAuthRetry: 'Try again',
    lnAuthSigning: 'Signing in to {domain}…',
    lnAuthDoneTitle: 'Logged in to {domain}',
    lnAuthDoneHint: 'Go back to their page — it should have let you in.',
    lnAuthFailed: 'They didn’t accept the login.',
    lnAuthNoKey: 'This account has no wallet key to log in with.',
    lnWithdrawTitle: 'Withdraw over Lightning',
    lnWithdrawOffer: '{domain} will pay you between {min} and {max} {unit}. How much?',
    lnWithdrawFixed: '{domain} will pay you {amount}.',
    lnWithdrawRange: 'Between {min} and {max} {unit}.',
    lnWithdrawGo: 'Withdraw',
    lnWithdrawAsking: 'Asking {domain} to pay…',
    lnWithdrawSent: '{domain} has your invoice for {amount}',
    lnWithdrawSentHint: 'It should arrive in a moment. You can leave this page; the payment will show in your history.',
    lnWithdrawPaid: 'Received {amount}',
    lnWithdrawNotPaid: 'They took the invoice but the payment didn’t arrive. Check with them before trying again.',
    lnWithdrawNoInvoice: 'Couldn’t make an invoice to be paid to.',
    lnWithdrawWatchOnly: 'This device can’t receive: the wallet is watch-only here.',
    lnWithdrawMemo: 'Withdrawal from {domain}',
    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;
}