# REPORT — v0.257.0: the app catalog can speak English (R-560, slice 5 Part A) 2026-09-20 · `main` `60f0a86` · image `gitea.dooplex.hu/admin/felhom-controller:0.257.0` · live on demo-hp guest 9201. MinAgent 0.131.0, unchanged. ## What this release does An app's customer-facing text lives in the catalog's `.felhom.yml` and was Hungarian only. This release lets the same file carry an English twin in an `i18n: {en: …}` sibling block, and makes the dashboard read it when the household is on English. Architecture document read first and named here: `felhom.eu/documentation/architecture/10-localisation.md` — §1 (the Hungarian must not move), §4 (fallback), §5 (the gates), §7 (the catalog's copy model, which this release turns from a proposal into a measured fact), §11 ruling 7 (app names are not translated). ## The five properties, and why each is load-bearing 1. **`For("hu")` is the parsed struct with `I18n` cleared, and nothing else.** Deep-compared against all 53 real catalog files, copied into `internal/stacks/testdata/catalog/`. `LocalizeStacks` returns the INPUT SLICE for Hungarian rather than a copy, so nothing on the Hungarian path is even touched. §1 holds by construction, then measured live. 2. **Fallback is field by field, and blank counts as absent.** A half-translated app is a legal, shippable state — that is what makes fifty apps pushable a batch at a time. 3. **The three prose lists replace WHOLE; every other list is matched by its own key** — `env_var`, option `value`, `match_group`, `target`, `path`. Position matching mistranslates silently the first time a Hungarian field is inserted above another, and the page still looks right. 4. **`For` never writes through.** The metadata is the stack manager's and is shared by concurrent requests; an in-place merge would put one household's language on another's page. 5. **An older controller ignores the block.** `LoadMetadata` uses non-strict `yaml.Unmarshal` — this repo constructs no `yaml.Decoder` at all, so there is no `KnownFields` anywhere to turn strict. Proven live below. ## Where a page may read catalog copy **Four templates** carry catalog copy — `dashboard.html` (the app rows' second line), `stacks.html` (the Apps list), `app_info.html` (the app page, including the data-folder cards) and `deploy.html`. A fifth, `app_export.html`, reads `.Stack.Meta.Subdomain`, which is configuration. **Five handler sites** feed them, and each now goes through `LocalizeStacks` / `LocalizeStackPtr` / `meta.For(lang)`: the dashboard, the Apps list, the deploy page, the app page — and the logs page, which today shows only the app's NAME and is routed through the same call anyway, because the next person to add a line to `logs.html` should not have to notice that this one page was the exception. Two producers sit in packages that have no request and therefore no language — the integration rows (`internal/integrations`) and the initial-credentials note (`internal/stacks/initialcreds.go`); both are re-taken from the localised metadata in the handler, identical bytes for Hungarian. `TestNoDirectMetaCopyReadOnPages` parses every non-test file in `internal/web` and fails on any `x.Meta.` read that is not in a named, reasoned allow-list (three entries today, all `appDetailHandler`'s localised `found`). **What it cannot do, stated in the test:** it reads source, so it cannot tell a localised receiver from an unlocalised one. It catches the ARRIVAL of a new copy read, which is when a human has to think — that is the whole claim made for it. ## Tests, and the red-proofs | test | red-proof (each seen to fail, then reverted) | |---|---| | `TestMetaForHuIsIdentity` (53 real files) | a copy field mutated inside `For`'s hu branch — FAILED naming 53 apps | | `TestDeployFieldsMatchedByKey` | deploy fields matched by position — FAILED, "Second field" landed on FIRST | | `TestEmptyEnglishFallsBack` | a blank English string allowed to win — FAILED | | `TestForDoesNotMutateTheReceiver` | the defensive copy removed — FAILED, showing the written-through strings | | `TestOverlayMatches…` (optional_config) | groups matched by position — FAILED | | `TestOverlayMatches…` (integrations) | integrations matched by position — FAILED | | `TestOverlayMatches…` (data paths) | data paths matched by position — FAILED | | `TestLocalizeStacksLeavesHungarianSliceAlone` | `I18n` left on the value handed to the page — FAILED | | `TestNoDirectMetaCopyReadOnPages` | `_ = st.Meta.Description` added to `logsHandler` — FAILED naming file and field | **Two of the nine convicted a hollow TEST rather than the code, and that is the finding worth keeping.** - `TestForDoesNotMutateTheReceiver` first did the obvious thing: copy the struct, call `For`, `DeepEqual` the copy against the original. **A struct copy shares its slices' backing arrays**, so a write through `out.DeployFields[i].Label` changes BOTH sides and the compare passes. It passed a deliberately broken merge. It now snapshots the concrete strings before the call. - The optional-config / integrations / data-path fixture first carried ONE entry per list. With one entry, position and key agree, and a position-matching implementation passes. It passed. The fixture now carries two entries per list with only the second translated. Full suite, vet and build green; `controller_gates.py` green (17 gates). ## Live proof — endpoint level, and that is stated because it matters No browser exists on DooPlex. Every page was fetched with `curl` from inside guest 9201 at the controller's container address, with the customer `Host` header and a real logged-in session: the same handlers and the same templates a browser drives, with only the drawing skipped. - **demo-hp, 0.257.0.** With the catalog pilot synced, `/apps/privatebin`, `/apps/paperless-ngx`, `/apps/romm`, `/stacks` and two deploy pages render the English catalog text on `?lang=en`. - **The Hungarian did not move.** The same seven pages in Hungarian, before and after the catalog push, are byte-identical apart from the per-session CSRF token: equal raw byte counts (43 253 / 41 532 / 41 547 / 147 121 / 74 326 / 70 290) and equal hashes once the token is normalised. - **demo-felhom, 0.255.0 — the old-controller check.** With the block synced onto that box (confirmed positively: the sync named the three apps, and the block is in both the cache and the stack copy), its pages hash identically before and after, and its log carries **no** parse warning — in a 93-line window that contains the sync's own lines, so the absence is evidence and not a dead log. Evidence: `felhom.eu/documentation/audits/i18n-slice5-2026-09-20/`. ## What is still Hungarian on an English app page Measured with an accent scan AND an ASCII-folded stem scan, both with a positive and a negative control. Three things, and the first is correct: 1. **the language picker's own „Magyar" button** — a picker names each language in its own tongue; 2. **the update badge „Naprakész" and its tooltip** — **R-589**. Slice 1 listed it (§2.3); slice 2 was the slice that was to take it, and closed without it; 3. **the data-folder card's consequence sentence** — **R-590**. This one makes a promise about the customer's files, and the label above it is already English, so the line reads half and half. The Apps LIST also shows fifty Hungarian descriptions — those are the apps nobody has translated yet. ## Gaps filed **R-589** (the update badge) · **R-590** (the data-folder promise) · **R-591** (`Stack.Copy()` deep-copies five `Meta` fields and not the new `I18n` map — safe today because nothing mutates an overlay, which is exactly why it is worth a row) · **R-592** (three defects inside the new catalog gate, each found by its own decoy). ## Needs the operator **Raise the fleet floor to 0.257.0.** Until then only demo-hp can read an English app block at all; every other box keeps showing Hungarian app text to an English household. Nothing breaks if it waits. ## Teardown Nothing installed or removed on any box; no app deployed. Guest 9201 on demo-hp was restarted onto the new image, which is the deploy. The staged password file and the capture scripts were shredded from both Proxmox hosts and both guests. Both boxes left on Hungarian.