v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
gates / gates (push) Successful in 23s

The notes a background run SAVES — last night's backup line, the last error, the proof
result, the restore outcome — are written in the BOX's language at the moment they are
written. A household that switches sees the previous run's note in the old language until
the next run rewrites it: the operator's §16 option 1, stated rather than hidden.
EndRestoreOp no longer receives a Hungarian literal from anywhere.

The language switch is a globe. Two text links wrapped in the sidebar footer and asked the
reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user
already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is
<details>/<summary> — a menu with no script, drawn inline because the icon sprite lives
only in layout.html and the visitor pages have their own shell.

Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only
felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household
can never inherit a language a previous visitor picked in the same browser. POST /lang is
CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the
language of the page the victim's own browser shows them — and safeBackPath refuses
//evil.example as well as https://, because "starts with /" alone is not the test. §16 taken:
a successful claim carries the cookie into the household's setting.

TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change
shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the
three pages that must not change.

I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. UpdateOffboxStatus holds the
settings write lock while running its callback; boxLang() wants the read lock; sync.RWMutex
is not reentrant. On a real box an off-site run would have hung forever HOLDING the settings
lock. Fixed by resolving the language before the callback, and guarded by a test that names
the file and line in a second instead of hanging for 25 minutes.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 14:19:31 +02:00
parent eb6aa58aa7
commit 48f3336956
142 changed files with 2199 additions and 318 deletions
+24 -4
View File
@@ -3614,7 +3614,7 @@ without reaching `Images()`.
---
### 18. Dashboard language (i18n) (v0.247.0–v0.253.0)
### 18. Dashboard language (i18n) (v0.247.0–v0.254.0)
Design: `felhom.eu/documentation/architecture/10-localisation.md`. Inventory:
`felhom.eu/documentation/audits/I18N-INVENTORY-2026-09-17.md`.
@@ -3624,9 +3624,10 @@ sidebar footer of every dashboard page. Every template is converted (v0.247.0 th
apps and settings, v0.249.0 backups, v0.250.0 storage, sharing, sign-in, claim, guest share, catch-all,
debug). Since **v0.252.0** the sentences the program BUILDS follow the language too — flash lines, page
data, the JSON the page's script reads, the alert banners, the country names, and the four page titles
built around an app name. Since **v0.253.0** every error message carries its key too, so a refusal made deep in a package is
rendered in the household's language by whoever prints it. Still Hungarian: text persisted by a
background run (release C), catalog copy (slice 5), and **everything the hub reads** (see below).
built around an app name. Since **v0.253.0** every error message carries its key too. Since **v0.254.0** the notes a background
run SAVES follow the language as well, and the switch is a **globe** — on the dashboard and on the
sign-in, claim and recovery pages. Still Hungarian: catalog copy (slice 5) and **everything the hub
reads** (see below).
- **Bundles:** `internal/i18n/locales/hu.json` (authoritative, every key) and `en.json`, embedded.
Flat `key → text`; a value may carry template actions (`{{.RecoveryAbandonDate}}`) and inline
@@ -3680,6 +3681,25 @@ background run (release C), catalog copy (slice 5), and **everything the hub rea
- **Plurals (v0.253.0):** a key that carries `.one`/`.other` in a language is a plural key and its FIRST
parameter is the count (`i18n.Bundle.form`). Hungarian has one form at every count. `.one`/`.other`
are RESERVED suffixes — `TestNoOrdinaryKeyEndsInAPluralSuffix`.
- **Saved notes (v0.254.0):** a background run has no request and no reader, so a note it SAVES is
written in the BOX's language at write time — `util.Text(lang, key, …)` via `m.note`/`s.note`
(`m.noteErr`/`s.noteErr` for an error). Consequence, by operator ruling (slice 2 §16 option 1): a
household that switches sees the previous run's note in the old language until the next run.
**TRAP, and it cost a 25-minute hang:** `UpdateOffboxStatus` and its siblings hold the settings WRITE
lock while running their callback, and `boxLang()` wants the READ lock — `sync.RWMutex` is not
reentrant, so a note rendered inside such a callback DEADLOCKS while holding the settings lock.
Resolve the language BEFORE the callback; `TestNoteHelpersAreNotCalledUnderTheSettingsLock` guards it.
- **The switch is a globe (v0.254.0):** `templates/lang_globe.html`, a `<details>`/`<summary>` menu with
no script, drawn inline (the sprite lives only in `layout.html`, so `<use>` would render nothing on
the shells). Language names are shown in their own language and never translated.
- **A visitor's language (v0.254.0):** the sign-in, claim and recovery pages carry the same globe, posting
to `POST /lang`, which sets the display-only `felhom_lang` cookie. `langFor`'s order: `?lang=` →
**household setting when a session exists** → cookie when none → setting → `hu`; a signed-in household
never reads the cookie. `/lang` is CSRF-exempt because its only achievable effect is the language of
the page the victim's own browser shows them — **if it gains a second effect it needs CSRF that day**.
`safeBackPath` refuses a protocol-relative `//host` as well as an absolute URL. A successful CLAIM
carries the cookie into the household setting (§16). **No globe on the guest share pages or the
not-found page** — R-577.
- **Report:** the hub report carries `"language"` (always present). No hub release reads it yet.
- **Tools and gates:** `scripts/i18n_extract.py` converts a template (moves each Hungarian run into
hu.json, leaves a marker); `scripts/i18n_missing_gate.py` (in `controller_gates.py`) checks keys