diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c746e4..0e8ce6c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
+## v0.250.0 — Every dashboard page in English; the language switch shown to every household (2026-09-17, R-556 release C)
+
+**MinAgent: 0.131.0** (unchanged). **No hub release needed.** **A Hungarian household sees one new thing:
+a „Magyar / English" switch in the sidebar footer.** Everything else in Hungarian is byte-identical.
+
+- **Converted (608 markers):** `storage` (162), `storage_network` (70), `storage_init` (42),
+ `storage_attach` (27), `sharing` (74), `login` (9), `claim` (21), `launcher_shared` (4),
+ `launcher_share_password` (6), `catchall` (4), `debug` (189). All 31 slice-1 templates are done; the
+ English gap is 0.
+- **Parity:** 24 fixture states captured from the UNCONVERTED templates (`0555091`). Those cases had
+ invented page titles; they now carry the handlers' real titles and the 12 affected fixtures were
+ re-captured from `0555091` (diff: the `
` line, and the nav highlight on the two drive wizards).
+- **Sign-in, claim, guest share (both), catch-all follow the language** through `executeTemplateLang`
+ (no session CSRF, no escrow reminder). `renderLogin` takes the request. Tests:
+ `TestDirectRenderHandlersFollowLanguage` (the real routes, en and hu), `TestI18nDirectRenderPagesHaveNoAdminChrome`
+ (escrow reminder genuinely due; absent from the page data of all six pages and from the guest page in both languages).
+- **The switch for everyone:** the hide condition in `addLanguageData` is deleted. 89 layout fixtures
+ re-captured; each differs from its predecessor by exactly one inserted switch form placed after the
+ version span (89 of 89 byte-for-byte after removing the form); the 17 standalone fixtures are unchanged.
+ `TestLanguageSwitch_EndToEnd` now asserts a Hungarian household with no `?lang=` sees the form.
+- **Titles:** `TitleKey` for storage, network storage, both drive wizards, sharing.
+ `TestHandlerTitleKeysMatchHungarianTitle` pins every handler's Hungarian title literal to its hu.json value.
+- By-eye review found six ASCII-only Hungarian JS fragments the extractor missed („, majd a(z)",
+ „FIGYELEM:", „jelenlegi:", „mp", „p", „ db"); converted by hand. No test would have caught them on the
+ English page (row filed).
+- `HU_FORMAL_CEILING` 12 → 16 (R-516) — and the stem list sees only 4 of at least 22 formal forms on these pages (R-516 extended).
+- `r400_debug_routes_test.go` and `debug_route_gate.py` still read `debug.html` raw: correct, they match
+ addresses and identifiers, and the bundles hold no `/api/` string (measured 0).
+
+**Red-proofs** (`felhom.eu/documentation/audits/i18n-slice1-2026-09-17/C/redproofs.txt`): parity (one letter of
+`sharing.megosztott_mappak` → 6 sharing cases), English page (`storage.eleresi_ut` removed → 2 cases),
+marker coverage (a marker in an unrendered branch), title keys (reworded hu value; deleted handler line),
+direct routes (all five back on `s.tmpl` → five English rows fail), no admin chrome (`addEscrowBanner`
+added to `executeTemplateLang` → fails per page), the switch (hide condition restored → end-to-end test
+and 89 parity cases fail).
+
## v0.249.0 — English for the backup pages, Hungarian byte-identical (2026-09-17, R-556 release B)
**MinAgent: 0.131.0** (unchanged). **No hub release needed.** Nothing changes for a Hungarian household.
diff --git a/CONTEXT.md b/CONTEXT.md
index 39fefa5..4f5a6eb 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -9,6 +9,8 @@
Last updated: 2026-09-17 (v0.247.0 — i18n spike: the dashboard can speak English, Hungarian byte-identical)
+> **2026-09-17 night — v0.250.0 (R-556 release C, slice 1 CLOSED).** All templates converted; the language switch is on every dashboard page (decision 6 superseded); login/claim/guest/catch-all through `executeTemplateLang`; Go-side messages and three app-name titles stay Hungarian (slice 2).
+
> **2026-09-17 evening — v0.248.0 + v0.249.0 (R-556 releases A, B).** Apps/settings and backup pages
> converted; `TestI18nParityCoversEveryMarker` (every marker rendered by a case); fixtures only written
> when missing (`-update-i18n-golden`, `-i18n-golden-only`); `executeTemplateLang` for pages outside the
diff --git a/controller/README.md b/controller/README.md
index 669262d..7b2e014 100644
--- a/controller/README.md
+++ b/controller/README.md
@@ -46,7 +46,7 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated
- [App-to-App Integrations](#15-app-to-app-integrations)
- [Network File Sharing — SMB](#16-network-file-sharing--smb-megosztás-v01440-r-7-slice-1)
- [Async-job feedback](#17-async-job-feedback-v0147x-feedback-slice-1)
- - [Dashboard language (i18n)](#18-dashboard-language-i18n-v02470-spike)
+ - [Dashboard language (i18n)](#18-dashboard-language-i18n-v02470v02500)
- [Repository Layout](#repository-layout)
- [Configuration](#configuration)
- [REST API](#rest-api)
@@ -3614,17 +3614,16 @@ without reaching `Images()`.
---
-### 18. Dashboard language (i18n) (v0.247.0, spike)
+### 18. Dashboard language (i18n) (v0.247.0–v0.250.0)
Design: `felhom.eu/documentation/architecture/10-localisation.md`. Inventory:
`felhom.eu/documentation/audits/I18N-INVENTORY-2026-09-17.md`.
-**What a household sees:** nothing new. Hungarian is the default and the only language shown unless
-the language is switched. English covers the **launcher**, the **backups overview** (`/backups`), the
-**app page** (`/apps/`), the shared **layout**, and since v0.248.0 the **dashboard, apps list,
-logs, monitoring, import, export, deploy/settings-of-an-app and the three settings pages** — other
-pages keep their Hungarian body inside an English menu. Server-built messages and catalog copy stay
-Hungarian (slices 2 and 5).
+**What a household sees:** Hungarian by default, and since v0.250.0 a **Magyar / English** switch in the
+sidebar footer of every dashboard page. Every template is converted (v0.247.0 three pages, v0.248.0
+apps and settings, v0.249.0 backups, v0.250.0 storage, sharing, sign-in, claim, guest share, catch-all,
+debug). Server-built messages (flash lines, errors, labels built in Go), three page titles built in Go
+around an app name, and catalog copy stay Hungarian (slices 2 and 5).
- **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
@@ -3637,15 +3636,18 @@ Hungarian (slices 2 and 5).
- **Fallback:** a key missing from English shows the Hungarian text and is counted (startup INFO line
`i18n: en template set shows Hungarian for N markers`); never a key, never a blank.
- **Language of a request:** `?lang=hu|en` (testing override, never persisted) → `settings.json`
- `language` → `hu`. `executeTemplate` renders from that language's set. Pages rendered outside
- `executeTemplate` (login, claim, recovery, the guest share pages) stay Hungarian.
+ `language` → `hu`. `executeTemplate` renders from that language's set. Pages outside the dashboard
+ chrome (login, claim, recovery, both guest share pages, the catch-all) render through
+ `executeTemplateLang`: the household language, but NO session CSRF fields and NO escrow reminder
+ (`TestI18nDirectRenderPagesHaveNoAdminChrome`, `TestDirectRenderHandlersFollowLanguage`).
- **The switch:** `POST /settings/language` (`lang`, `back`; session CSRF like every form) saves the
- language and redirects to `back` without its query. Shown in the sidebar footer **only when the page
- is not Hungarian or the request carries `?lang=`** — deliberate for this release (three pages are
- English); the way back is always on screen.
-- **Go-side copy:** a handler names its title key (`data["TitleKey"]`); non-Hungarian sets override
- the copy-producing template funcs (`stateLabel`, `timeAgo`, `timeAgoStr`, `nextRunLabel`,
- `statusText`) from the bundle. The Hungarian funcs are untouched.
+ language and redirects to `back` without its query. Shown in the sidebar footer of **every**
+ dashboard page, in both languages, since v0.250.0 (until then only on non-Hungarian pages or with
+ `?lang=`, while English was partial).
+- **Go-side copy:** a handler names its title key (`data["TitleKey"]`, pinned equal to its Hungarian
+ literal by `TestHandlerTitleKeysMatchHungarianTitle`); non-Hungarian sets override the copy-producing
+ template funcs (`stateLabel`, `timeAgo`, `timeAgoStr`, `nextRunLabel`, `statusText`, `infraMeta`) from
+ the bundle. The Hungarian funcs are untouched.
- **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
diff --git a/controller/internal/web/i18n_web.go b/controller/internal/web/i18n_web.go
index 7e21699..1b956c8 100644
--- a/controller/internal/web/i18n_web.go
+++ b/controller/internal/web/i18n_web.go
@@ -20,18 +20,13 @@ import (
// addLanguageData puts the request's language and the switch state into a page's data.
//
-// The switch is shown only when the page is NOT in Hungarian, or the request carries ?lang=. That is a
-// deliberate property of this release, not an oversight: English covers three pages, so offering it to
-// every household would put a half-English dashboard one click away. A Hungarian household therefore
-// sees nothing new; the way back from English is always on screen. Slice 1 of the plan makes the
-// switch visible to everyone once the remaining pages are converted.
+// The switch is on every dashboard page, in every language (v0.250.0, slice 1 release C). Until then it
+// was hidden from Hungarian pages without ?lang=, because English covered only part of the dashboard;
+// every template is converted now, so the offer no longer leads to a half-English page. Pinned by
+// TestLanguageSwitch_EndToEnd (a Hungarian household with no ?lang= sees the form).
func (s *Server) addLanguageData(data map[string]interface{}, r *http.Request, lang string) {
data["Lang"] = lang
- show := lang != i18n.Default
- if r != nil && r.URL.Query().Get("lang") != "" {
- show = true
- }
- if show && r != nil {
+ if r != nil {
data["LangSwitch"] = true
data["LangSwitchBack"] = r.URL.Path
}
diff --git a/controller/internal/web/i18n_wiring_test.go b/controller/internal/web/i18n_wiring_test.go
index d2e5c50..9e41357 100644
--- a/controller/internal/web/i18n_wiring_test.go
+++ b/controller/internal/web/i18n_wiring_test.go
@@ -206,9 +206,11 @@ func TestLanguageSwitch_EndToEnd(t *testing.T) {
return rec.Body.String()
}
+ // v0.250.0: the switch is offered to everyone — a Hungarian household with no ?lang= sees the form.
+ // RED-PROOF: put back the „show only when not Hungarian or ?lang=" condition in addLanguageData.
hu := body("/launcher")
- if strings.Contains(hu, `action="/settings/language"`) {
- t.Error("a Hungarian household must not see the language switch in this release")
+ if !strings.Contains(hu, `action="/settings/language"`) || !strings.Contains(hu, `name="lang" value="en"`) {
+ t.Error("a Hungarian household with no ?lang= must see the language switch (form and the English choice)")
}
if !strings.Contains(hu, "
Indítópult
") || !strings.Contains(hu, `Indítópult") {
- t.Error("back on Hungarian: the page must be Hungarian with no switch")
+ if back := body("/launcher"); !strings.Contains(back, `action="/settings/language"`) || !strings.Contains(back, "
Indítópult
") {
+ t.Error("back on Hungarian: the page must be Hungarian, with the switch still offered")
}
}
diff --git a/controller/internal/web/testdata/i18n_parity/app_export.html b/controller/internal/web/testdata/i18n_parity/app_export.html
index 4cd66eb..c5aabc7 100644
--- a/controller/internal/web/testdata/i18n_parity/app_export.html
+++ b/controller/internal/web/testdata/i18n_parity/app_export.html
@@ -147,7 +147,7 @@