v0.247.0: i18n spike — the dashboard can speak English, Hungarian byte-identical
gates / gates (push) Successful in 19s

Message bundles (internal/i18n) expanded into templates before parsing, one
template set per language. Launcher, /backups, /apps/<slug> and the layout
converted; household language setting, POST /settings/language, ?lang= override,
report field. Parity test against fixtures captured from unconverted templates;
copy gates read templates expanded; new i18n_missing_gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-17 14:50:57 +02:00
parent 89dd3e94b1
commit 612c417024
49 changed files with 11734 additions and 248 deletions
+41
View File
@@ -46,6 +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)
- [Repository Layout](#repository-layout)
- [Configuration](#configuration)
- [REST API](#rest-api)
@@ -3613,6 +3614,46 @@ without reaching `Images()`.
---
### 18. Dashboard language (i18n) (v0.247.0, spike)
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/<slug>`) and the shared **layout** (menu, banners, the layout's modals) — other
pages keep their Hungarian body inside an English menu.
- **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
markup (`<strong>`), which are the message's parameters. English plurals are `key.one` / `key.other`
(`Bundle.Plural`); Hungarian has one form.
- **Templates:** a converted template carries `{{T "key"}}` markers. `loadTemplates` parses **one
template set per language**, expanding the markers textually **before** `html/template` parses the
file — so the Hungarian set is parsed from byte-identical source in identical escaping contexts.
An undefined key is left in place and the set fails to load ("function T not defined").
- **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.
- **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.
- **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
exist, no orphans, the English gap and the formal „ön" count only shrink (ratchets), no pleading
English. The emoji, native-confirm, secret-in-markup and retrieval-promise gates read templates
**expanded** through `scripts/i18n_bundle.py`; mojibake scans the bundles.
- **Release gate:** `TestI18nParity` compares the Hungarian render of 14 fixture states with HTML
captured from the unconverted templates (`internal/web/testdata/i18n_parity/`). Fixtures are never
regenerated to make a conversion pass.
## Repository Layout
```