Files
felhom-controller/REPORT.md
T
2026-09-17 14:58:32 +02:00

8.6 KiB
Raw Blame History

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):

  1. „fmtTime, fmtBytes, timeAgo do not exist; fmtMB is the one size helper" — all three exist (internal/web/funcmap.go L265/L318/L403), plus nine more copy- or locale-producing helpers and four size helpers. timeAgo builds „%d perce" — the plural problem lives there.
  2. „fmtMB used 10× in templates" — 8×.
  3. „~13 .Format( layout calls in internal/web" — 10 at the baseline (plus 8 time.RFC3339).
  4. Template lines 1 613 / 1 838 — 1 553, by two methods; neither figure reproduced by four variants.
  5. Go literals 1 234 in 99 files — 1 141 in 84 (a lexer, comments skipped); comments explain most of the difference.
  6. 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, embedded locales/hu.json + en.json). loadTemplates parses one template set per language via parseTemplateSet (= ParseFS + Expand); s.tmpl stays Hungarian. Why not a runtime T func: the escaper changes + ' " bytes and 618 of 1 867 template strings are inside <script>. Why not x/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 via TitleKey; English overrides for stateLabel, timeAgo, timeAgoStr, nextRunLabel, statusText; <html lang> follows.
  • Setting / switch / report: settings.json language; 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 in cmd/controller/main.go.
  • Gates: new i18n_missing_gate.py (registered in controller_gates.py). emoji, native_confirm, secret_in_markup, retrieval_promise now read templates expanded (scripts/i18n_bundle.py); mojibake scans .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 in en.json, and the HEAD retrieval gate never saw a planted promise in hu.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 with ParseFS; they now parse through parseTemplateSet for 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-ParseFS tests; 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/privatebin fetched 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=en and, after POST /settings/language lang=en (302 → /launcher, settings "language": "en", INFO language 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

  1. The wire-contract gate passes an unread field whose name appears in a hub Go comment (language). FILED: R-555
  2. Four places decide behaviour by matching Hungarian wording (HTTP status, quota class, alert placement, a persisted warning marker). FILED: R-553
  3. The extractor and the inventory detect ASCII-only Hungarian only through a word list; eight strings slipped past on three pages. FILED: R-556
  4. TestI18nEnglishPages subtracts 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
  5. Six formal „ön" forms in the converted Hungarian copy, now counted by a ratchet. FILED: R-516
  6. Dates and sizes are not locale-formatted; the two template date layouts disagree; sizes use a decimal point. FILED: R-562
  7. storage_handlers_test.go imported text/template for its template-parse check. NOT-A-FINDING: a test-only parse check with no escaping assertion; it now parses through the production parseTemplateSet, which uses html/template.