README gains "Catalog copy in a second language" under §18: the format, the field-by-field fallback, whole-list replacement vs key matching, the no-write-through rule, the four templates that carry copy, and why an older controller is unaffected. REPORT records the nine red-proofs — including the two that convicted a hollow TEST rather than the code — and the live proof on both demo boxes, naming the method (endpoint level, no browser on DooPlex) rather than implying a click-through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
8.2 KiB
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
For("hu")is the parsed struct withI18ncleared, and nothing else. Deep-compared against all 53 real catalog files, copied intointernal/stacks/testdata/catalog/.LocalizeStacksreturns 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.- 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.
- The three prose lists replace WHOLE; every other list is matched by its own key —
env_var, optionvalue,match_group,target,path. Position matching mistranslates silently the first time a Hungarian field is inserted above another, and the page still looks right. Fornever 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.- An older controller ignores the block.
LoadMetadatauses non-strictyaml.Unmarshal— this repo constructs noyaml.Decoderat all, so there is noKnownFieldsanywhere 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.<copy-field> 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.
TestForDoesNotMutateTheReceiverfirst did the obvious thing: copy the struct, callFor,DeepEqualthe copy against the original. A struct copy shares its slices' backing arrays, so a write throughout.DeployFields[i].Labelchanges 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,/stacksand 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:
- the language picker's own „Magyar" button — a picker names each language in its own tongue;
- 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;
- 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.