179 Hungarian sentences were built deep inside a package with fmt.Errorf and printed by whoever caught them: too late to translate where they are shown, too early where they are made. Every one now carries its key across that gap. ZERO Hungarian error literals remain. util.MsgError does three things at once, each earned: - Error() is the Hungarian, byte for byte, so every un-converted printer is unchanged; - errors.Is answers for the kind AND for a wrapped cause (KindErrorf dropped the cause); - an error ARGUMENT renders recursively, so "formázás sikertelen: %w" translates whole. A foreign error — restic, docker, ssh, the stdlib — prints verbatim. It is not ours. 76 display sites go through errText, and TestNoErrErrorInPageOutput convicts any that do not. memoryVerdict returns an error rather than a sentence, so the deploy's 409 and the household's language come from one value; UpdateRefusal gained a Cause to carry it. Plurals, one rule, stated once: a key with .one/.other takes its COUNT first. Not a per-call-site flag — the producer somebody forgot would read "3 app is not running". The guard caught a real key collision (alert.deadapp.one) the day the rule landed. TWO DEFECTS FOUND IN MY OWN TOOLING, recorded rather than quietly fixed. The bulk converter silently dropped multi-line concatenations, damaging 7 producers — and the parity gate could not see it, because every surviving fragment WAS a real base literal while the CALL had lost text; two behaviour tests caught it. And the counting script was case-sensitive, so it said "0 left" while five remained. 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
14 KiB
REPORT — localisation slice 2, releases A and B (controller v0.252.0 + v0.253.0, R-557)
2026-09-18 · base commit 736f54b49610 (v0.251.0) · MinAgent 0.131.0, unchanged · no hub release.
Two releases this session: A (the sentences shown now) and B (the 179 error messages).
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, theunitRestore*MsgFmtandtier2*Msgfamily inhandlers.go, andbackup/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/fmtDurationinfuncmap.goare copy-producing helpers thatlocaleFuncsdoes 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 reachesAlert.Messageas 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/.
Release B — v0.253.0: errors carry a key
179 Hungarian error literals converted; ZERO remain. util.MsgError carries the key from the
package that makes the error to the handler that prints it.
1. The mechanism, and the three things it had to do at once
| property | why it is not optional | pinned by |
|---|---|---|
Error() is the Hungarian, byte for byte |
every un-converted printer (a log line, a %v, a third-party wrapper) keeps printing what it printed. Without it, 179 producers could not be converted before all their printers were. |
TestMsgErrorKeepsKindAndHuText |
errors.Is answers for the kind and for a wrapped cause |
KindErrorf returned the kind alone and dropped the cause; a converted producer usually wraps one |
TestMsgErrorUnwrapsTheCauseToo |
| an error ARGUMENT renders recursively | „formázás sikertelen: %w" is a sentence wrapping a sentence; both halves are ours |
TestErrTextRendersAWrappedMessageErrorToo |
| a FOREIGN error prints verbatim | restic, docker, ssh and the stdlib are not ours to translate (R-553's rule) | TestErrTextFallsBackVerbatim |
76 display sites in internal/web and internal/api now go through errText;
TestNoErrErrorInPageOutput examines 316 display-sink lines and convicts any that do not. A
strings.Contains(err.Error(), …) is a COMPARISON and stays untouched — the four English ones are
R-569.
memoryVerdict returns an ERROR rather than a sentence, so the deploy's 409 and the household's
language come from one value; UpdateRefusal gained a Cause so that error survives the update path
to the API.
2. Plurals — one rule, stated once
A key that carries .one/.other forms in a language is a plural key, and its FIRST parameter is
the count (i18n.Bundle.form). Hungarian never carries them, so a Hungarian render is unchanged at
every count. Deliberately NOT a per-call-site flag: the producer somebody forgot would read „3 app is
not running" with nothing to catch it, and the bundle is where a translator works.
TestPluralFirstArgIsNumeric pins that every plural value really takes its count first.
TestNoOrdinaryKeyEndsInAPluralSuffix pins that the two suffixes stay reserved — it caught a real
collision the day the rule landed (alert.deadapp.one, the ONE dead app, would have been read as
the singular of a key that does not exist). Renamed to alert.deadapp.single.
3. Two defects release B found in its OWN tooling
Both are recorded here rather than quietly fixed, because each is a shape that will recur.
(a) The bulk converter silently dropped a multi-line concatenation.
fmt.Errorf("a: "+ "b: %s", x) kept only "a: " and lost the rest with its arguments — 7 producers
damaged. The parity gate did NOT catch it, and the reason matters: every surviving fragment WAS a
real base-commit literal, so the gate's question ("is this text real?") was answered yes while the
CALL had lost text. What caught it was two behaviour tests that assert the sentence a customer reads
(TestR356_ScenarioC_UndeployedAppIsStillRefused, TestR379_ScenarioA_RollbackSucceeds_AppComesBack).
All 7 were rebuilt as joined keys and the six wrong keys pruned from both bundles. The lesson: a
structural gate over the TEXT cannot see a defect in the CALL; only a test that renders the sentence
can.
(b) My own counting script was case-sensitive, so it reported "0 error literals left" while five
remained („occ parancs sikertelen", „hub hiba", „OnlyOffice aldomain nem ismert" ×2). That is the
R-565 shape inside the measuring instrument. Re-measured with re.I; the five are converted. The
ASCII-fragment search that found them is in §5 below, with its controls.
4. Red-proofs
Seven planted breaks, each seen to convict and then reverted (full log:
audits/i18n-slice2-2026-09-18/redproofs.txt, entries 8–14). Plus the two live catches above, which
are not red-proofs — nobody planted them.
5. The "no Hungarian error left" claim, with controls
- Negative control:
fmt.Errorf("…qzxvkjq→ 0 hits (the fragment is in no file). - Instrument works: the same ASCII search run against the release-B BASE commit finds
sikertelen12× inmigrate.goandnincs7× inoffbox_restore.go. - The claim, now: an ASCII search for
sikertelen|sikeres|telepitve|mentes|nincs|kotelezo|hiba|folyamatbaninsidefmt.Errorf/errors.New, case-insensitively, and a separate accented-letter search: 0 hits each, outside a comment ininternal/util/msgerr.gothat quotes an example.
6. What is left after B
705 Hungarian literals, none of them errors: the country TABLE (113, not on the wire and not
translated on purpose), handler_debug.go (R-574), the notifier's 27 wire messages (R-558),
funcmap.go's two un-overridden helpers (R-572), the text a background run PERSISTS (release C), and
the R-570 producer. Release C's default stands: written in the box's language at write time.