# REPORT — localisation slice 2, COMPLETE (controller v0.252.0 + v0.253.0 + v0.254.0, R-557) **2026-09-18 · base commit `736f54b49610` (v0.251.0) · MinAgent 0.131.0, unchanged · no hub release.** **Three releases: A (the sentences shown now), B (the 179 error messages), C (the saved notes + the globe).** Architecture read first and named: `felhom.eu/documentation/architecture/10-localisation.md` §1 (parity), §2 (mechanism), §5 (gates), §9 (the R-553 signals), §10 (this slice); `07-backup-architecture.md` for what the hub reads from a box; `05-hub-architecture.md` for what the hub composes itself. --- ## 1. Claims in the task that live source disproved — named first | the task said | source says | |---|---| | "`cloudflare/countries.go` is used by `report/types.go` (on the wire)" | **No.** `report/builder.go` puts country **CODES** on the wire (`AllowedCountries []string`); no country NAME leaves the box. `CountryName()` has no caller at all. So the names are display-only and were free to translate — which is what this release did. | | "the hub's dispatcher composes its own customer mail from event kinds (grep of `dispatcher.go` for `.Message`: nothing)" | **Wrong in substance.** `.Message` finds nothing because the message arrives as a PARAMETER, not a field. `FormatCustomerEmail` (hub `notify/templates.go` L167-210) uses `customerMessages[eventType]` when it has one and **falls back to the controller's message** when it does not — and appends it as „- Üzenet: %s" whenever the two differ. Several event types deliberately carry no entry so the controller's sentence IS the mail (hub `api/handler.go`, the notes at L2034/L2045/L2066). The conclusion (do not translate them) was right; the reason was not. | | "5 flash readers / 8 writers" | 8 writers, **8 read sites** — the three named plus `auth.go:100` (login), `server.go:498` (claim) and the `flash_error` twins. All eight converted. | | per-file counts (`handlers.go` 121, `offbox_handlers.go` 72, `storage_handlers.go` 48, `handler_debug.go` 40, `offbox_reconstitute.go` 41, `notify/notifier.go` 30, `api/router.go` 19) | measured at `736f54b49610`: **120 / 70 / 45 / 39 / 38 / 31 / 18**. Totals: **1 120** Hungarian literals, not 1 141. | | "`scripts/i18n_inventory.py`" (as a controller script) | it lives in **`felhom.eu/scripts/`**. Its Go-literal walker was copied into the new gate rather than imported — a controller clone need not sit beside felhom.eu, and a gate that can fail to import its scanner can silently skip. | ## 2. What shipped **226 Go literals converted**, plus 237 country names now available in English. 426 keys, each recorded in `scripts/i18n_go_keys.json` against the literal it replaced. | file | before | after | converted | of the rest, `fmt.Errorf` (release B) | |---|---|---|---|---| | `internal/web/handlers.go` | 120 | 44 | 76 | 0 | | `internal/web/offbox_handlers.go` | 70 | 22 | 48 | 0 | | `internal/web/storage_handlers.go` | 45 | 19 | 26 | 16 | | `internal/api/router.go` | 18 | 0 | 18 | 0 | | `internal/web/share_handlers.go` | 22 | 5 | 17 | 0 | | `internal/web/sharing_handlers.go` | 18 | 3 | 15 | 1 | | `internal/web/alerts.go` | 13 | 0 | 13 | 0 | | `internal/web/tier2_config_handler.go` | 8 | 3 | 5 | 0 | | `internal/api/geo.go` | 5 | 0 | 5 | 0 | | `internal/web/backup_handlers.go` | 12 | 9 | 3 | 0 | | `internal/cloudflare/countries.go` | 113 | 113 | 0 (the TABLE stays — it validates codes) | 0 | Mechanism: `i18n.Bundle.Msgf`; `web.(*Server).msg/msgLang/msgN`; `api.(*Router).msg/msgLang/langFor/countryName`; `flashQuery`/`flashText`/`flashFrom`; `Alert.MessageKey`/`MessageArgs`/`LinkTextKey` + `Alert.rendered(lang)`; `TitleArgs` for the four app-named titles (R-566, closed). **Word order without a second syntax.** The task asked for a named-parameter (`{{.Name}}`) form for multi-parameter messages. It is not here, deliberately: English reorders with Go's own explicit argument indexes (`%[2]s`), which `fmt` already understands. The Hungarian value then stays the format string the code always had, byte for byte — which is exactly what the parity gate compares, so the measurement needs no exception. `TestBundleParametersMatchAcrossLanguages` counts an indexed verb as one verb, so a reordered English still has to use the same verbs on the same values. ## 3. The wire — surveyed, frozen, not translated (A.1) Everything a HUB reads keeps its bytes. Filed against **R-558** (slice 3 owns it): | producer | where it lands | why it stays | |---|---|---| | `internal/monitor/healthcheck.go` — 5 storage sentences | report `health.warnings` / `health.issues` | the hub stores and shows them to the operator | | `internal/notify/notifier.go` — 31 event messages | event `message` → `FormatCustomerEmail` | **the household reads these in an e-mail**; several types have no `customerMessages` entry precisely so this sentence is the mail | | `internal/cloudflare/countries.go` — the table | nothing (codes only) | it validates codes; the DISPLAY name is what follows the language | Pinned by `TestStorageWarningWireTextIsFrozen`, `TestStorageWarningFormatsAreFrozen`, `TestEventMessageWireTextIsFrozen`, `TestEventMessagesCarryNoBundleKey`. Both goldens were captured from the producers at the base commit, before any change, and both were seen to fail. The **persisted** text found by the same survey is listed in §6 for release C. ## 4. Tests and their red-proofs Every one was run against a planted break and watched to convict; the planted change was then reverted and the suite re-run green. Full log: `audits/i18n-slice2-2026-09-18/redproofs.txt`. | test / gate | what was planted | what convicted | |---|---|---| | `i18n_go_parity.py` | one byte added to a hu.json value | `CHANGED`, naming the key and the base literal | | `i18n_go_parity.py` | a key citing text no literal has | `INVENTED`, naming the text | | `i18n_go_parity.py` | a joined key with reworded fragments | `INVENTED`, naming the fragment | | `test_gate_decoys.py` go-parity ×3 | reworded / invented / unlisted | all three rejected | | `TestEventMessageWireTextIsFrozen` | „telepítve" → „telepitve" | got/want printed | | `TestStorageWarning*IsFrozen` | a wire warning translated | both tests | | `TestFlashOnAServerWithNoBundleField` | `s.bundle()` without the `i18n.Shared` fallback | the page showed `flash.share.enabled` | | `TestFlashKeyRoundTrip` | — | legacy prose, unknown key, `