REPORT: localisation slice 1 (R-556) — releases A, B, C, per-page table, red-proofs, live evidence
gates / gates (push) Successful in 23s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-17 18:50:59 +02:00
parent be2efe5624
commit e236dca486
+88 -87
View File
@@ -1,109 +1,110 @@
# REPORT — controller v0.247.0: the i18n spike — English on three pages, Hungarian byte-identical
# REPORT — localisation slice 1 (R-556): the whole dashboard in English, 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`.
**Date:** 2026-09-17 · **Releases:** v0.248.0 (`ff68b0b`, apps and settings), v0.249.0 (`11b790e`, backups),
**v0.250.0** (`ac149c2` + `be2efe5`, storage, sharing, sign-in, guest, catch-all, debug, the switch for
everyone) · **MinAgent:** 0.131.0 (unchanged) · **Baseline of the slice:** v0.247.0.
**Architecture read before any claim:** `felhom.eu/documentation/architecture/10-localisation.md`
(updated: §2.2, §2.3, §3 decision 6 SUPERSEDED, §5, §10, §11), `02-controller-module-map.md`.
**Evidence:** `felhom.eu/documentation/audits/i18n-slice1-2026-09-17/{A,B,C}/`.
## Claims in the brief that turned out wrong — named first
## Claims in the prompt that turned out wrong — named first
The brief named four already. Further ones (details and methods in the inventory §0):
1. **Per-page Hungarian line counts** did not reproduce (debug 92 accented lines, not 103; monitoring 48,
not 72; others differ the same way).
2. **Marker counts of the v0.247.0 pages** — measured layout 105, backups 59, app_info 58, launcher 28;
the prompt said 95 / 52 / 48 / 25.
3. **"Templates decide nothing by Hungarian wording"** is false: `backups_remote.html` runs
`indexOf('Fut')` on its own status text. The prompt's sweep checked only `==`, `===` and `case`.
The word stays Hungarian (R-563).
4. **The list of unconverted copy omitted `backups_shared` `restore_banner_js`** — it was still
Hungarian after v0.247.0; converted in B.
5. **"Six direct-render pages"** — confirmed, not wrong: login, claim, recovery, launcher_shared,
launcher_share_password, catchall. No seventh `s.tmpl` render exists now.
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).
## Per-page table — all 31 templates
## What shipped
Markers = `{{T}}` occurrences today. States = parity fixture cases rendering the page (captured from the
unconverted template first).
- **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.
| rel | page | markers | states | | rel | page | markers | states |
|---|---|---|---|---|---|---|---|---|
| A | dashboard | 48 | 4 | | B | backups_restore_wizard | 46 | 5 |
| A | stacks | 37 | 1 | | B | backups_escrow | 58 | 4 |
| A | logs | 9 | 1 | | B | tier2_config | 23 | 4 |
| A | monitoring | 61 | 3 | | B | recovery | 43 | 7 |
| A | app_import | 40 | 2 | | C | storage | 162 | 2 |
| A | app_export | 30 | 1 | | C | storage_network | 70 | 2 |
| A | deploy | 171 | 5 | | C | storage_init | 42 | 1 |
| A | settings_system | 94 | 4 | | C | storage_attach | 27 | 1 |
| A | settings_security | 58 | 2 | | C | sharing | 74 | 6 |
| A | settings_notifications | 34 | 2 | | C | login | 9 | 1 |
| A | app_row | 0 (no copy) | via stacks/dashboard | | C | claim | 21 | 4 |
| A | meta_badge | 0 (no copy) | — | | C | launcher_shared | 4 | 2 |
| A | icons | 0 (no copy) | — | | C | launcher_share_password | 6 | 1 |
| B | backups_apps | 97 | 6 | | C | catchall | 4 | 2 |
| B | backups_remote | 81 | 12 | | C | debug | 189 | 1 |
| B | backups_restore | 78 | 4 | | | **total, all 36 templates** | **1 874** | **106** |
## Red-proofs — each seen failing, then passing (`felhom.eu/documentation/audits/i18n-2026-09-17/redproofs/`)
Bundles: hu 1 587 keys, en 1 590 (plural forms), English gap 0.
## What release C shipped
- 11 templates converted; six ASCII-only Hungarian JS fragments the extractor missed, found by eye.
- `login`, `claim`, both guest share pages and the catch-all through `executeTemplateLang` (household
language, no session CSRF, no escrow reminder).
- Title keys for storage, network storage, both wizards, sharing; the pin test for all 17 title keys.
- The switch on every dashboard page: the hide condition deleted; 89 layout fixtures re-captured, each
proven to equal its predecessor plus exactly one switch form after the version span (89/89).
- A correction: the release C cases had invented page titles; they now carry the handlers' titles and
the 12 affected fixtures were re-captured from `0555091` (unconverted). Diff: `<title>` and the wizard nav.
- `HU_FORMAL_CEILING` 12 → 16 (four more formal forms in unchanged Hungarian).
## Red-proofs (release C; A and B in their folders) — each seen failing, then passing
| 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` 89dd3e94b1de (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`).
| `TestI18nParity` | hu `sharing.megosztott_mappak` loses one accent | 6 sharing cases `Hungarian page CHANGED` |
| `TestI18nEnglishPages` | en `storage.eleresi_ut` deleted | `Hungarian template copy on the English page … Elérési út` (2 cases) |
| `TestI18nParityCoversEveryMarker` | a marker in a branch no case renders | `marker never rendered by any parity case: storage_attach.hiba` |
| `TestHandlerTitleKeysMatchHungarianTitle` | hu title reworded; the sharing TitleKey line deleted | `handler title "Hálózati megosztás", but hu.json … = "Hálózati megosztások"`; `page.title.sharing has no handler` |
| `TestDirectRenderHandlersFollowLanguage` | all five call sites back on `s.tmpl.ExecuteTemplate` | five rows: `with en saved: … <html lang="en" false` |
| `TestI18nDirectRenderPagesHaveNoAdminChrome` | `addEscrowBanner` added to `executeTemplateLang` | `put EscrowBanner / CSRFField into the page data` for every page. The page layer alone stays green under this mutation (the guest template has no slot for the bar) — stated in the test |
| `TestLanguageSwitch_EndToEnd` | the v0.247.0 hide condition restored | `a Hungarian household with no ?lang= must see the language switch`; 89 parity cases fail |
## 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.
- `go build ./... && go vet ./... && go test ./...` → rc 0 before each commit.
- `controller_gates.py --fast` → rc 0 (i18n: English missing 0/0, formal 16/16; retrieval-promise OK);
`test_gate_decoys.py` 20/20. Pre-push hook ran on both pushes; no `--no-verify`.
## 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/`.
Deployed by hand; **fleet floor NOT raised, no golden.** Startup: `lang=hu … 1874 markers expanded in
27.35ms`, `lang=en … 28.37ms` (B: 26.5 / 21.3 ms; A: ~24 / ~20 ms).
- **Hungarian 0.249.0 → 0.250.0:** each dashboard page carries one switch form after the version span;
removed, 7 of 8 pages are identical; `/dashboard` differs in live numbers and the order of two
drive rows. `/login`, `/claim`, catch-all: identical, no switch.
- **English:** `POST /settings/language lang=en` → 302; all 11 pages `<html lang="en">`; no accented
Hungarian on storage, sharing, wizards, debug, sign-in. Left: catalog taglines, Go-built text.
- **Back to Hungarian:** identical to the after fetch apart from live numbers.
- **Hub report:** `hu` 16:45:10, `en` 16:46:06, `hu` 16:46:43 (all 0.250.0).
- Guest share pages: not exercised live (would create a share link on the household's box); proven
through the real route in unit tests.
## 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.
Machine: box on `hu`, 0.250.0; password, session and header files shredded; fetched pages pulled off
and removed; the three fetch/switch scripts (no secrets) stay in `/root`. Host: transfer files removed.
Hub: nothing written; DB copy shredded. Nothing provisioned.
## 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`.**
1. A page decides its progress poll by reading its own Hungarian word „Fut". **FILED: R-563**
2. The retrieval gate's Hungarian stems miss split verbs, exposed by the English translation. **FILED: R-564**
3. Six ASCII-only Hungarian fragments reached the English pages with every test green; the English page test sees accented letters only. **FILED: R-565**
4. Three page titles built in Go around an app name stay Hungarian in the English tab. **FILED: R-566**
5. The two drive wizard pages do not highlight the storage menu group. **FILED: R-567**
6. The dashboard's drive-health rows swapped order between two fetches a minute apart. **FILED: R-568**
7. The formal-form stem list counts 4 of at least 22 formal forms on the release C pages. **FILED: R-516**
8. `r400_debug_routes_test.go` and `debug_route_gate.py` read `debug.html` raw after conversion. **NOT-A-FINDING: they match addresses and identifiers, which never move into a bundle (0 `/api/` strings in either bundle, measured); both stay green and correct.**
9. traefik answers an unrouted host itself, so the catch-all page was fetched from the container address. **NOT-A-FINDING: routing of unknown subdomains is traefik configuration by design; the controller page renders when reached, and the method is stated in the evidence.**