Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
8.6 KiB
REPORT — controller v0.247.0: the i18n spike — English on three pages, Hungarian byte-identical
Date: 2026-09-17 · Task: localisation starter (spike + design), Phase 1 · Baseline: main
89dd3e94b1de (v0.246.0, MinAgent 0.131.0) · Released: 612c417 → image
gitea.dooplex.hu/admin/felhom-controller:0.247.0 · MinAgent: 0.131.0 (unchanged).
Architecture read before any claim: felhom.eu/documentation/architecture/02-controller-module-map.md
(templates, setup wizard obsolete) and 05-hub-architecture.md (report ingestion, config push).
Design written after the spike ran: felhom.eu/documentation/architecture/10-localisation.md.
Inventory: felhom.eu/documentation/audits/I18N-INVENTORY-2026-09-17.md.
Claims in the brief that turned out wrong — named first
The brief named four already. Further ones (details and methods in the inventory §0):
- „
fmtTime,fmtBytes,timeAgodo not exist;fmtMBis the one size helper" — all three exist (internal/web/funcmap.goL265/L318/L403), plus nine more copy- or locale-producing helpers and four size helpers.timeAgobuilds „%d perce" — the plural problem lives there. - „
fmtMBused 10× in templates" — 8×. - „~13
.Format(layout calls ininternal/web" — 10 at the baseline (plus 8time.RFC3339). - Template lines 1 613 / 1 838 — 1 553, by two methods; neither figure reproduced by four variants.
- Go literals 1 234 in 99 files — 1 141 in 84 (a lexer, comments skipped); comments explain most of the difference.
- Banner 33 lines — 34 once ASCII-only Hungarian words count (method difference).
What shipped
- Mechanism (b), expanded at template LOAD:
internal/i18n(Bundle,Expand,Msg,Plural, embeddedlocales/hu.json+en.json).loadTemplatesparses one template set per language viaparseTemplateSet(=ParseFS+Expand);s.tmplstays Hungarian. Why not a runtimeTfunc: the escaper changes+ ' "bytes and 618 of 1 867 template strings are inside<script>. Why notx/text/message: no gender, only count plurals — a dependency for nothing. - Converted:
launcher.html,backups.html,app_info.html,layout.html,backups_shared.html(backups_empty) — 252 markers, 235 hu keys, all translated (238 en keys incl. plural forms). Page titles viaTitleKey; English overrides forstateLabel,timeAgo,timeAgoStr,nextRunLabel,statusText;<html lang>follows. - Setting / switch / report:
settings.jsonlanguage;POST /settings/language;?lang=; switch in the sidebar footer only when not Hungarian or?lang=is present;"language"in the hub report at all four build sites incmd/controller/main.go. - Gates: new
i18n_missing_gate.py(registered incontroller_gates.py).emoji,native_confirm,secret_in_markup,retrieval_promisenow read templates expanded (scripts/i18n_bundle.py);mojibakescans.json. Found while converting: retrieval-promise went STALE (red) the moment copy moved; the other three went silently BLIND — the HEAD emoji gate passed a planted emoji inen.json, and the HEAD retrieval gate never saw a planted promise inhu.json. - Tool:
scripts/i18n_extract.py. First pass pulled layout<span>s and a button-link pair into messages and mis-split a JS string that starts inside a tag — fixed and re-run from clean templates. It also missed eight ASCII-only Hungarian strings (Megszakadt, Elavult, Csak x86, Pi kompatibilis, Rendszermonitor, Most nem, sikertelen, ismeretlen) and one JS fragment („adatait ide:"), found by a by-eye review of what it left; the word list was extended and the fragment converted by hand. - Two existing tests (
TestTemplatesParse,TestTemplatesParseWithFuncmap) parsed raw templates withParseFS; they now parse throughparseTemplateSetfor every language.
Red-proofs — each seen failing, then passing (felhom.eu/documentation/audits/i18n-2026-09-17/redproofs/)
| test | mutation | failure seen |
|---|---|---|
TestI18nParity |
fixture file absent | no parity fixture (red before the fixture existed) |
TestI18nParity |
„Megosztási link" → „…linK" in hu.json | launcher_full: Hungarian page CHANGED at line 261 (+ launcher_nopassword) |
TestI18nEnglishPages |
layout.inditopult deleted from en.json |
Hungarian template copy on the English page, line 111 … Indítópult (5 cases) |
TestLanguageSwitch_EndToEnd |
s.addLanguageData removed from executeTemplate |
?lang=en must render English and show the switch; title and switch missing |
TestI18nJSContextValuesAreSafe |
en launcher.masolva = Copied' |
contains '\'', which breaks its context |
TestBundleParametersMatchAcrossLanguages |
en banner drops {{.RecoveryAbandonDate}} |
template fields ".RecoveryAbandonDays", Hungarian has ".RecoveryAbandonDate,.RecoveryAbandonDays" |
TestLocaleFuncsHungarianBundleMatchesFuncMap |
hu func.state.running = „Futás" |
hu.json "Futás", funcmap "Fut" |
gate decoys (test_gate_decoys.py) |
undefined key, „Please", gap grew, new formal form; emoji / confirm( / secret / retrieval promise planted in a bundle | all 18 decoys rejected; HEAD emoji gate passed the bundle emoji, HEAD retrieval gate did not see the bundle promise |
Fixtures: captured by running the parity test with -update-i18n-golden in a clean git worktree of
origin/main 89dd3e94b1 (the test file adapted to s.tmpl), then copied in; the current tree passed
them before AND after conversion. All files restored after each mutation (cmp).
Gates
- Green gate:
go build ./... && go vet ./... && go test ./...→ rc 0, 29 packages ok (first run found the two raw-ParseFStests; fixed; re-run green). controller_gates.py --fast→ all OK incl.i18n(English missing 0 / ceiling 0; formal forms 6 / ceiling 6). Pre-push hook ran them on the push.
Live validation (endpoint-level; no browser on DooPlex) — demo-hp guest 9201
Deployed by hand (/etc/felhom-controller-image, bootstrap restart); fleet floor NOT raised; no
golden. Startup: loadTemplates: lang=hu … 252 markers expanded and lang=en … 252.
- Hungarian unchanged: launcher,
/backups,/apps/privatebinfetched on 0.246.0 and on 0.247.0 — identical after replacing version string and CSRF token; raw sizes equal (44 462 / 46 250 / 42 081). A fetch 7 s after restart differed only in app state tags („Nem egészséges" while health warmed up). - English:
?lang=enand, afterPOST /settings/language lang=en(302 →/launcher, settings"language": "en", INFOlanguage switch: household language set to en), identical renders. No accented Hungarian from template copy. Remaining Hungarian, all Go view models or catalog: backup-target banner/offer and whole-guest tier labels (/backups); the „Naprakész" update badge and the catalog tagline / use cases / first steps (/apps/privatebin). Launcher: none. - Hub report: stored reports read no field (0.246.0),
hu,en(12:54:43Z),hu(12:55:22Z). Negative control: demo-felhom (0.246.0) no field. - Refusals: no
_csrf→ 403; no session → 302 to login; setting unchanged. - Evidence (CSRF redacted):
felhom.eu/documentation/audits/i18n-2026-09-17/live/.
Teardown
Machine: guest temp scripts, password file, session file shredded/removed; box left on hu, 0.247.0.
Host: the tarball removed from demo-hp. Hub: nothing written; the DB copy read for reports shredded on
DooPlex. Nothing provisioned. The scratch worktree of origin/main is removed.
Observations
- The wire-contract gate passes an unread field whose name appears in a hub Go comment (
language). FILED: R-555 - Four places decide behaviour by matching Hungarian wording (HTTP status, quota class, alert placement, a persisted warning marker). FILED: R-553
- The extractor and the inventory detect ASCII-only Hungarian only through a word list; eight strings slipped past on three pages. FILED: R-556
TestI18nEnglishPagessubtracts fixture data strings, so a template word equal to a data string (the launcher title „Indítópult") is masked on the launcher cases. FILED: R-556- Six formal „ön" forms in the converted Hungarian copy, now counted by a ratchet. FILED: R-516
- Dates and sizes are not locale-formatted; the two template date layouts disagree; sizes use a decimal point. FILED: R-562
storage_handlers_test.goimportedtext/templatefor its template-parse check. NOT-A-FINDING: a test-only parse check with no escaping assertion; it now parses through the productionparseTemplateSet, which useshtml/template.