# 10 — The product in more than one language > **LIVING DOCUMENT. Every localisation slice updates this file in the same session.** > Opened 2026-09-17 by the localisation starter, AFTER its spike ran (controller v0.247.0). Before it, > no architecture document mentioned localisation at all (a grep of `architecture/*.md` for > `i18n|locali|language` found one incidental line in 07). > > Marks, as in 07 and 02: **[DESIGN]** — a decision taken, not derived from code. **[FACT]** — observed, > with a citation. Unmarked means not yet classified. **This file carries the REASONING. The register (`backlog/OPEN-ITEMS.md`, R-553..R-562) carries the work. The numbers are in `audits/I18N-INVENTORY-2026-09-17.md`. The source is the truth.** --- ## 1. The rule that makes this safe **[DESIGN] The Hungarian product must render byte-for-byte the same before and after every slice — measured page by page, never by reading.** A household that never switches must not be able to tell a localisation release happened. English goes on top of an unchanged Hungarian product. **[FACT]** It is enforceable, and enforced for the three converted pages: `TestI18nParity` (`felhom-controller/controller/internal/web/i18n_parity_test.go`) renders 14 fixture states of the launcher, `/backups` and `/apps/` in Hungarian and compares them with HTML captured from a clean worktree of `origin/main` 89dd3e94b1de **before** any template carried a marker. One changed byte in `hu.json` fails it naming the page and the line (red-proof, `felhom-controller/REPORT.md`). **Fixtures are captured from unconverted templates and never regenerated to make a conversion pass** — that is the release gate for every slice. The one normalisation: relative ages („3 napja") become „# napja", because they come from the wall clock. **[FACT] And live, on demo-hp guest 9201 (2026-09-17, endpoint-level):** the launcher, `/backups` and `/apps/privatebin` fetched in Hungarian on 0.246.0 and again on 0.247.0 are **identical apart from the version string and the CSRF token**, with equal raw byte counts (44 462 / 46 250 / 42 081), and again after a round trip through English (`audits/i18n-2026-09-17/live/`). --- ## 2. The mechanism, as measured ### 2.1 What was chosen, and why **[DESIGN] A flat message bundle per language, expanded into the template BEFORE `html/template` parses it.** Decided by CC in the spike (the starter delegated the choice, §4 1.1 of the task). | option | cost | verdict | |---|---|---| | (a) `golang.org/x/text/message` catalogs + a `T` func | a new external dependency; its value is gender/plural/ordinal selection | **not needed**: the inventory found no gender agreement and only count plurals; Hungarian does not inflect after a numeral | | (b1) flat bundle, **runtime** `T` template func | every string passes through the contextual escaper: `+`, `'`, `"` change bytes; inside `