e02bc03819
gates / gates (push) Successful in 24s
The setup code and the owner passphrase now follow the household's language,
one word longer in English so the entropy never drops (setup 3 hu / 4 en,
passphrase 5 hu / 6 en). List and count are chosen together so a caller cannot
pair an English list with a Hungarian count. Hungarian is byte-unchanged.
Three claims in the row were wrong and are recorded as such:
- the RECOVERY CODE is minted by felhom-agent from the EFF list and has
always been English; the hub does not own it and no row was added.
- no claim mail states a word count; the only count wording was the bind
page's passphrase hint, whose English half is now count-free.
- the proposed phone-safe filter removes 68% of the list (5270 of 7772
words) and was measured, then declined, with the reason in source.
Also: guide_quote_gate binds the English volunteer guide's three quoted
messages to the controller's English bundle — nothing did, so the guide would
have gone on quoting Hungarian after the fix. Seven decoys, all convicting,
including the name-for-fact one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
826 lines
58 KiB
Markdown
826 lines
58 KiB
Markdown
# 10 — The product in more than one language
|
||
|
||
> **LIVING DOCUMENT. Every localisation slice updates this file in the same session.**
|
||
> Opened 2026-09-17 by the localisation starter, AFTER its spike ran (controller v0.247.0). Before it,
|
||
> no architecture document mentioned localisation at all (a grep of `architecture/*.md` for
|
||
> `i18n|locali|language` found one incidental line in 07).
|
||
>
|
||
> Marks, as in 07 and 02: **[DESIGN]** — a decision taken, not derived from code. **[FACT]** — observed,
|
||
> with a citation. Unmarked means not yet classified.
|
||
|
||
**This file carries the REASONING. The register (`backlog/OPEN-ITEMS.md`, R-553..R-562) carries the
|
||
work. The numbers are in `audits/I18N-INVENTORY-2026-09-17.md`. The source is the truth.**
|
||
|
||
---
|
||
|
||
## 1. The rule that makes this safe
|
||
|
||
**[DESIGN] The Hungarian product must render byte-for-byte the same before and after every slice —
|
||
measured page by page, never by reading.** A household that never switches must not be able to tell a
|
||
localisation release happened. English goes on top of an unchanged Hungarian product.
|
||
|
||
**[FACT]** It is enforceable, and enforced for the three converted pages: `TestI18nParity`
|
||
(`felhom-controller/controller/internal/web/i18n_parity_test.go`) renders 14 fixture states of the
|
||
launcher, `/backups` and `/apps/<slug>` in Hungarian and compares them with HTML captured from a clean
|
||
worktree of `origin/main` 89dd3e94b1de **before** any template carried a marker. One changed byte in
|
||
`hu.json` fails it naming the page and the line (red-proof,
|
||
`felhom-controller/REPORT.md`). **Fixtures are captured from unconverted templates and never
|
||
regenerated to make a conversion pass** — that is the release gate for every slice.
|
||
|
||
The one normalisation: relative ages („3 napja") become „# napja", because they come from the wall
|
||
clock.
|
||
|
||
**[FACT] And live, on demo-hp guest 9201 (2026-09-17, endpoint-level):** the launcher, `/backups` and
|
||
`/apps/privatebin` fetched in Hungarian on 0.246.0 and again on 0.247.0 are **identical apart from the
|
||
version string and the CSRF token**, with equal raw byte counts (44 462 / 46 250 / 42 081), and again
|
||
after a round trip through English (`audits/i18n-2026-09-17/live/`).
|
||
|
||
---
|
||
|
||
## 2. The mechanism, as measured
|
||
|
||
### 2.1 What was chosen, and why
|
||
|
||
**[DESIGN] A flat message bundle per language, expanded into the template BEFORE `html/template`
|
||
parses it.** Decided by CC in the spike (the starter delegated the choice, §4 1.1 of the task).
|
||
|
||
| option | cost | verdict |
|
||
|---|---|---|
|
||
| (a) `golang.org/x/text/message` catalogs + a `T` func | a new external dependency; its value is gender/plural/ordinal selection | **not needed**: the inventory found no gender agreement and only count plurals; Hungarian does not inflect after a numeral |
|
||
| (b1) flat bundle, **runtime** `T` template func | every string passes through the contextual escaper: `+`, `'`, `"` change bytes; inside `<script>` a string becomes a quoted JS literal — 618 of the 1 867 template strings are in `<script>` | **rejected**: breaks byte parity by construction |
|
||
| **(b2) flat bundle, expanded at template LOAD** | one parsed template set per language (startup parses every template once per language — not measured); a translation is raw source text, so its context safety must be tested | **chosen**: the Hungarian set is parsed from the same bytes, in the same escaping contexts — parity holds by construction and is then measured |
|
||
|
||
### 2.2 How it works
|
||
|
||
- **[FACT]** Bundles: `controller/internal/i18n/locales/hu.json` (authoritative — every key) and
|
||
`en.json`, embedded (`internal/i18n/i18n.go`). Flat `key → text`. A value may carry **template
|
||
actions** (`{{.RecoveryAbandonDate}}`) and **inline markup** (`<strong>`, `<a>`): those are the
|
||
message's parameters, kept inside one message so a translator can move them. English plurals are
|
||
`key.one` / `key.other` (`Bundle.Plural`), and a template value may also branch
|
||
(`{{if eq $n 1}}file{{else}}files{{end}}`).
|
||
- **[FACT]** A converted template carries `{{T "key"}}`. `Server.loadTemplates` parses one set per
|
||
supported language through `parseTemplateSet` = `ParseFS` + `i18n.Expand` per file, naming templates
|
||
exactly as `ParseFS` does. `s.tmpl` stays the Hungarian set, so every renderer that predates i18n is
|
||
unchanged.
|
||
- **[FACT]** An **undefined** key is left in place, so the set fails to load („function T not
|
||
defined") — loud at startup and in every render test (`TestExpandLeavesUndefinedMarker`).
|
||
- **[FACT]** `executeTemplate` renders dashboard pages in the request's language. Since slice 1
|
||
(v0.249.0 recovery, v0.250.0 the rest) the pages outside the dashboard chrome — login, claim,
|
||
recovery, both guest share pages, the catch-all — render through `executeTemplateLang`: the language
|
||
set and `Lang`, and deliberately NOT the session CSRF fields or the escrow reminder (R-543) that
|
||
`executeTemplate` adds. Pinned by `TestDirectRenderHandlersFollowLanguage` (the real routes) and
|
||
`TestI18nDirectRenderPagesHaveNoAdminChrome` (reminder genuinely due, absent from the page data of all
|
||
six and from the guest page in both languages). No renderer uses `s.tmpl` directly any more.
|
||
- **[FACT]** Go-side copy on a converted page: the handler names its title key (`data["TitleKey"]`;
|
||
`TestHandlerTitleKeysMatchHungarianTitle` pins each handler's Hungarian literal equal to its hu.json
|
||
value); the non-Hungarian sets override the copy-producing funcs `stateLabel`, `timeAgo`, `timeAgoStr`,
|
||
`nextRunLabel`, `statusText`, `infraMeta` from the bundle (`web/i18n_web.go` `localeFuncs`). The Hungarian funcs
|
||
are not touched; `TestLocaleFuncsHungarianBundleMatchesFuncMap` pins that `hu.json` carries the same
|
||
words they return.
|
||
|
||
### 2.3 What a translation must not do (pinned by tests)
|
||
|
||
- **Carry different parameters.** Same `{{.Field}}` set and same printf verbs as the Hungarian
|
||
(`TestBundleParametersMatchAcrossLanguages`).
|
||
- **Break its context.** Expansion is textual; inside a JS string a bare `'`/`"`/`\` or newline ends
|
||
it, inside a double-quoted attribute a `"` ends it — and `html/template` cannot see this, because it
|
||
reads the expansion as the author's own source. A value may carry a quote only where the Hungarian
|
||
carries the same one (`TestI18nJSContextValuesAreSafe`, red-proofed with `Copied'`). English
|
||
therefore avoids apostrophes in JS contexts („do not", not „don't"); elsewhere it uses ’.
|
||
- **Render blank or raw.** No raw key, no marker, no element empty in English that is not empty in
|
||
Hungarian (`TestI18nEnglishPages`).
|
||
|
||
**[FACT] What stays Hungarian on the English pages, live (v0.250.0, all 31 slice-1 templates converted):**
|
||
Go view-model text (flash lines, errors, the backup-target banner and offer, the whole-guest tier
|
||
labels, the update badge „Naprakész", the catch-all's status sentence), three page titles built in Go
|
||
around an app name (logs, deploy, tier-2 settings — R-566), and catalog copy (tagline, use cases, first
|
||
steps) — slices 2 and 5. Deliberately left in a template: „Fut…" on `backups_remote`, which the page's
|
||
own JS reads (R-563). Evidence: `audits/i18n-slice1-2026-09-17/{A,B,C}/live/`.
|
||
|
||
**[FACT] Measured limits of the English page test.** (1) It subtracts fixture DATA strings before
|
||
looking for Hungarian; since slice 1 release A only data strings of ≥ 2 words or ≥ 12 characters are
|
||
subtracted, so a one-word template title is no longer masked. (2) **It sees accented Hungarian only.**
|
||
An ASCII-only Hungarian word left in a template („mp", „ db", „jelenlegi:", „FIGYELEM:", „, majd a(z)")
|
||
passes it on the English page; release C found six such fragments by eye, none by a test (R-565).
|
||
|
||
---
|
||
|
||
## 3. Who picks the language, and how it flows
|
||
|
||
**[DESIGN] Operator decision 2 (2026-09-17, agreed): the operator sets it per customer on the hub at
|
||
creation (default Hungarian); the household can switch it on their dashboard; the box reports it so
|
||
the hub's e-mails follow.**
|
||
|
||
- **[FACT] Built in v0.247.0:** `settings.json` `language` (`hu`|`en`; empty reads `hu`,
|
||
`Settings.GetLanguage`); `POST /settings/language` (session CSRF like every form; `lang`, `back`;
|
||
redirect drops the query); `?lang=hu|en` per-request override, never persisted; the hub report's
|
||
`"language"` field, always present, set at all four report build sites in `cmd/controller/main.go`.
|
||
- **[FACT] Live 2026-09-17 on demo-hp:** `POST /settings/language lang=en` → 302, `settings.json`
|
||
`"language": "en"`, the three pages English; the hub's stored reports read no field (0.246.0), `hu`,
|
||
`en` at 12:54:43Z, `hu` at 12:55:22Z after switching back; without `_csrf` → 403 and nothing changed
|
||
(`audits/i18n-2026-09-17/live/README.md`).
|
||
- **[FACT] Slice 3 Part A, hub v0.118.0 (2026-09-18):** the hub READS the reported language and writes
|
||
the household's e-mails in it. `hub/internal/i18n` (79 keys, hu authoritative, hu fallback, missing
|
||
ceiling 0); 56 mail goldens captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian
|
||
ones pass unchanged. `customerMessages`/`severityLabels` are derived from the bundle. Order: last
|
||
reported → `customer_configs.language` → `hu` (`Store.CustomerLanguage`). `message_customer` is
|
||
accepted on `POST /api/v1/event` for the box's own sentences. The bind page is per-language, with
|
||
`expired` pinned to Hungarian so the language cannot become the oracle the text refuses to be.
|
||
Full design: `05-hub-architecture.md` §15. **R-555 closed** — the `language` allowlist entry is out
|
||
of `wire_contract_gate.py` and the gate now checks the field for real.
|
||
- **[DESIGN] Slice 3 Part A as planned — now built; the box half (Part B) is the remaining piece:** the hub stores a per-customer language, renders it into
|
||
`controller.yaml` next to `customer.id/name/domain/email` (`hub/internal/configgen/configgen.go`),
|
||
and the box uses it **only while the household has never chosen** (`settings.json` empty). The
|
||
household's own choice always wins; the hub's e-mails follow the language the box **reports**, which
|
||
is therefore the household's choice. The hub decodes the report with independent `json.Unmarshal`
|
||
calls and no `DisallowUnknownFields` (`hub/internal/api/handler.go` `handleReport`), so the field
|
||
was additive.
|
||
|
||
**[DESIGN] Decided by CC in the spike — operator may reverse: the switch is SHOWN only on a page that
|
||
is not Hungarian, or on a request carrying `?lang=`.** One answerable sentence: *should a Hungarian
|
||
household see an English switch while only three pages are English?* Options: (1) show it to everyone
|
||
now — cost: one click from a half-English dashboard, and every page but three keeps a Hungarian body;
|
||
(2) hide it until slice 1 converts the remaining pages — cost: a household cannot discover English
|
||
yet, which no household has asked for. **Chosen (2)**: it follows rule §1 (nothing visible changes)
|
||
and is reversed by deleting one condition in `addLanguageData`. The way back from English is always on
|
||
screen.
|
||
|
||
**[FACT] SUPERSEDED 2026-09-17 by slice 1 release C (controller v0.250.0):** every template is converted,
|
||
so the condition is deleted and the switch is on every dashboard page, in both languages. The 89
|
||
layout parity fixtures were re-captured and each equals its predecessor plus exactly one switch form
|
||
(`audits/i18n-slice1-2026-09-17/C/switch-fixture-diff.txt`); `TestLanguageSwitch_EndToEnd` pins that a
|
||
Hungarian household with no `?lang=` sees it. Live on demo-hp: every Hungarian dashboard page equals
|
||
its 0.249.0 fetch once the switch form is removed (live numbers aside); login, claim and the catch-all
|
||
are unchanged. **A Hungarian household sees it only when the fleet floor reaches 0.250.0** — the
|
||
operator's cadence (R-242/R-468); no floor was raised.
|
||
|
||
---
|
||
|
||
## 4. Fallback
|
||
|
||
**[DESIGN] Operator decision 3 (2026-09-17, agreed): a missing English line shows the Hungarian one
|
||
and is counted by a gate; a missing line never shows a key or an empty box.**
|
||
|
||
- **[FACT]** `Bundle.Text` falls back to Hungarian and flags it; the loader logs
|
||
`i18n: en template set shows Hungarian for N markers`; `Bundle.Msg` of a key absent everywhere
|
||
returns the key (visible, never blank) and `TestBundleKeysUsedExistInHungarian` refuses to ship one.
|
||
- **[FACT]** `controller/scripts/i18n_missing_gate.py` (in `controller_gates.py`, pre-push): keys exist,
|
||
no orphans, **the English gap is a ratchet** — `EN_MISSING_CEILING` (0 today) convicts above AND
|
||
below, so it can only be lowered deliberately. Raising it is a decision recorded here.
|
||
|
||
---
|
||
|
||
## 5. The gates, per language
|
||
|
||
**[FACT] Found in the spike: moving copy out of the templates blinded four gates and staled one.**
|
||
The retrieval-promise gate went red on STALE allowlist entries the moment the first page converted;
|
||
the emoji, native-confirm and secret-in-markup gates silently stopped seeing the moved copy. The HEAD
|
||
versions of the emoji and retrieval gates PASSED a planted emoji and a planted retrieval promise in the
|
||
bundles. They now judge the page **as rendered in each language** (`controller/scripts/i18n_bundle.py`
|
||
`read_template(path, lang)`); mojibake scans the bundles; decoys for each are in
|
||
`controller/scripts/test_gate_decoys.py`.
|
||
|
||
**[DESIGN] Voice, per language:**
|
||
- **hu** — the product speaks in „te". The converted copy carries formal („ön") forms (R-516). They are
|
||
not fixed by a localisation release (§1), so the gate counts them against `HU_FORMAL_CEILING` as a
|
||
ratchet: a new one convicts, fixing one lowers the ceiling. The ceiling followed the conversion:
|
||
6 (spike) → 10 (release A) → 12 (release B) → **16** (release C). **[FACT] Measured limit:** the stem
|
||
list is short — on the release C pages it counts 4 formal forms, while a wider list („írja be",
|
||
„adja meg", „válassza", „biztosan eltávolítja", „engedélyezze", …) finds at least 22 keys (R-516).
|
||
- **en** — second person, plain: no „please", no „kindly".
|
||
|
||
**[FACT] The retrieval-promise gate scans English too** since slice 1 release A (`EN_PATTERNS`,
|
||
`ALLOWLIST_EN`, decoys). Translating exposed that its Hungarian stems miss split verbs („állíthatók
|
||
vissza") — R-564. The hub copy gate and the catalog have no language concept
|
||
(`scripts/hub_copy_gate.py` stems end in a Hungarian character class; `catalog_gates.py` checks no copy).
|
||
|
||
---
|
||
|
||
## 6. Deliberately out
|
||
|
||
- **[DESIGN] The hub's operator pages** — the operator reads them; they stay English (decision 1).
|
||
- **[DESIGN] The first-boot wizard** (`controller/internal/setup/`, 8 pages, 95 strings) — out of scope
|
||
and obsolete (decision 4, 2026-09-17; `02-controller-module-map.md` L56). A household still reaches
|
||
it when bootstrap ingestion leaves `customer.id` empty (inventory §2.7). Its deletion is **R-554**.
|
||
- **[FACT]** App containers' own UIs (Uptime Kuma, PrivateBin…) — not Felhom's copy (R-516 items 5-6).
|
||
|
||
---
|
||
|
||
## 7. The catalog's copy model
|
||
|
||
**[FACT] MEASURED 2026-09-20 (controller v0.257.0 + catalog pilot). The proposal below was tried and
|
||
it holds; what changed under measurement is written out, because the numbers it was specced against
|
||
were wrong in two places.**
|
||
|
||
A sibling block per language inside `.felhom.yml`, not a sibling file:
|
||
|
||
```yaml
|
||
description: Titkosított jegyzetek…
|
||
app_info:
|
||
first_steps: [...]
|
||
i18n:
|
||
en:
|
||
description: Encrypted notes…
|
||
app_info:
|
||
first_steps: [...]
|
||
```
|
||
|
||
Why: one app's copy stays in one file a reviewer sees whole; the controller's `yaml.Unmarshal` ignores
|
||
unknown keys (`internal/stacks/metadata.go` L316 — **verified: this repo constructs no `yaml.Decoder`
|
||
at all, so there is no `KnownFields` anywhere to turn strict**), so older controllers are unaffected;
|
||
a missing English field falls back field by field.
|
||
|
||
**[FACT] The read path** is `Metadata.For(lang)` in `controller/internal/stacks/metadata_i18n.go`,
|
||
reached only through `LocalizeStacks` / `LocalizeStackPtr` / `Stack.MetaFor`.
|
||
|
||
- **`For("hu")` is the parsed struct with `I18n` cleared and nothing else**, deep-compared against all
|
||
53 real catalog files (`TestMetaForHuIsIdentity`, fixtures in `internal/stacks/testdata/catalog/`).
|
||
§1 therefore holds by construction for catalog copy, and `LocalizeStacks` returns the INPUT SLICE
|
||
for Hungarian rather than a copy, so nothing on the Hungarian path is even touched.
|
||
- **Fallback is field by field, and blank counts as absent** (ruling 3). A half-translated app is a
|
||
legal state — that is what makes the batches pushable one at a time.
|
||
- **The three prose lists replace WHOLE** (`use_cases`, `first_steps`, `prerequisites`): merging item
|
||
3 of one language with item 4 of another produces a list nobody wrote. **Every other list is
|
||
matched by its own key** — `deploy_fields` by `env_var`, options by `value`, `optional_config`
|
||
groups by `match_group` (the Hungarian `group` value they translate; a group has no other identity),
|
||
its fields by `env_var`, integrations by `target`, data paths by `path`. Position matching
|
||
mistranslates silently the first time a Hungarian field is inserted above another, and the page
|
||
still looks right.
|
||
- **`For` never writes through.** The metadata lives in the stack manager's cache and is shared by
|
||
concurrent requests; an in-place merge would put one household's language on another's page.
|
||
- **Two copy producers 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.
|
||
|
||
**[FACT] The numbers, measured on `94bc5febaca2`** — the inventory's were close on one count and wrong
|
||
on another, and both matter to the gate:
|
||
|
||
| | inventory / task | measured |
|
||
|---|---|---|
|
||
| copy strings | 835 | **1 032** (incl. 13 placeholders the table omitted, and every ASCII label) |
|
||
| with a Hungarian letter | 832 | **832** ✔ |
|
||
| ASCII-ONLY Hungarian | „Igen", „Nem", „Nincs" — three | **~120**. Those three do not occur; the three ASCII-only OPTION labels are „Magyar", „Angol", „Magyar + Angol" — language names, not yes/no words |
|
||
|
||
The ASCII-only count is the one that decides an instrument: „Aldomain" (53×), „A szerver domain neve"
|
||
(53×), „Jelentkezz be: …" (5×), „Magyar", „Angol", „Titkos kulcs", „Oszd meg a linket". **An
|
||
accent-only scan passes every one of them inside an English block** — the same blind spot §2.3 records
|
||
for the English page test (R-565), arriving again in a different repo. So
|
||
`app-catalog-felhom.eu/scripts/check-copy-i18n.py` folds and matches word-bounded stems, with a
|
||
positive and two negative controls printed on every run.
|
||
|
||
**[FACT] The gate**, fifth row of `catalog_gates.py`, static, in the pre-push hook:
|
||
|
||
1. **FREEZE** — every Hungarian copy string equals `scripts/copy_freeze/hu.json`, captured in its own
|
||
commit before any translation. Runs on all 53 apps whatever scope is named. A new app must be
|
||
admitted with `--add-app NAME --reason "…"`.
|
||
2. **STRUCTURE** — the `en` block may carry copy fields and nothing else; every key-matched entry must
|
||
have a Hungarian twin, or it is INERT on the box and the translator never learns.
|
||
3. **LANGUAGE** — no accented letter, no ASCII-only Hungarian, no „please"/„kindly", no English
|
||
retrieval promise the Hungarian does not make, the app name and „Felhom" preserved.
|
||
4. **CREDENTIALS** — the login tokens inside `default_creds` and the initial-credentials note survive
|
||
translation verbatim, matched on WORD boundaries.
|
||
5. **RATCHET** — `EN_MISSING_CEILING`, convicting above and below, counted over the WHOLE catalog
|
||
whatever scope is named.
|
||
|
||
33 decoy cases (R-421). **Three defects in the gate were found by its own decoys and not by reading
|
||
it** — R-592.
|
||
|
||
**[FACT] What a gate cannot judge**, and the pilot report lists per app: whether an app's „first
|
||
steps" name the buttons that app's ENGLISH interface actually shows. The Hungarian was written against
|
||
whatever interface the author had; the English UI label may differ. Those lines are marked unverified
|
||
and belong to the slice-6 walk (R-561).
|
||
|
||
## 8. Dates, numbers, plurals — as found
|
||
|
||
- **[FACT]** Plurals: 42 Go format strings with `%d` and 7 template runs with a numeric parameter
|
||
(inventory §2.2/2.1). Hungarian needs one form; English two.
|
||
- **[FACT]** Dates: 10 layout literals in `internal/web` Go, 2 in templates that **disagree with each
|
||
other** (`2006. 01. 02. 15:04` vs `2006-01-02 15:04`), 25 more in the controller; 9 copy-producing
|
||
helpers (`timeAgo` „%d perce", `nextRunLabel` „ma"/„holnap", `pruneLabel` „vasárnap"…).
|
||
- **[FACT]** Sizes print a decimal POINT (`%.1f GB`) — Hungarian convention is a comma. The Hungarian
|
||
pages are already un-Hungarian there; §1 keeps it that way until someone decides otherwise (R-562).
|
||
- **[FACT]** Word order: 237 Go format strings and 111 concatenations; in templates, JS sentences split
|
||
around `+ name +` („Az alkalmazás (" + name + ") törölve lett.") — translatable but fragile.
|
||
- **[FACT]** Flash messages travel **inside the redirect URL** (`?flash=<text>`) in the language of the
|
||
handler that redirected — slice 2 moves them to keys.
|
||
|
||
---
|
||
|
||
## 9. Compared, not shown — latent bugs a translation would trigger
|
||
|
||
**[FACT] There were FIVE, and they are fixed (controller v0.251.0, R-553 + R-563, 2026-09-17).** Four
|
||
in Go and one in a page script. Each producer now attaches a machine-readable signal and each decision
|
||
reads that signal; every Hungarian sentence is byte-identical (pinned per producer), and the hub report
|
||
is unchanged.
|
||
|
||
| decision | read before | reads now |
|
||
|---|---|---|
|
||
| deploy HTTP status — `api.deployStatusFor` | „kötelező", „memória", "does not exist", "already deployed" | `stacks.ErrRequiredField`, `ErrPathMissing`, `ErrNotEnoughMemory` (400); `ErrAlreadyDeployed` (409) |
|
||
| off-site failure class — `ClassifyOffsiteFailure` | „tárhelykeretet" | `backup.ErrOffsiteQuota` |
|
||
| alert placement — `web/alerts.go` | „meghajtó" / „adattároló" in the warning | `monitor.WarnKindStorageNotSeparate`, carried in `HealthReport.WarningKinds` (internal; NOT on the wire) |
|
||
| the stale off-site note — `offboxWarningDisplay` | „nincs mentésre jelölt alkalmazás" in the PERSISTED text | `settings.OffboxTarget.LastWarningKind` = `backup.OffboxWarnNoAppsSelected` |
|
||
| the remote-backup poll — `backups_remote.html` | the displayed word „Fut" | `data-status="running"` on the status element (R-563) |
|
||
|
||
**[DESIGN] The rule this establishes:** a text signature may remain ONLY where the text is not ours.
|
||
The classifier's restic and ssh signatures stay, because that output is neither written nor translated
|
||
here; every sentence this product writes is display, and a decision reads a signal beside it.
|
||
`internal/util.KindErrorf` exists for exactly that: the same message bytes `fmt.Errorf` produced, plus
|
||
a sentinel for `errors.Is` — never `fmt.Errorf("%w: …")`, which would prepend the sentinel's own text
|
||
to the customer's sentence.
|
||
|
||
**[FACT] One exception, with an end date:** a box upgraded to 0.251.0 carries the OLD persisted note
|
||
with no kind until its next off-site run, so `offboxWarningDisplay` keeps the substring test for
|
||
`kind == ""` only. **Slice 2 (R-557) must not translate that producer until R-570 closes.**
|
||
|
||
**[FACT] Measured after the fix** (`audits/r553-2026-09-17/`): the inventory's comparison detector finds
|
||
**no template compare** (was one) and no Go compare of ours — the two remaining hits are the known
|
||
false positive (an `[INFO]` log line) and the deliberate legacy fallback above; planted decoys prove the
|
||
detector still convicts. **What it does NOT cover:** four API handlers pick their status by matching
|
||
ENGLISH internal words — the same shape, one language over, filed as R-569 rather than folded in here.
|
||
|
||
---
|
||
|
||
## 10. The plan
|
||
|
||
**ALL SIX SLICES ARE DONE** — slice 1 (2026-09-17, v0.250.0), slice 2 (2026-09-18, v0.254.0),
|
||
slice 3 (2026-09-18, hub v0.118.x), slice 4 (2026-09-18, ISO 1.29.0), slice 5 (2026-09-20, v0.257.0 +
|
||
the whole catalog), slice 6 (2026-09-20, v0.258.0 + the guide + the walk). What remains is written in
|
||
§10.6b's residue table, and every line of it has a row.
|
||
|
||
|
||
Costs are CC-hours, estimated from the spike (mechanism + three pages + layout + gates ≈ one working
|
||
session). Each slice ends with the parity gate green for every page it touched.
|
||
|
||
| slice | row | what | proves | cost |
|
||
|---|---|---|---|---|
|
||
| 0 (done) | — | mechanism, 3 pages + layout, setting, report field, gates | Hungarian unchanged by measurement; English reachable | spent |
|
||
| 1 (done, v0.248.0–v0.250.0) | R-556 | the other 31 dashboard templates (~1 500 strings, 600 of them JS), parity fixtures per page; switch shown to everyone; English retrieval stems; the extractor's ASCII word list reviewed per page | the whole dashboard in English with Hungarian byte-identical | 12–16 h, three releases |
|
||
| 2 (done, v0.252.0–v0.254.0) | R-557 | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages; the saved notes; the globe | a page's server messages follow the language | **CLOSED 2026-09-18** |
|
||
| 3 | R-558 | hub: per-customer language at creation; `configgen` renders it; `customerMessages` (39), the 5 lifecycle mails and the self-bind page in English; dispatcher reads the reported language | a household's e-mails arrive in its language | 6–8 h, one hub + one controller release |
|
||
| 4 | R-559 | console banner (34 lines, console-font limits) and the download page | an English household meets English from the first boot screen (in scope — ruling 1b) | 4–6 h + an ISO train |
|
||
| 5 | R-560 | catalog: §7 format, controller reads it, 835 strings / ~5 000 words, catalog copy gates per language | an app card, its settings and its first steps in English | 12–16 h |
|
||
| 6 | R-561 | the volunteer guide in English (~1 600 words); **then a stranger's first hour in English**, the 2026-09-14 walk; closes R-516 against the inventory | a newcomer can set up and use a box in English | 6–8 h |
|
||
|
||
### 10.1 Slice 2, as measured (release A, controller v0.252.0, 2026-09-18)
|
||
|
||
**[FACT] The counts the plan carried were the inventory's, and they moved.** At slice 2's base commit
|
||
`736f54b49610` there were **1 120** Hungarian Go literals, not 1 141; `handlers.go` 120 (not 121),
|
||
`offbox_handlers.go` 70 (not 72), `storage_handlers.go` 45 (not 48), `api/router.go` 18 (not 19).
|
||
Release A converted **226** of them and added 237 country keys.
|
||
|
||
**[DESIGN] A flash is a KEY, because it is read by a different request than the one that wrote it.**
|
||
`?flash=<key>&fa=<param>…`, built by `flashQuery`, resolved by `s.flashFrom`. A value the bundle does
|
||
not know is shown **verbatim** — that is what keeps a link already in a customer's tab, a bookmark or
|
||
a back-forward cache reading correctly, and it is the same rule that makes a hand-typed `?flash=`
|
||
harmless (`TestFlashKeyRoundTrip`). The same shape one layer in: an **alert banner** is built by a
|
||
background health cycle and read minutes later, so `Alert` carries `MessageKey` + `MessageArgs` and
|
||
`GetAlerts(lang)` renders on the way out.
|
||
|
||
**[DESIGN] Word order is Go's explicit argument index, not a second placeholder syntax.** The plan
|
||
proposed a named-parameter (`{{.Name}}`) form for multi-parameter Go messages. English reorders with
|
||
`%[2]s`, which `fmt` already understands, so the Hungarian value stays **the format string the code
|
||
always had, byte for byte** — and that is precisely what the parity gate compares. A second syntax
|
||
would have needed an exception in the measurement, which is the thing this slice cannot afford.
|
||
`TestBundleParametersMatchAcrossLanguages` counts an indexed verb as one verb.
|
||
|
||
**[FACT] Parity for Go copy is measured, not read.** `controller/scripts/i18n_go_parity.py` freezes
|
||
every Go string literal at the base commit (**7 467**, `scripts/i18n_go_base.json`) and refuses a key
|
||
whose Hungarian is not that text byte for byte — reworded, re-punctuated, split or joined.
|
||
`scripts/i18n_go_keys.json` records what each key replaced (a string for one literal, an ordered list
|
||
for a join, `{"inside": …}` for the three sites where the Hungarian was already URL-escaped inside a
|
||
redirect target). In `controller_gates.py`; three decoys, each seen to convict.
|
||
|
||
**[FACT] The gate found a live defect in its own first version — the R-565 class again.** The capture
|
||
filtered literals through an ASCII-Hungarian word list and missed seven real ones („Naponta",
|
||
„5 percenkent", „Eletjel (Heartbeat)", „Adatbazis mentes", „Biztonsagi mentes", „Mentes integritas",
|
||
„Rendszer allapot"); the gate refused the keys citing them. **The filter is gone.** The index answers
|
||
one question — *did the base commit contain this text?* — and an unfiltered index answers it for every
|
||
string. **The lesson generalises: a word list is a list, and nobody knows which words are missing.**
|
||
|
||
**[FACT] Two claims in the plan that live source disproved, both about the wire.**
|
||
|
||
1. `cloudflare/countries.go` is **not** on the wire. `report/builder.go` carries country **codes**;
|
||
no name leaves the box, and `CountryName()` has no caller. The 237 names were therefore free to
|
||
translate, and are — at DISPLAY, in `api/geo.go`, re-sorted per language. The table stays: it
|
||
validates codes.
|
||
2. The hub does **not** compose every customer mail from the event kind. `FormatCustomerEmail`
|
||
(`hub/internal/notify/templates.go` L167-210) uses `customerMessages[eventType]` when it has one
|
||
and **falls back to the controller's message when it has none**, appending it as „- Üzenet: %s"
|
||
whenever the two differ; several event types carry no entry precisely so the controller's sentence
|
||
IS the mail. So `notify/notifier.go`'s 31 messages are customer-facing text on the wire. The
|
||
verdict (do not translate them here) was right; the reason was not, and the reason is what slice 3
|
||
has to act on.
|
||
|
||
**[DESIGN] On the wire = not translated, and now measurable.** `internal/monitor` and
|
||
`internal/notify` carry wire goldens capturing those producers' exact bytes at the base commit. A
|
||
later slice that translates one fails a test that prints both strings. R-558 is the row that unfreezes
|
||
them, by teaching the hub which language to compose in.
|
||
|
||
**[FACT] Still Hungarian after release A:** 894 literals — **176 of them `fmt.Errorf`/`errors.New`**
|
||
(release B), the text a background run persists (release C), the R-570 producer, `handler_debug.go`
|
||
(R-574), two copy-producing funcmap helpers (R-572) and the two channel-health banners (R-573).
|
||
|
||
**[DESIGN] Persisted text (release C) — the plan's §16 option 1, its own stated default.** A note is
|
||
written in the box's language **at the time it is written**, and a household that switches sees last
|
||
night's note in the old language until the next run rewrites it. The alternative (store a code, render
|
||
live) costs a dozen new persisted fields and a legacy path for each — the R-570 shape, a dozen times
|
||
over. Recorded here when release C ships.
|
||
|
||
### 10.2 Slice 2 release B — an error carries the key of the sentence it is (v0.253.0)
|
||
|
||
**[DESIGN] An error is made where there is no language and printed where there is no key, so it
|
||
carries the key across.** `util.MsgError(key, args…)` / `MsgErrorf(kind, key, args…)`. **All 179
|
||
Hungarian error literals are converted; zero remain** (an ASCII-fragment search with a positive and a
|
||
negative control, case-insensitively, plus an accented-letter search).
|
||
|
||
Four properties, each one a different failure if it is missing:
|
||
|
||
| property | the failure without it | pinned by |
|
||
|---|---|---|
|
||
| `Error()` is the Hungarian, byte for byte | 179 producers could not be converted until every printer was, in one commit | `TestMsgErrorKeepsKindAndHuText` |
|
||
| `errors.Is` answers for the kind **and** a wrapped cause | `KindErrorf` returned the kind alone; a caller testing for the cause silently stops matching | `TestMsgErrorUnwrapsTheCauseToo` |
|
||
| an error ARGUMENT renders recursively | „formázás sikertelen: %w" is a sentence wrapping a sentence; half of it would stay Hungarian | `TestErrTextRendersAWrappedMessageErrorToo` |
|
||
| a FOREIGN error prints verbatim | restic, docker, ssh and the stdlib are not ours (§9's rule) | `TestErrTextFallsBackVerbatim` |
|
||
|
||
**[DESIGN] Plurals are a BUNDLE rule, not a call-site flag.** *A key that carries `.one`/`.other`
|
||
forms in a language is a plural key, and its FIRST parameter is the count* (`i18n.Bundle.form`).
|
||
Hungarian never carries them, so a Hungarian render is unchanged at every count. One answerable
|
||
sentence: *where does the fact that English needs two forms live?* Options: (1) a flag at every
|
||
producer of every count message, in three packages — cost: the one somebody forgets reads „3 app is
|
||
not running", with nothing to catch it; (2) the bundle. **Chosen (2)**: the bundle is where a
|
||
translator works, and it needs no change at any call site. `.one`/`.other` become RESERVED suffixes —
|
||
`TestNoOrdinaryKeyEndsInAPluralSuffix`, which caught a real collision (`alert.deadapp.one`) the day
|
||
the rule landed.
|
||
|
||
**[FACT] The parity gate has a blind spot, measured rather than reasoned (R-576).** The bulk
|
||
converter silently dropped the continuation of a multi-line concatenation, damaging **7** producers —
|
||
and `i18n_go_parity.py` stayed GREEN, because every surviving fragment WAS a byte-equal base-commit
|
||
literal. Its question is *"is this text real?"*; it cannot ask *"did the call keep all of it?"*. Two
|
||
BEHAVIOUR tests caught it, because they assert the sentence a customer reads. **A structural gate over
|
||
the TEXT cannot see a defect in the CALL** — the general form, and the reason a rendered test is not
|
||
made redundant by a gate.
|
||
|
||
**[FACT] A second instrument defect, same session (recorded because the shape recurs).** The script
|
||
counting what was left was case-sensitive, so it reported "0 error literals remain" while five did
|
||
(„occ parancs sikertelen", „hub hiba", „OnlyOffice aldomain nem ismert" ×2). That is R-565's shape
|
||
inside the measurement. Every "no Hungarian left" claim in this slice is made case-insensitively and
|
||
with both controls.
|
||
|
||
**[FACT] One gap release B could not close: R-575.** `memoryVerdict`'s soft overcommit WARNING is a
|
||
string with no error to carry a key and no language where it is built, so it renders Hungarian on an
|
||
English page. The copy is in the bundle; only the render is fixed. Named in the code, filed as a row.
|
||
|
||
### 10.3 Slice 2 release C — the saved notes, and a globe (v0.254.0). SLICE 2 CLOSED.
|
||
|
||
**[DESIGN] A note a background run SAVES is written in the BOX's language at write time** (operator
|
||
ruling, §16 option 1). ~70 producers. **The consequence, recorded because it is the cost of the
|
||
choice:** a household that switches language sees the previous run's note in the old language until
|
||
the next run rewrites it — usually the next night. The alternative (store a code, render live) needs a
|
||
dozen new persisted fields and a legacy path for each: the R-570 shape a dozen times over.
|
||
`EndRestoreOp` now receives no Hungarian literal from anywhere.
|
||
|
||
**[FACT] A deadlock, introduced and caught by the suite hanging (R-578).** `UpdateOffboxStatus` holds
|
||
the settings WRITE lock while running its callback; `boxLang()` reads the language through the READ
|
||
lock; `sync.RWMutex` is not reentrant. A note rendered inside that callback deadlocks **holding the
|
||
settings lock**, which wedges everything else on the box that touches `settings.json`. The only
|
||
symptom was `go test` going from 8 minutes to a 25-minute timeout. **The general rule this establishes:
|
||
a helper that takes a lock must never be called from inside a callback that holds one** — and a hang
|
||
is the worst symptom to diagnose, which is why R-578 asks for a gate rather than one test in one package.
|
||
|
||
**[DESIGN] Decision 6, superseded a second time: the switch is a GLOBE, and a visitor's language is
|
||
their own.** One answerable sentence: *how does someone who cannot read Hungarian find the way out of
|
||
Hungarian?* Options: (1) keep two text links („Magyar"/„English") in the sidebar footer — cost: they
|
||
wrap at the sidebar's width, and finding them means recognising two words as links; (2) one globe, the
|
||
symbol every web user already reads as "language". **Chosen (2)**, `<details>`/`<summary>` so the menu
|
||
needs no script and a screen reader announces it. Language names inside are shown in their own
|
||
language and are never translated. Drawn inline rather than in the icon sprite, because the sprite
|
||
lives only in `layout.html` and the pages outside the dashboard chrome have their own shell.
|
||
|
||
**[DESIGN] Who the page is FOR decides where its globe posts, and getting that wrong makes the button
|
||
do nothing.**
|
||
|
||
| page | reader | globe posts to | their choice lives in |
|
||
|---|---|---|---|
|
||
| every dashboard page | the household, signed in | `/settings/language` (session CSRF) | `settings.json` |
|
||
| `/recovery` — an AUTHENTICATED route | the household, signed in | `/settings/language` | `settings.json` |
|
||
| `/login`, `/claim` | a visitor, no session | `/lang` (no CSRF) | the `felhom_lang` cookie, their browser |
|
||
| the two guest share pages, the catch-all | a stranger / nobody | **no globe** | — (R-577, the operator's) |
|
||
|
||
`langFor`'s order is fixed: `?lang=` → **the household's setting when a session exists** → the cookie
|
||
when there is none → the setting → `hu`. **A signed-in household never reads the cookie**, so they
|
||
cannot inherit a language a previous visitor picked in the same browser. The recovery row above was
|
||
measured live before it was right: an anonymous form there sets a cookie that `langFor` then ignores,
|
||
and the button appears to do nothing.
|
||
|
||
**[FACT] Where the globe SITS, and a stale-stylesheet trap that only a screenshot could show
|
||
(v0.255.0, R-579).** On the pages outside the dashboard chrome the globe is **inside the card, centred
|
||
under the footer**, with the menu opening upward — the shared `.lang-globe-menu` rule, so the two
|
||
surfaces cannot drift apart. It was first placed at the corner of the VIEWPORT, which read as a stray
|
||
browser control rather than part of the page. And five of those shells requested `style.css` with **no
|
||
`?v=`**, so a browser holding a copy from before the globe existed kept serving CSS with no
|
||
`.lang-globe` rules and it rendered as a bare, unstyled `<details>`. **Every test passed, because they
|
||
all read the markup and the fault was in which CSS file the browser fetched.** `Version` is now set in
|
||
`executeTemplateLang`, once, for every shell.
|
||
|
||
**[DESIGN] `POST /lang` is CSRF-exempt, for a reason narrow enough to check.** The only achievable
|
||
effect of a forged request is to change the language of the page the victim's own browser shows them.
|
||
It writes one display-only cookie, reads nothing, touches no setting, and `safeBackPath` refuses a
|
||
protocol-relative `//host` as well as an absolute URL — *"starts with `/`"* alone is not the test,
|
||
because a browser reads `//evil.example` as another origin. **If that handler ever gains a second
|
||
effect it needs CSRF that day.** That is the anonymous surface `04-control-plane-authorization.md`
|
||
governs: changing what a visitor reads is within it, changing anything the household owns is not.
|
||
|
||
**[DESIGN] §16, the operator's default, taken: a successful CLAIM carries the visitor's language into
|
||
the household's setting.** Someone who switched the claim page to English and then claimed the box
|
||
chose English. Only on success, and only there — the one moment an anonymous visitor becomes the
|
||
household.
|
||
|
||
**[FACT] The parity exceptions, measured rather than asserted.** 106 fixtures, a real (LCS) diff
|
||
against the fixtures as they stood at v0.253.0: **3 change shapes** (the footer; the recovery globe;
|
||
the login/claim globe) and **5 byte-identical**, which are exactly the pages that must not change.
|
||
`audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt`. **A second measurement error worth
|
||
keeping: the first attempt compared LINE BY INDEX, and an insertion shifts every line below it — it
|
||
reported 60 520 changed lines and measured nothing. A line-index compare is not a diff.**
|
||
|
||
---
|
||
|
||
### 10.4 Slice 3 — the hub's e-mails follow the household (hub v0.118.x, controller v0.256.x). SLICE 3 CLOSED.
|
||
|
||
SHIPPED 2026-09-18. Full design: `05-hub-architecture.md` §15.
|
||
|
||
**The hub half (v0.118.0, v0.118.1).** `hub/internal/i18n`, 79 keys, hu authoritative, hu fallback,
|
||
missing-key ceiling 0. Every customer mail and the public bind page render from it.
|
||
`customerMessages`/`severityLabels` are DERIVED from the bundle, so a sentence is written once and
|
||
"a new event type enters `allowedEventTypes` and `customerMessages` together" now means a line in
|
||
`hu.json` plus its English twin. **56 mail goldens per language, captured from v0.117.0 before any
|
||
string moved; all 56 Hungarian ones pass unchanged.** Language order: **last reported →
|
||
created-with → `hu`**; `reports.language` defaults to EMPTY (never told us ≠ chose Hungarian) and
|
||
the newest report is found by the autoincrement `id`, because `received_at` has second granularity.
|
||
The bind page's `expired` state always renders Hungarian — it is the state an unknown token lands
|
||
in, and the language must not answer what the text refuses to.
|
||
|
||
**The box half (v0.256.0, v0.256.1).** `message_customer` on `POST /api/v1/event`: the same sentence
|
||
in the household's language, `omitempty`, beside the unchanged Hungarian `message`. **A Hungarian
|
||
household sends no second copy at all**, so the fleet's payload is byte-for-byte what it is today and
|
||
the hub's fallback path stays the one production exercises. 19 producers render both from ONE bundle
|
||
key; the Go parity gate checks all 19 against the base-commit literals. `customer.language`
|
||
bootstraps a new box (stored choice → config → `hu`) and is never persisted into `settings.json`.
|
||
v0.256.1 added the one observable the feature was missing: the push log says `[hu-only]` or
|
||
`[+household(en)]`.
|
||
|
||
**Proven live, twice.** Part A: a real English e-mail read in the operator's inbox and the Hungarian
|
||
one 74 seconds later, same button, same box. Part B: the same event pushed 57 seconds apart showing
|
||
`[hu-only]` then `[+household(en)]`, with an identical Hungarian sentence both times.
|
||
|
||
**What is still Hungarian for an English household:** six producers whose sentence arrives already
|
||
finished from another package (R-585) — of which `offbox_enlarge_blocked` matters most, because it
|
||
has no hub entry so its raw sentence IS the mail. Plus the R-570 sentence. The 15 operator-tier types
|
||
are Hungarian by design.
|
||
|
||
**Gaps filed:** R-581 (same-second report ties, and `GetCustomers()` still has the shape), R-582 (an
|
||
English copy-guard ported word-for-word convicted 141 honest sentences), R-583 (the test mail was the
|
||
one mail that did not follow the language — and the one an operator would use to check), R-584
|
||
(credential-bearing probes left in a live guest's `/tmp`), R-585. **R-555 closed.**
|
||
|
||
|
||
### 2026-09-17 (on the starter)
|
||
|
||
1. **Scope: what the household sees — the controller, its e-mails, the guide, the app catalog.** Not
|
||
the hub's operator pages.
|
||
2. **Who picks:** the operator per customer at creation (default Hungarian); the household switches on
|
||
the dashboard; the box reports it so the hub's e-mails follow.
|
||
3. **Fallback:** a missing English line shows the Hungarian one and is counted; never a key or a blank.
|
||
4. **The first-boot wizard:** out of scope, obsolete — a separate row to delete it (R-554).
|
||
|
||
### Decided by CC in the spike — operator may reverse
|
||
|
||
5. **Mechanism (b2)** — §2.1.
|
||
6. ~~**Switch hidden while only three pages are English** — §3.~~ **Superseded 2026-09-17** by slice 1
|
||
release C (v0.250.0): every template converted, switch shown to every household (§3).
|
||
**Superseded again 2026-09-18** by slice 2 release C (v0.254.0): the switch is a GLOBE, it is on the
|
||
sign-in and claim pages too, and a visitor's choice lives in their own browser (§10.3).
|
||
|
||
8. **A successful claim carries the visitor's language into the household's setting** (§16 default,
|
||
taken 2026-09-18). Only on success; every other anonymous request leaves `settings.json` alone.
|
||
|
||
### 2026-09-17 (evening) — the two open questions, ruled
|
||
|
||
1b. **The console banner and the download page ARE in scope** (operator: „yes"). Slice 4 (R-559) is
|
||
unblocked; it stays late in the plan and rides an ISO release train.
|
||
7. **Interface nouns translate** (operator: „translate"). „Indítópult" → Launcher, „Vezérlőpult" →
|
||
Dashboard, „Biztonsági mentés" → Backup, as the spike built. App names and „Felhom" stay as they
|
||
are. Every slice follows this.
|
||
|
||
### 10.5 Slice 4 — the console banner and the download page (ISO 1.29.0 source, R-559)
|
||
|
||
SOURCE SHIPPED 2026-09-18. **The image is built but NOT published** — that is an operator step; see
|
||
`documentation/audits/i18n-slice4-2026-09-18/README.md`.
|
||
|
||
**The box's screen is BILINGUAL, and that is a decision rather than a stage.** At the moment the
|
||
pairing code is shown, nobody has told the box who owns it — there is no language to follow. So the
|
||
three console texts carry the Hungarian block byte-for-byte as before, then one blank line, then an
|
||
English block, inside the same frame: `print_pairing_banner`, `print_bound_banner` and
|
||
`install_felhom_issue` (with the `postinst`'s byte-coupled copy of the issue text). The GRUB entries
|
||
gain an English half. **§16 default taken: bilingual for good** — it needs no hub change, no protocol
|
||
field, and it has no way to show the wrong language.
|
||
|
||
**The Hungarian is a golden, not a grep** (`scripts/iso/test/golden/*.hu.txt`, captured before any
|
||
English existed). The harness also pins: no Hungarian letter in the English block (with the Hungarian
|
||
block as the positive control), every line ≤ 80 columns, and **the whole paint ≤ 25 rows**. The
|
||
pairing banner is **24 rows on a 25-row console** — one row of margin, which is why the height is
|
||
pinned: two more lines and the Hungarian pairing code, at row 5, scrolls off the top.
|
||
|
||
**The download page has an English twin** at `felhom.eu/en/download`, linked both ways with
|
||
`hreflang`. The rest of the marketing site stays Hungarian (ruling 1b). A site gate refuses the two
|
||
pages naming different installer files or checksums.
|
||
|
||
**The release gate had to be amended.** G16 required every Felhom-authored string to be Hungarian and
|
||
would have stopped this publication; ruling 1b supersedes that scope, so G16 was rewritten rather
|
||
than waived — Hungarian FIRST, pinned by the golden.
|
||
|
||
**Gaps filed:** R-586 (the ISO harness had been red for two days and is in no gate), R-587
|
||
(root-password files in the publish source directory), R-588 (release records live in two places).
|
||
|
||
### 10.6 Slice 5 — the app catalog's own words (controller v0.257.0 + the catalog pilot, R-560)
|
||
|
||
PART A + PART B SHIPPED 2026-09-20. **PART C — the other fifty apps — is deliberately not started:**
|
||
the §16 default was to stop after the pilot for the operator's read, and nobody has read it yet.
|
||
|
||
**The format is no longer a proposal — §7 is now measured.** The controller reads the block; three
|
||
apps carry one; the fleet is unaffected until the floor moves.
|
||
|
||
**What was proven, live, rather than reasoned about:**
|
||
|
||
- **The English pages show the English.** On demo-hp (0.257.0) the app pages for privatebin,
|
||
paperless-ngx and romm, the Apps list and two deploy pages render the catalog's English text.
|
||
- **The Hungarian did not move.** The same seven pages fetched 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.
|
||
- **An old controller ignores the block.** demo-felhom runs **0.255.0**. With the pilot 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 includes the sync lines**, so the absence is a fact and not a
|
||
dead log.
|
||
|
||
**What the English pages still carry in Hungarian, measured with an ASCII-folded scan plus an accent
|
||
scan, both controlled:** exactly three things, and one of them is correct.
|
||
|
||
1. The language picker's own „Magyar" button — a language picker names each language in its own
|
||
tongue. Not a defect.
|
||
2. The update badge „Naprakész" and its tooltip — **R-589**. Slice 1 listed it (§2.3); slice 2 was to
|
||
take it and closed without it.
|
||
3. The data-folder card's consequence sentence — **R-590**, and this one makes a promise about the
|
||
customer's files while the label above it is already English.
|
||
|
||
Everything else Hungarian on an English Apps LIST is simply the fifty apps nobody has translated yet.
|
||
|
||
**Gaps filed:** R-589, R-590, R-591 (`Stack.Copy()` has one shallow field and it is the new one),
|
||
R-592 (three defects inside the new gate, each found by its own decoy).
|
||
|
||
**Evidence:** `documentation/audits/i18n-slice5-2026-09-20/`.
|
||
|
||
**PART C SHIPPED THE SAME DAY.** The operator read the pilot's English and said go, so the other
|
||
fifty followed its voice in three pushes — 17, 17 and 16 apps; 319, 317 and 306 strings. **1 031 of
|
||
the catalog's 1 032 customer-facing strings now carry an English twin.**
|
||
|
||
**[DESIGN, decided by CC] The blocks are GENERATED from a flat `{path: english}` map, not
|
||
hand-written.** Fifty nested blocks whose keys must match the Hungarian side exactly is fifty
|
||
chances to mistype an `env_var` — and **a mistyped key is INERT on the box, not an error**, so the
|
||
translator never learns. The generator builds from the same flat paths the freeze uses, which are
|
||
derived from the Hungarian file itself, so a key that does not exist on the Hungarian side cannot be
|
||
written at all. The review then happens on the map, where one line is one string, instead of on
|
||
YAML indentation.
|
||
|
||
**[FACT] The one string left untranslated, on purpose.** papra's
|
||
`deploy_fields[AUTH_SECRET].description` reads „Az alkalmazás aldomainje" — the sentence that belongs
|
||
on `SUBDOMAIN`, sitting on a session-signing key (R-593). §1 forbids changing the Hungarian in a
|
||
localisation release, and translating a wrong sentence faithfully would ship the error in a second
|
||
language. So it falls back. **That is why `EN_MISSING_CEILING`'s floor is 1 rather than 0, and it is
|
||
written into the ceiling's own comment** so a later session does not "reach zero" by editing
|
||
Hungarian.
|
||
|
||
**[FACT] Measured on the English Apps list, all 53 apps: ZERO Hungarian app descriptions.** The only
|
||
Hungarian left on that page is the „Naprakész" badge (R-589, ten occurrences) and the language
|
||
picker naming itself. The Hungarian Apps list is identical to the pre-slice capture once the
|
||
per-session CSRF token and Docker's own „Up N hours" string are normalised.
|
||
|
||
**[FACT] The gate convicted two of the translator's own sentences and was HALF right.** Vaultwarden's
|
||
invite step ended „…can open an account", and the English retrieval-promise pattern reads `can …
|
||
open` as the claim that sealed backups can be opened. Opening an ACCOUNT is not that claim, so the
|
||
conviction was a false positive — and the wording was also the weaker wording, so it became „can
|
||
sign up". **But the gate has no way to REGISTER a true occurrence**, which the shared vocabulary's
|
||
own design calls for; R-594.
|
||
|
||
**[FACT] The fleet floor was raised to 0.257.0** the same session (operator asked), `min_agent`
|
||
0.131.0 declared — above the vouched golden 0.246.0, so the declaration is what carries it (R-472).
|
||
Hub: `managed floor SERVED for demo-felhom: floor 0.257.0, agent requirement "0.131.0" from declared
|
||
(golden 0.246.0)`. **demo-felhom went 0.255.0 → 0.257.0 by itself in under 12 seconds**, healthy,
|
||
its own log reading `settle-gate: GO — at/above floor 0.257.0`, and it then rendered the English
|
||
tagline — the floor delivered function, not just a version string. Peti's box and tester-1 are DOWN
|
||
and take it unattended when they return; untested on this version.
|
||
|
||
|
||
### 10.6b Slice 6 — the guide in English, and a stranger's first hour (controller v0.258.0, R-561)
|
||
|
||
SHIPPED 2026-09-20. **All six slices are now done.** The slice had three parts and all three landed:
|
||
the four leftovers slice 5 found live (v0.258.0), the guide's English twin, and the walk.
|
||
|
||
**Part 0 — the four leftovers.** R-589 (the update badge) and R-590 (the data-folder card's backup
|
||
promise) went through `localeFuncs` and `s.msgLang`; R-573 (the agent-channel and endpoint-drift
|
||
banners) is now keyed by the checker's own CLASSIFICATION, with the composed sentence kept as a
|
||
fail-open fallback. **R-572 was not what its row said** — measured, no template and no Go file called
|
||
`pruneLabel`/`nextPruneLabel`; they were dead func-map entries returning Hungarian, so they were
|
||
**deleted**, and the deletion is fail-loud (a template naming a removed func panics `loadTemplates`
|
||
at startup, proven).
|
||
|
||
**Part A — the guide.** `runbooks/VOLUNTEER-first-hour.en.md`, a twin: 16 sections in the same order,
|
||
identical step counts, table rows and warning blocks per section (0 sections differing in structure).
|
||
**Word counts are NOT a twin** — English runs 19 % longer overall and up to 42 % on the short
|
||
sections, because Hungarian is agglutinative; the ±15 % criterion the task set does not survive
|
||
contact with this language pair, and structure was measured instead. **The walk then corrected the
|
||
guide in five places** — it had been written from the bundle, and the bundle is not the screen.
|
||
|
||
**Part B — the walk.** `audits/DRILL-first-hour-en-0258-2026-09-20.md`. The verdict:
|
||
**not yet ready for an English-speaking tester, because the claim page answers in Hungarian (R-596)** —
|
||
everything else held.
|
||
|
||
**[FACT] What stays Hungarian for an English household — built ONLY from what the stranger saw:**
|
||
|
||
| what | why | row |
|
||
|---|---|---|
|
||
| ~~the **claim page's messages**~~ | ~~composed sentences passed into page data~~ | **R-596 CLOSED**, controller 0.259.0 |
|
||
| ~~the **setup code** and the **owner passphrase**~~ | ~~one Hungarian wordlist~~ | **R-597 CLOSED**, hub 0.119.0 |
|
||
| ~~the **Backup page's two protection warnings** and its two target names~~ | ~~composed sentences in Go~~ | **R-598 CLOSED**, controller 0.259.0 |
|
||
| the menu word **"Debug"** | it is already English; the complaint was the Hungarian household's | R-516 item 1 |
|
||
| the **operator's copy** of every event | operator-tier is Hungarian **by design** (ruling 1) | — |
|
||
| the **18 formal „ön" forms** | a localisation release may not change Hungarian bytes (§1); counted, ratcheted | R-516 |
|
||
| the **apps' own English UIs** | not Felhom's copy — and for this reader an advantage | — |
|
||
|
||
**That is the honest residue.** Everything not in this table — the download page, the console, all
|
||
three customer mails, the bind page and its refusal, the dashboard, Apps, Storage, Monitoring, the
|
||
Launcher, both app pages, the whole catalog, the language switch — was **English with zero Hungarian
|
||
lines** on a box installed from scratch that day.
|
||
|
||
**R-516 does NOT close**, and `audits/i18n-slice6-2026-09-20/R-516-item-by-item.md` says why item by
|
||
item: more than half its twelve items are about what a **Hungarian** household reads, and an English
|
||
walk cannot see them.
|
||
|
||
**R-214 closed as a side effect** — the console's last paint is now the bilingual "the box is linked"
|
||
banner. The 2026-09-14 walk recorded it as still reproducing.
|
||
|
||
### 10.6c Slice 6's residue, closed (controller v0.259.0 + hub v0.119.0, 2026-09-21)
|
||
|
||
The three rows above are closed. What is worth keeping is not that they closed but **what each one
|
||
turned out to be**, because two of the three were not what the row said.
|
||
|
||
**R-596 — the claim page.** Fourteen live call sites carrying **nine** distinct messages, not the
|
||
sixteen literals the row counted; one of the sixteen (`data["Title"]`) was **dead** — `claim.html` is
|
||
standalone and `.Title` belongs to `layout.html` — and was deleted rather than translated. The
|
||
anonymous, cookie-less page takes its language from `customer.language` in `controller.yaml`
|
||
(`langFor` → `settings.GetLanguage` → `configLanguage`); that chain was an unpinned assumption and is
|
||
now a test.
|
||
|
||
**R-598 — the backup warnings.** `degradedMessageFor` now returns a **key**, so the decision stays
|
||
language-free and in one place while the words are chosen by whoever knows the reader. The English
|
||
is asserted to carry the same NEGATION the Hungarian does — *protects against corrupted files, but
|
||
**not** against a disk failure*. An English sentence that promised disk-failure protection would be
|
||
worse than leaving it Hungarian.
|
||
|
||
**R-597 — the codes.** Two of the row's three secrets were mis-attributed:
|
||
|
||
- **The recovery code was never Hungarian.** `felhom-agent` mints it from the **EFF large wordlist**
|
||
and always has — ten English words, ≈129 bits. The hub does not own it, and no row was added for
|
||
it: a second definition of that secret is exactly the drift this section exists to prevent.
|
||
- **No claim mail states a word count.** The only count wording in the product is the bind page's
|
||
passphrase hint, and its English half is now count-free.
|
||
- The setup code and the owner passphrase now follow the household's language, one word longer in
|
||
English so the entropy **never drops**: setup 3 hu (44.6 bits) → 4 en (51.7); passphrase 5 hu
|
||
(74.3) → 6 en (77.5). The floor is computed from the embedded lists at test time, not asserted
|
||
against a constant.
|
||
|
||
**[FACT] The defect class is now four instances deep** — R-573, R-590, R-596, R-598: *a composed
|
||
sentence handed to a renderer as page data*. Nothing structural sees it. A template-parity fixture
|
||
renders the field faithfully; `TestI18nEnglishPages` reads a template, not a struct; the Go-parity
|
||
gate proves the Hungarian is unchanged and says nothing about which language reached the page. **The
|
||
only instruments that find it are a live English page and a handler-level render test**, and this
|
||
release added the second for both surfaces.
|
||
|
||
**[FACT] A method finding worth more than the result.** The first live check of the backup page used
|
||
the `felhom_lang` cookie and got the **Hungarian** page for `en`. That is correct: `langFor` step 2
|
||
says a request carrying a **session** reads the household's saved setting and deliberately ignores
|
||
the visitor cookie. The cookie is the right instrument for the anonymous claim page and the **wrong**
|
||
one for any signed-in page — where `?lang=` is. A session that had run only the cookie probe would
|
||
have concluded R-598 was unfixed and fixed it again. Recorded in
|
||
`audits/i18n-closing-2026-09-21/live/backups-page.md`.
|
||
|
||
**What did NOT get a live walk:** the degraded and absent-drive warnings themselves. Guest 9201 has a
|
||
real backup drive, so it is healthy and renders nothing — by design (E-2 Scenario E) — and producing
|
||
either state would mean un-assigning a live box's backup target. They are covered by render tests
|
||
through the real handler. **The next English walk on a one-drive machine is what actually closes
|
||
that**, and it is the same walk R-516 is waiting for in the other language.
|
||
|
||
## 11. Operator decisions
|
||
|
||
These are rulings, not proposals. Anything specced against a different assumption is wrong.
|