i18n: add Portuguese (pt-BR) to the bundled core - #79
Merged
Conversation
nyblnet
force-pushed
the
claude/i18n-compact
branch
from
July 26, 2026 00:17
7a76afa to
3a76e8b
Compare
Portuguese joins the bundled core per docs/i18n-packs.md: Brazil has a genuine English-proficiency gap — unlike the markets where a localized UI mostly goes unused — and pt sits in the cheapest cost tier. It is the only language being added to the core; everything else becomes a pack. Brazilian terminology throughout: arquivo (not ficheiro), tela (not ecrã), salvar (not guardar), compartilhar (not partilhar). pt-PT aliases here for now — European Portuguese diverges enough to deserve its own catalog, and it is a natural first PACK once that machinery exists (noted in the file header). Five strings were generated programmatically from the canonical keys rather than hand-typed: the markdown-help and embed-size confirms carry backslash and newline escaping, and two PAIRS of them differ from each other ONLY in that escaping. A mistyped escape produces a key that silently never matches, so hand-copying them was the one part worth not doing by hand. COST: +11,832 B (+2.2%), close to the ~14 KB per-language rule and in the cheapest tier as predicted. With the key-once packing this branch builds on, the shell is still 44,695 B (-7.5%) SMALLER than main today. VERIFIED: all 650 strings resolve through the REAL lookup path — zero fell back to English, zero mismatched against the source catalog. Picker shows 9 locales; pt, pt-BR and pt-PT all resolve. Interpolation and inline HTML survive. Confirmed visually in the running editor. build-i18n.mjs --check current, splice conformance gate passes, tsc -b clean. Machine-drafted, native review welcome — as the file header says, and as every other catalog here says.
…open #72, #73, #74 and #82 each landed new UI strings after this catalog was written. Without these twelve, Portuguese would have silently fallen back to English on the save-capability wording, the release-notes link, Start from scratch and the morph unpair option. 662/662 now, verified against the packed table.
nyblnet
force-pushed
the
claude/i18n-portuguese
branch
from
July 26, 2026 00:20
f3038cb to
877993c
Compare
nyblnet
added a commit
that referenced
this pull request
Jul 26, 2026
Builds the machinery from docs/i18n-packs.md so that adding a language stops
costing shell bytes. Korean is the first pack and the proof it works.
WHY THIS BEFORE MORE CONTENT. Every i18n change in this series was verified
the same way: call t() through the REAL lookup path and confirm it returns a
translation rather than the English fallback. Without a loader that check is
impossible, so generating catalogs first would mean thousands of machine-
drafted strings with ZERO evidence any of them resolve — and a mistyped key
falls back to English silently, forever. Pipeline first makes every language
after this verifiable.
KERNEL. addPack() layers a pack over the bundled core, keyed by language.
- Packs are consulted BEFORE the core, so a pack can also CORRECT a bundled
language without cutting an app release — which is what turns native
review into a continuous process instead of a release blocker.
- Deliberately tolerant: a pack built against an older app simply lacks the
newer keys, and each miss falls through to the core and then to English.
A stale pack degrades string by string instead of failing to load.
- Rejects a pack built for a DIFFERENT app: its keys would be wrong, not
merely missing, which is the one case worth refusing outright.
- registerI18n re-appends pack languages to the picker, because block order
in the shell isn't guaranteed and a pack loaded BEFORE registration would
otherwise lose its entry.
BUILD. `build-i18n.mjs --packs <dir>` emits one JSON per language from
slides/src/i18n/packs/*.ts — authored exactly like a core catalog, but nothing
imports that directory so it never enters the module graph. It errors if a
language is listed as both bundled and packed.
COST: the pipeline adds 200 B to the shell. The Korean pack is 7,207 B and
lives entirely OUTSIDE it.
VERIFIED end-to-end in the browser:
before loading ko absent from the picker; t('Delete slide') = English
after addPack() ko in the picker labelled 한국어; '슬라이드 삭제', '저장',
'공유'; interpolation intact ('슬라이드 4')
degradation 'Version history…' — a key this partial pack does NOT
carry — correctly returns English while carried keys
resolve in Korean
wrong app a pack marked app:'spaces' is rejected
Korean is absent from packed.ts (not bundled). Core --check current, splice
conformance gate passes, app + kernel typecheck clean.
KOREAN IS PARTIAL: 262 of 650 strings, covering the most visible UI. That is
a VALID pack by design — the rest fall back per string — and completing it is
the obvious next step. Issue #17 is a volunteer offering to write this
catalog; they should be offered review and ownership of it.
Stacked on #79 (Portuguese) -> #75 (key-once packing).
3 tasks
YishenTu
pushed a commit
to YishenTu/bento
that referenced
this pull request
Jul 26, 2026
Next item on the i18n-packs checklist: packs now ship. release.mjs emits every non-core language beside the shell, and each pack's sha256 goes INTO the signed manifest payload. NO SECOND KEY, NO SECOND SIGNATURE. The manifest signature already covers the whole payload string, so listing pack hashes there means they are signed too. A client verifies the manifest once, then checks a downloaded pack against its signed hash — the same two-step the shell itself goes through. A separate pack-signing path would be a new trust root to get wrong for no gain. VERIFIED end-to-end with a THROWAWAY key (the real key at ~/.bento/release-key.json was never touched, and the test key is deleted): 1. manifest signature valid 2. packs present in the signed payload — ko (한국어), 60,114 B 3. signed hash matches the emitted pack file 4. pack tampered -> hash mismatch, so a client rejects the download 5. manifest tampered to swap that hash -> signature fails That is the whole chain: you cannot substitute a pack without breaking the hash, and you cannot fix the hash without breaking the signature. Also recorded in docs/i18n-packs.md, measured earlier and previously unwritten: a pack must be COMPRESSED when spliced into a file — 56.8 KB raw against 26.4 KB through the shell's existing deflate+base64 blocks. The CDN artifact stays uncompressed (Pages gzips it in transit, and a readable .json is worth keeping for translators), but a spliced pack lives in every saved copy of that file forever, so 30 KB per file per language would be permanent and unfixable once files are in the wild. Status list refreshed — it still showed nyblnet#75/nyblnet#79/nyblnet#81 as unbuilt — and the note about shell-gate.mjs corrected: it deliberately checks only the splice contract, and passed a build with a silently corrupted stylesheet, so pack-carrying coverage belongs in a separate smoke check, not there.
nyblnet
added a commit
that referenced
this pull request
Jul 27, 2026
i18n: add Portuguese (pt-BR) to the bundled core
nyblnet
added a commit
that referenced
this pull request
Jul 27, 2026
Builds the machinery from docs/i18n-packs.md so that adding a language stops
costing shell bytes. Korean is the first pack and the proof it works.
WHY THIS BEFORE MORE CONTENT. Every i18n change in this series was verified
the same way: call t() through the REAL lookup path and confirm it returns a
translation rather than the English fallback. Without a loader that check is
impossible, so generating catalogs first would mean thousands of machine-
drafted strings with ZERO evidence any of them resolve — and a mistyped key
falls back to English silently, forever. Pipeline first makes every language
after this verifiable.
KERNEL. addPack() layers a pack over the bundled core, keyed by language.
- Packs are consulted BEFORE the core, so a pack can also CORRECT a bundled
language without cutting an app release — which is what turns native
review into a continuous process instead of a release blocker.
- Deliberately tolerant: a pack built against an older app simply lacks the
newer keys, and each miss falls through to the core and then to English.
A stale pack degrades string by string instead of failing to load.
- Rejects a pack built for a DIFFERENT app: its keys would be wrong, not
merely missing, which is the one case worth refusing outright.
- registerI18n re-appends pack languages to the picker, because block order
in the shell isn't guaranteed and a pack loaded BEFORE registration would
otherwise lose its entry.
BUILD. `build-i18n.mjs --packs <dir>` emits one JSON per language from
slides/src/i18n/packs/*.ts — authored exactly like a core catalog, but nothing
imports that directory so it never enters the module graph. It errors if a
language is listed as both bundled and packed.
COST: the pipeline adds 200 B to the shell. The Korean pack is 7,207 B and
lives entirely OUTSIDE it.
VERIFIED end-to-end in the browser:
before loading ko absent from the picker; t('Delete slide') = English
after addPack() ko in the picker labelled 한국어; '슬라이드 삭제', '저장',
'공유'; interpolation intact ('슬라이드 4')
degradation 'Version history…' — a key this partial pack does NOT
carry — correctly returns English while carried keys
resolve in Korean
wrong app a pack marked app:'spaces' is rejected
Korean is absent from packed.ts (not bundled). Core --check current, splice
conformance gate passes, app + kernel typecheck clean.
KOREAN IS PARTIAL: 262 of 650 strings, covering the most visible UI. That is
a VALID pack by design — the rest fall back per string — and completing it is
the obvious next step. Issue #17 is a volunteer offering to write this
catalog; they should be offered review and ownership of it.
Stacked on #79 (Portuguese) -> #75 (key-once packing).
nyblnet
added a commit
that referenced
this pull request
Jul 27, 2026
Next item on the i18n-packs checklist: packs now ship. release.mjs emits every non-core language beside the shell, and each pack's sha256 goes INTO the signed manifest payload. NO SECOND KEY, NO SECOND SIGNATURE. The manifest signature already covers the whole payload string, so listing pack hashes there means they are signed too. A client verifies the manifest once, then checks a downloaded pack against its signed hash — the same two-step the shell itself goes through. A separate pack-signing path would be a new trust root to get wrong for no gain. VERIFIED end-to-end with a THROWAWAY key (the real key at ~/.bento/release-key.json was never touched, and the test key is deleted): 1. manifest signature valid 2. packs present in the signed payload — ko (한국어), 60,114 B 3. signed hash matches the emitted pack file 4. pack tampered -> hash mismatch, so a client rejects the download 5. manifest tampered to swap that hash -> signature fails That is the whole chain: you cannot substitute a pack without breaking the hash, and you cannot fix the hash without breaking the signature. Also recorded in docs/i18n-packs.md, measured earlier and previously unwritten: a pack must be COMPRESSED when spliced into a file — 56.8 KB raw against 26.4 KB through the shell's existing deflate+base64 blocks. The CDN artifact stays uncompressed (Pages gzips it in transit, and a readable .json is worth keeping for translators), but a spliced pack lives in every saved copy of that file forever, so 30 KB per file per language would be permanent and unfixable once files are in the wild. Status list refreshed — it still showed #75/#79/#81 as unbuilt — and the note about shell-gate.mjs corrected: it deliberately checks only the splice contract, and passed a build with a silently corrupted stylesheet, so pack-carrying coverage belongs in a separate smoke check, not there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Portuguese to the bundled core, per
docs/i18n-packs.md(#77).Based on #75, not
main— it regeneratespacked.ts, so it needs the packing infrastructure. Merge #75 first; this PR targets that branch and will retarget tomainonce it lands.Why Portuguese, and only Portuguese
Brazil has a genuine English-proficiency gap — unlike the markets where a localised UI mostly goes unused — and
ptsits in the cheapest cost tier. It's the only language joining the core; everything else becomes a pack.Brazilian terminology throughout:
arquivo(not ficheiro),tela(not ecrã),salvar(not guardar),compartilhar(not partilhar).pt-PTaliases here for now. European Portuguese diverges enough to deserve its own catalog, and it's a natural first pack once that machinery exists — noted in the file header.Cost
+11,832 B (+2.2%) — close to the ~14 KB per-language rule established in #77, and in the cheapest tier as predicted.
With #75's key-once packing underneath, the shell is still 44,695 B (−7.5%) smaller than main today.
Verification
pt,pt-BRandpt-PTall resolve{placeholder}interpolation and inline HTML survivebuild-i18n.mjs --checkcurrent · splice conformance gate passes ·tsc -bcleanOne thing worth reviewing
Five strings were generated programmatically from the canonical keys rather than hand-typed. The markdown-help and embed-size confirms carry backslash and newline escaping, and two pairs of them differ from each other only in that escaping. A mistyped escape yields a key that silently never matches — so those were the one part worth not doing by hand.
Machine-drafted, native review welcome — as the header says, and as every other catalog here says.