| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
| |
The claim flow already generated a fresh wallet, so the dead-end screen now lets
the opener keep it (→ back up the recovery phrase → into the wallet) or head to
the home screen. The backup step's copy adapts (Claimed! vs Your new wallet).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Save the wallet-screen nav (tab and open tx detail) to sessionStorage on each
render and restore it on load, so refreshing keeps your place instead of jumping
back to Receive. A restored tx detail re-fetches its fee/details if missing.
Gift links still open the claim screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
A watcher-credited receive arrives with no fee (the push can't compute it from
the raw tx, and the reduced reconcile doesn't revisit the now-used address). On
opening the tx detail, fetch the full tx from the explorer to fill in the fee
(and confirmations) — works for pending mempool txs too.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Persist the receive-ack the moment the 'Payment received!' screen is shown
(not only on tap), so refreshing or navigating away without tapping won't bring
it back. It still stays visible in the current session until dismissed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gift links now carry a compact binary encoding of just the claim-essential data
(presigned inputs + the committed change output) instead of a full PSBT (~25%
fewer bytes), base32-encoded and served at an uppercase /g/<code> path. The QR
renders the uppercase URL in the QR alphanumeric mode (5.5 bits/char vs byte
mode's 8), so the grid drops from ~73 to ~53 modules — much easier to scan.
- wallet: _encodeCompactGift/_decodeCompactGift + _giftTx dispatcher; legacy
base64url-PSBT links still claim (auto-detected by a magic byte). Verified
round-trip + claim-byte-identical across 1/2/3-input and whole-balance gifts,
against the real exported preview/claim/outpoint functions.
- app: giftUrl uses /g/<code>; opener parses /g/ (and legacy #gift=); QR uses
uppercase + Alphanumeric mode (qrSvg gains a mode option w/ byte fallback).
- nginx already SPA-routes /g/*; the SW serves the shell for the new path.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Avoids flashing 'You've been sent Bitcoin' before flipping to 'Already claimed'
for a spent gift. Offline fallback clears the checking flag so the loader can't
hang.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Opening a gift link now checks whether its funding coin is already spent — via
the Esplora outspend endpoint, which reports a spend even by an unconfirmed
claim. The Claim button is gated until that check passes; if the coin is spent,
show an 'Already claimed' screen with a link to the claim tx instead of a
claimable amount. doClaim also re-checks on broadcast failure (the race window),
and refuses to run while unchecked/taken — so a second opener can't attempt a
doomed double-spend.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The WS badge reads Live but mempool.space's free address tracking doesn't
reliably push incoming mempool txs, so a deposit could take ~18s to appear (the
25s safety poll catching it). While the user is on the Receive tab (online), now
poll just the fresh receive address every ~3s regardless of Live state — one
cheap request that guarantees near-instant arrival. Toggled idempotently from
render() based on the active tab; stops when they navigate away.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The refresh request storm (and the 429s) was a full API re-scan running on every
load even when the local cache or Nostr already held the complete state. Now the
full scan runs ONLY when there's neither cache nor Nostr (a seed on a fresh
device); otherwise the socket + frontier poll keep things current and reconcile
held coins — a handful of requests instead of re-querying every historical
address plus its utxos and txs.
Also reordered: startRealtime() now runs BEFORE syncFromNostr (which blocks up to
6s waiting on the relay) and before any discovery scan, so the wallet goes Live
immediately instead of sitting in Polling. retrack() after sync/scan re-subscribes
to the final frontier; a fresh open re-baselines its celebration against it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pending tx is judged 'stuck' once it has waited ~10 blocks (≈100 min) and
its fee rate (fee/vsize) is below the mempool's current floor (minimumFee) —
i.e. it's been purged and won't confirm without a bump. _txSummary now records
vsize and a firstSeen timestamp (persisted in cache) to support this.
Stuck txs are dropped from active monitoring: excluded from the watched-address
set and from the fast-poll trigger, so they don't hold a socket slot or keep us
in the 6s lane. The slow ~25s reconcile still catches one if the floor later
drops and it confirms. History flags them (⚠ stuck) and the detail view explains
it — pointing outgoing ones at the existing fee bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- watchedAddresses() now includes any address holding an unconfirmed coin (not
just the frontier), so a pending deposit's confirmation pushes over the socket
instantly. Capped at the socket's 10-address limit. refreshLive/rescanAddress
retrack when a new pending coin changes the set.
- While any coin is pending, the frontier poll runs every ~6s even when live, so
confirmations show fast as a belt-and-suspenders to the WS push.
- Per-address rescans no longer disable each other: clicking many queues them,
and the api.js global scheduler (1 concurrent, 500ms spacing, 429 backoff)
serializes and staggers the underlying requests within the rate limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Remove the periodic full re-scan entirely — the WS and the frontier poll keep
the balance current, and refreshLive's _reconcileHeld catches spends of held
coins. The only gap (a deposit to a reused old address) is now handled
explicitly: Settings → Rescan opens a paginated list of every known address,
each with its own Rescan button (wallet.rescanAddress re-queries just that one).
Replaces the full 'Rescan wallet' button.
This sharply cuts recurring REST volume (the main rate-limit driver). Trade-off:
when the socket is down, a pending tx's confirmed badge in History may lag until
the WS reconnects or you rescan — the balance itself stays correct via reconcile.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
preventDefault() without a following prompt() makes Chrome log a 'Banner not
shown' warning. Modern Chrome shows no auto-banner to suppress, so we just
stash the event for our own install link without preventing the default.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Incoming deposits show instantly when the mempool.space WebSocket is healthy
(_ingestWs updates immediately). The lag came from the socket being starved:
a 20s frontier poll plus a full wallet re-scan every 120s generated enough
REST volume to trip mempool's per-IP rate limit (the 429/CORS errors), which
in turn degraded the socket — a feedback loop.
- While the socket is live, skip the periodic full scan entirely (the WS pushes
confirmations and block events) and poll the frontier only every ~25s as a
half-open-socket safety net — near-zero recurring REST.
- While the socket is down, poll the frontier every ~6s (was 20s) so receives
still show quickly, and keep the 120s reconcile scan.
- Refresh the frontier on WS (re)connect to catch deposits that landed while
disconnected.
- Surface a Live/Polling badge on the balance card so a stuck socket is visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A gift PSBT commits a sender change output (>= dust), so the entered amount
always needed gift + 294 — making it impossible to gift the entire balance.
Add a Max toggle on the gift form that builds a no-change sweep: all inputs
SIGHASH_NONE, no change output, so the recipient gets the whole balance minus
their claim fee. previewGift/buildClaimTx now treat a zero-output PSBT as
zero change. A specific-amount gift that can't keep a dust change now points
the user to Max instead of failing with a confusing 'not enough funds'.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
- Spendable headline no longer counts pending incoming receives: compute it
from UTXOs (confirmed coins + our own unconfirmed change, minus gift locks)
so a pending spend still debits immediately but a pending receive shows only
on the PENDING line (new pendingIncoming getter).
- Claim button shows the fee in parens in slightly smaller font, no dot.
- Gift recipients now get the same "Verify backup" / "Skip verification" flow
as new-wallet creation instead of a single "I've written it down" button.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Outstanding gifts no longer dominate the History page — show at most 3 inline
with a 'View all gifts (N)' button that opens a paginated gift-only list.
Transactions paginate 10 per page with prev/next controls. Both reuse a shared
pager helper; the all-gifts sub-view resets when leaving the History tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Unclaimed gift links now appear at the top of the History list (they aren't
on-chain transactions until claimed or revoked). Each is tappable to cancel
via the same reclaim/revoke flow as the Send → gift card, reusing ui.revokeId
and the doReclaim/doRevoke handlers.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gift split no longer makes the sender wait for confirmation: createGiftFromSplit
broadcasts the carve-out, waits only until it's visible in the mempool, then
builds the gift spending that still-unconfirmed coin. Funding + claim form a
chain that confirms together; the recipient sees a pending balance until then.
Polls for mempool visibility before reserving so a background refresh can't drop
a not-yet-indexed coin and prune the reservation (double-spend risk).
Regular sends can already draw on unconfirmed coins; surface that honestly:
a 'pending' tag on unconfirmed coins in coin control, and a plain-language
warning at review when a payment spends unconfirmed bitcoin (won't confirm
until the ancestor does).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gift coin selection now picks the smallest single coin that covers the gift
(was largest-first), locking the least value until claim. When no near-exact
coin exists, offer to pre-split: broadcast a self-send carving out a gift+dust
coin so the gift later locks only ~the gift amount.
The split is only offered when the liquidity it frees (lock - gift - dust)
exceeds the split's own fee, so it never costs more than it regains. Decision
is surfaced as an interstitial on Create, since a full re-render drops input
focus and a live preview isn't viable.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Coins reserved behind unclaimed gift links were already excluded from
spending, but the headline balance still counted them. Show the spendable
balance (total minus gift locks) as the headline and surface the locked
amount as its own line, so a gift debits immediately rather than at claim.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Previously createGift reserved a claim-fee headroom on top of the entered
amount based on the fee rate at creation. Because the fee was only actually
charged at claim time, fee-rate drift between the two moments silently ate
into (or padded) the recipient's amount.
Now the entered amount is exactly the recipient's full received total (sum of
inputs less our change); no fee is reserved. The claim screen shows that full
total as the headline and surfaces the live network-fee estimate on the Claim
button, where it's deducted from the recipient's balance at claim time.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
- warn-box, addr-box, summary, toast and green badge/notice borders used
hardcoded light colors that washed out or vanished in dark mode; route
them through theme variables (new --toast-bg/-fg and --green-line pairs).
- Drive the footer "Install app" link from a persisted localStorage flag
instead of the live beforeinstallprompt event so it's present on first
paint after a refresh, eliminating the layout shift.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Dark mode: theme variables + a footer "Dark mode/Light mode" toggle,
persisted and applied pre-paint via a small head script (defaults to the
OS preference). Hardcoded surfaces moved to CSS variables.
- Gift amount: the unit label is now the clickable sats/BTC toggle, and the
amount is parsed in the selected unit.
- Gifts can now span multiple coins: createGift selects several confirmed
coins (first input SIGHASH_SINGLE to lock the change, the rest
SIGHASH_NONE), so a gift larger than any single coin works. Claim/preview
sum all inputs and size the fee by input count. Verified end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
| |
It now lives in the global footer, available on every screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Defense-in-depth so the "payment received" screen can never fire for an
old payment on opening a wallet: in addition to the ack baseline, require
an incoming tx that is pending or confirmed within the last ~2 hours.
Verified it still celebrates genuine new (pending/recent) payments.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
| |
It was redundant with the "Saved · forget" link shown beneath the name.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
- The Accounts action is now "Clear all", which prompts a confirmation
warning that any wallet whose recovery phrase/key isn't backed up will
be lost. Confirming wipes everything from the device: session accounts,
the encrypted vault, and saved watch-only accounts.
- Each account has a rename (✎) control: edit the label inline and save.
Persists to the session, the watch-only store, and the vault as needed.
New strings translated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Home screen is back to two tabs (Create / Import). The Import box now
takes a recovery phrase, an xpub/zpub, or an xprv/zprv:
- mnemonic → full wallet (as before)
- xpub/zpub → watch-only (view/receive, no Send)
- xprv/zprv → full spending wallet with no mnemonic. parseExtendedKey
classifies + normalizes version bytes; account() derives the BIP84
path from a master xprv (depth 0) or uses an account-level xprv as-is;
signing works (only watch-only blocks it). Vault persists xprv accounts
too; Settings shows an "Imported key" note instead of a recovery card
and hides Nostr sync (no seed). Public-key export still works.
Note: a seed phrase cannot be derived from an xprv (one-way KDF), so an
xprv wallet has no recovery words by design. New strings in all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The "payment received" screen is for payments that arrive while you're
using the wallet, but the seen-index is persisted per-seed, so
re-importing/unlocking/switching to a wallet used before would resurface
the celebration for a payment that had already arrived (and confirmed)
in between. Opening a wallet (import, unlock, account switch, watch-only
add, fresh-session restore) now baselines the ack to the current receive
frontier; only a same-session refresh keeps the persisted ack so an
unacknowledged live celebration survives the refresh.
Verified: with a stale persisted ack, a fresh import shows no
celebration and re-baselines to the current frontier.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Per-account "Save on this device" encrypts a seed account into a
localStorage vault (scrypt KDF + XChaCha20-Poly1305 via @noble), so it
survives a browser close. On reopen, a vault prompt unlocks all saved
wallets with the password (or "Use another wallet" to skip). Default
stays session-only; the password is held only in memory for the session
and re-encrypts on save/forget. Wrong password fails closed (AEAD).
Adds @noble/ciphers dependency. New strings translated across all 19
locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Accounts: a switcher (header chip → Accounts screen) lets you keep
several wallets loaded and flip between them without re-importing. Per
the chosen model, full (seed) accounts live only in sessionStorage —
ephemeral, no seed on disk; watch-only accounts (no seed) are persisted
in localStorage and reload across restarts. enterWallet is refactored
around an accounts list (add/switch/remove/lock); Add wallet reaches the
unlock screen with a back button.
Watch-only: load any wallet from its account xpub/zpub (parseAccountKey
normalizes zpub→xpub; the existing public-derivation path yields the
same addresses). Signing is blocked, the Send tab and seed/sync/offline
cards are hidden, and a watch-only note is shown. Settings gains a
"Public key" card (export this wallet's zpub) so it can be watched
read-only elsewhere.
Password-encrypted persistence for full accounts is the planned next
step. New strings translated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Minimum: the flat 546-sat floor is replaced by a fee-aware minimum
(dust + one claim fee of headroom, floored at 546) via giftMinimum(),
so the recipient clears dust comfortably even if fees rise before they
claim — e.g. ~1000 sats at 5 sat/vB, ~7300 at 50. The create form shows
the live minimum and rejects below it; buildClaimTx keeps the hard
294-sat dust guard as a final backstop (claim fails rather than handing
over dust).
UI: the gift entrypoint moves from Settings to a "Gift some Bitcoin"
link on the Send page, opening the gift view as a send sub-view with a
back button.
New strings translated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Passive reclaim previously dropped the coin from tracking, so there was
no way to revoke afterward. Now reclaim moves the coin from "reserved"
to a "reclaimed" set: it becomes spendable, but its still-live link
stays listed (tagged "still claimable") with a Revoke option, until the
coin is actually spent (by a payment or by revoking), at which point it
drops off. revokeGift and outstandingGifts handle both sets; existing
reserved gifts are unaffected. Strings updated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The reserved-coin button is now "Reclaim". Tapping it prompts a choice:
- Reclaim (free): just un-reserves the coin so a future payment can
spend it — no fee, but the link stays claimable until then.
- Revoke now (pays a fee): sweeps the coin back immediately, killing
the link once it confirms.
New/updated strings translated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The old "Reclaim" only un-reserved the coin locally, so the gift link
stayed claimable. Replace it with "Revoke": a confirmed action that
sweeps the reserved coin back into the sender's own wallet at a high
fee (wallet.revokeGift), double-spending the gift's presigned input so
the link can't be claimed once it confirms. Races a simultaneous
claimer; if the gift was already claimed the broadcast just fails.
New strings translated across all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gifting no longer shares a seed. "Gift some Bitcoin" (Settings) presigns
a chosen amount as a #gift= link: one SIGHASH_SINGLE input (the sender's
change output is cryptographically fixed, so a front-runner can only take
the gift portion, never the change) plus the rest left open. The coin is
reserved per-wallet so normal sends skip it; reclaim it anytime.
Opening the link generates a brand-new wallet the sender never sees,
shows the amount, and on Claim adds the claimer's address as the output,
finalizes, and broadcasts — then prompts backup of the fresh seed. This
gives forward secrecy (sender locked out once confirmed), one-shot
semantics (claiming spends the input), and keeps funds in the sender's
wallet until claimed.
Crypto verified end-to-end (sign NONE/SINGLE -> add output -> finalize ->
valid tx; deterministic-signature equality proves the presigned sig is
valid for any output the claimer picks). Strings retranslated across all
19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Create a #claim=<code> link from Settings → "Gift this wallet" (gated
behind a reveal + warning, with a QR): the seed (and passphrase, if any)
are encoded as BIP39 entropy in base64url in the URL fragment, which
never reaches a server. Opening such a link loads that wallet and shows
a welcome → balance → firm "back up your recovery phrase" flow, then
drops into the wallet. The claim code is scrubbed from the address bar
on load.
Bearer by design — whoever opens the link controls the funds — so the
creator UI warns to keep amounts small. New strings translated into all
19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Bump now opens its own screen with the Economy/Normal/Priority/Custom
fee tiers (defaults to Priority) and a live "fee: old → new" preview as
you change the rate, then Replace builds + signs + broadcasts. Refactor
bumpTx into prepareBump (async fetch/reconstruct) + planBump (pure
preview) + buildBump (sign), so the picker can preview each rate without
rebuilding. New bumpInsufficient string translated into all 19 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Signal opt-in RBF (BIP125, sequence 0xfffffffd) on all new sends so
they're replaceable.
- "Bump fee" on an unconfirmed sent tx (its detail view): rebuilds the
same inputs + recipient outputs at the Priority rate, taking the extra
fee from the change output (pulling in a confirmed coin if change can't
cover it), re-signs, and routes to a "Replace with a higher fee?"
confirmation showing old → new fee before broadcasting.
- New keys translated into all 19 locales.
Honest scope: P2A/TRUC/package-relay are L2/pre-signed-tx + node-relay
features that don't apply to a single-sig wallet broadcasting single txs
via public explorers, so they're intentionally out. CPFP (for receives /
unbumpable sends) is a possible follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Only English is bundled in index.html now; the other 19 languages are
emitted as dist/locales/<code>.json and fetched on demand when a visitor
actually uses one (via auto-detect, the picker, or ?lang=xx). This drops
index.html from ~511KB to ~318KB; a non-English visitor fetches just
their one ~8-10KB locale, which the service worker then caches for
offline use. t() falls back to English until a locale resolves.
- i18n.js keeps only en + a loadLocale() fetcher; locale data moved to
src/locales/*.json.
- build.js emits dist/locales/; dev.js serves /locales/<code>.json.
- Deploy hook publishes the locales/ subdir.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|