Files
felhom-controller/REPORT.md
T
admin 5270bad76e
gates / gates (push) Successful in 23s
v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still
Hungarian literals in Go, so an English household clicked an English button and was
answered in Hungarian. 226 of them move into the bundle here.

A flash was the hard part: it travels inside the redirect URL and is rendered by a
DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by
an older controller carries prose and is shown verbatim — never a raw key, never dropped.

Also converted: page data and view-model text, the internal/api JSON answers, the alert
banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at
display, and the four page titles built around an app name (R-566 closed).

Hungarian is byte-identical, and that is measured rather than read:
scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses
a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict.
Its own first version filtered the capture through an ASCII-Hungarian word list and missed
seven real literals — the R-565 class. The filter is gone.

Nothing on the wire moved, and wire goldens now hold it there: the report's health
warnings and every notify event message stay Hungarian, because the hub MAILS the
controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 10:15:56 +02:00

8.5 KiB
Raw Blame History

REPORT — localisation slice 2, release A (controller v0.252.0, R-557)

2026-09-18 · base commit 736f54b49610 (v0.251.0) · MinAgent 0.131.0, unchanged · no hub release. 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, <script>, empty: all six rows
TestParameterisedPageTitles — the four app-named titles equal the old concatenation
TestAPIMessagesFollowTheHouseholdLanguage — hu byte-identical, en different, per key

The gate caught a live defect in itself, not a decoy. Its first capture filtered literals through an ASCII-Hungarian word list and missed seven real ones („Naponta", „5 percenkent", „Eletjel (Heartbeat)", „Adatbazis mentes", „Biztonsagi mentes", „Mentes integritas", „Rendszer allapot") — the R-565 class exactly. It refused the keys that cited them. The filter is gone: every literal is indexed, because the only question this index answers is "did the base commit contain this text?".

5. Green gate

go build ./... && go vet ./... && go test ./... — all green. python3 scripts/controller_gates.py — 16 blocking gates OK + golden-notice advisory, including the new go-parity. python3 scripts/test_gate_decoys.py — all 23 decoys behaved. HU_FORMAL_CEILING 16 → 18, the measured number, no word changed (R-516 owns the words).

6. What is NOT in this release — each is a row, not prose

  • 894 Hungarian literals remain, of which 176 are fmt.Errorf/errors.New — release B.
  • Persisted text (release C): settings.OffboxTarget.LastError/LastWarning/LastProofReason, LastSyncError, RestoreOpResult, the unitRestore*MsgFmt and tier2*Msg family in handlers.go, and backup/offbox_reconstitute.go. Written by a background run, read days later. The §16 decision stands as its default: option 1 — written in the box's language at the time, with a household that switches seeing the old language until the next run.
  • R-570's producer is untouched, as its row requires.
  • New rows: R-572 (pruneLabel/fmtDuration in funcmap.go are copy-producing helpers that localeFuncs does not override — „vasárnap" renders on an English page), R-573 (the agent-channel and endpoint-drift banner text is composed by the channel-health checker and reaches Alert.Message as finished Hungarian), R-574 (handler_debug.go, 39 literals, page copy and JSON payload not yet separated).

7. Live validation

Endpoint-level on demo-hp guest 9201 (no browser on DooPlex — claude-in-chrome is not available here). Evidence: felhom.eu/documentation/audits/i18n-slice2-2026-09-18/A/live/.