diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83fbc94..b2a4d14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,45 @@
+## v0.247.0 — the dashboard can speak English: mechanism proven on three pages, Hungarian byte-identical (2026-09-17, i18n spike)
+
+**MinAgent: 0.131.0** (unchanged — nothing here talks to the agent). **No hub release needed:** the
+report gains an additive `language` field the hub stores raw and does not read yet.
+
+**Nothing changes for a household.** Hungarian stays the default and the only language shown; the
+switch appears only on an English page or a request carrying `?lang=`.
+
+- **Mechanism — a flat message bundle, expanded BEFORE the template is parsed.**
+ `internal/i18n/locales/{hu,en}.json` (embedded; hu authoritative). Converted templates carry
+ `{{T "key"}}` markers; `loadTemplates` parses one template set per language after `i18n.Expand` has
+ replaced them, so the Hungarian set is parsed from the same bytes, in the same escaping contexts, as
+ before. Chosen over `golang.org/x/text/message` because the inventory found no gender and only
+ count-plurals (English `key.one`/`key.other`; Hungarian has one form), and over a runtime `T` func
+ because the contextual escaper would change Hungarian bytes (`+`, `'`, `"`) and need per-context
+ handling inside ``)
+var i18nMarkerRe = regexp.MustCompile(`\{\{\s*T\s+"([A-Za-z0-9_.\-]+)"\s*\}\}`)
+
+func templateSources(t *testing.T) map[string]string {
+ t.Helper()
+ names, err := fs.Glob(templateFS, "templates/*.html")
+ if err != nil || len(names) == 0 {
+ t.Fatalf("no templates: %v", err)
+ }
+ out := map[string]string{}
+ for _, n := range names {
+ b, err := templateFS.ReadFile(n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ out[n] = string(b)
+ }
+ return out
+}
+
+// Every key a template or the Go side names exists in Hungarian. (The loader refuses an undefined
+// template key too; this test names the key instead of a parse error, and covers the Go-side keys the
+// loader never sees.)
+func TestBundleKeysUsedExistInHungarian(t *testing.T) {
+ b, err := i18n.Shared()
+ if err != nil {
+ t.Fatal(err)
+ }
+ used := 0
+ for name, src := range templateSources(t) {
+ for _, m := range i18nMarkerRe.FindAllStringSubmatch(src, -1) {
+ used++
+ if !b.Has(i18n.Default, m[1]) {
+ t.Errorf("%s: marker key %q is not in hu.json", name, m[1])
+ }
+ }
+ }
+ if used < 200 {
+ t.Fatalf("only %d markers found — the scan is not reading the converted templates", used)
+ }
+ goKeys := []string{"page.title.launcher", "page.title.backups", "func.time.now", "func.time.minutes_ago",
+ "func.time.hours_ago", "func.time.yesterday", "func.time.days_ago", "func.time.today_at", "func.time.tomorrow_at"}
+ for _, st := range []string{"pending", "restoring", "done", "failed", "skipped"} {
+ goKeys = append(goKeys, "func.restore_status."+st)
+ }
+ for _, st := range allContainerStates() {
+ goKeys = append(goKeys, stateLabelKey(st))
+ }
+ for _, k := range goKeys {
+ if !b.Has(i18n.Default, k) {
+ t.Errorf("Go-side key %q is not in hu.json", k)
+ }
+ }
+}
+
+func allContainerStates() []stacks.ContainerState {
+ return []stacks.ContainerState{stacks.StateRunning, stacks.StateStarting, stacks.StateDeploying, stacks.StateUnhealthy,
+ stacks.StateDegraded, stacks.StateStopped, stacks.StateExited, stacks.StateRestarting, stacks.StateNotDeployed,
+ stacks.StatePaused, stacks.ContainerState("something-new")}
+}
+
+// Expansion is textual, so a translation lands in the source exactly where the Hungarian was. Inside a
+// JS string literal a bare quote or backslash ends or corrupts the string; inside a double-quoted
+// attribute a `"` ends the attribute. Neither is caught by html/template, which sees the expanded text as
+// the author's own source. So: a value may carry a quote character only where the Hungarian carries the
+// same one (the author already made it safe there).
+func TestI18nJSContextValuesAreSafe(t *testing.T) {
+ b, err := i18n.Shared()
+ if err != nil {
+ t.Fatal(err)
+ }
+ checked := 0
+ check := func(where, key string, forbidden string) {
+ hu, _, _ := b.Text(i18n.Default, key)
+ for _, lang := range i18n.Supported {
+ for _, form := range []string{key, key + ".one", key + ".other"} {
+ v, fellBack, ok := b.Text(lang, form)
+ if !ok || fellBack {
+ continue
+ }
+ checked++
+ for _, c := range forbidden {
+ if strings.ContainsRune(v, c) && !strings.ContainsRune(hu, c) {
+ t.Errorf("%s: %s %q contains %q, which breaks its context", where, lang, form, c)
+ }
+ }
+ }
+ }
+ }
+ for name, src := range templateSources(t) {
+ for _, sm := range i18nScriptRe.FindAllStringSubmatch(src, -1) {
+ for _, m := range i18nMarkerRe.FindAllStringSubmatch(sm[1], -1) {
+ check(name+"
- A vezérlőpult még nincs jelszóval védve — a beállító kódot hamarosan e-mailben küldjük.
+ {{T "layout.a_vezerlopult_meg_nincs_jelszoval"}}
{{end}}
@@ -146,28 +146,28 @@
reachable state: a fresh escrow ceremony during the window replaces the package, which
is the exact act that cost both demo boxes their history on 2026-08-04. A countdown
started before this shipped carries no pin and takes the cautious branch. */}}
- A korábbi távoli mentéseidet {{.RecoveryAbandonDays}} nap múlva ({{.RecoveryAbandonDate}}) véglegesen töröljük, a kérésed szerint.
- {{if .RecoveryAbandonRetrievalOffered}}Addig még visszaszerezheted őket a helyreállítási kóddal.{{else}}Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha vissza szeretnéd kapni őket, írj nekünk a törlés előtt.{{end}}
+ {{T "layout.a_korabbi_tavoli_menteseidet_nap"}}
+ {{if .RecoveryAbandonRetrievalOffered}}{{T "layout.addig_meg_visszaszerezheted_oket_a"}}{{else}}{{T "layout.hogy_ezek_meg_visszaszerezhetok_e"}}{{end}}
{{else if ge .RecoveryReminderTier 14}}
- Két hete várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat. Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá.
+ {{T "layout.ket_hete_varnak_rad_a"}}
{{else if ge .RecoveryReminderTier 7}}
- Már egy hete megvannak a korábbi távoli mentéseid, de a helyreállítási kódod nélkül nem tudjuk megnyitni őket.
+ {{T "layout.mar_egy_hete_megvannak_a"}}
{{else if ge .RecoveryReminderTier 3}}
- A korábbi távoli mentéseid megvannak — a megnyitásukhoz a helyreállítási kódod szükséges.
+ {{T "layout.a_korabbi_tavoli_menteseid_megvannak"}}
{{else}}
- A korábbi távoli mentéseid megvannak, de ehhez a géphez a helyreállítási kódod szükséges.
+ {{T "layout.a_korabbi_tavoli_menteseid_megvannak_2"}}
{{end}}
- Megnézem
+ {{T "layout.megnezem"}}
{{if not .RecoveryAbandonDays}}
{{end}}
@@ -183,13 +183,12 @@
- A távoli mentés szünetel, amíg nem hozod létre a helyreállítási kódot.
- Helyreállítási kód létrehozása →
+ {{T "layout.a_tavoli_mentes_szunetel_amig"}}
' +
- 'Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
+ '{{T "layout.ha_ujratelepiti_az_alkalmazast_az"}}' +
'
+ Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,
+ de frissítések és biztonsági javítások már nem érkeznek hozzá.
+
+ Hiányzó tárhely: HDD 1
+ Ennek az alkalmazásnak az adattárolója jelenleg nem elérhető, ezért le van állítva. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.
+
+
+
+
+
+
+
+
+
Titkosított szövegmegosztás.
+
+
+ ~64M RAM
+ Eszközök
+ HDD szükséges
+ Csak x86
+ Magyar felület
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hova tegyem a fájlokat?
+
Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.
Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.
+
+
+ Áthelyezés
+
+
+
+
+
+
+
+
+
+
+
Mire használható?
+
+
Jelszó átadása
+
+
+
+
+
+
+
Első lépések
+
+
Nyisd meg
+
+
+
+
+
+
+
Előfeltételek
+
+
Semmi
+
+
+
+
+
+
+
Kezdeti belépési adatok
+
+
+
+
Felhasználónév
+
admin
+
+
+
+
+
Jelszó
+
+
+ ••••••••••••
+ Megjelenítés
+ Másolás
+
+
+
+
+
+
A jelszót a Felhom állította be.
+
Az első bejelentkezés után azonnal változtasd meg! Ez a kezdeti, automatikusan generált jelszó — ha már megváltoztattad, hagyd figyelmen kívül.
+
+
+
+
+
+
Alapértelmezett belépés
+
nincs
+
Az első bejelentkezés után azonnal változtasd meg!
A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.
+
+
Az ügynök nem elérhető.
+
+
+
+
+
+
+
Mentési időablak
+
A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.
A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.
+
+
+
+
✓
+
Utolsó teljes mentés
+ 2026-09-14 05:12 (# napja)
+
+
+
+
5.0 GB
+
PBS (helyi)
+
+
+
Naprakész
+
Következő mentés
+ # órája — a mentési ablakon belül
+
+
+
+
✓
+
Visszaállítás ellenőrizve
+ 2026-09-14 05:12
+
+
+
+
+
+
+
+
Helyi
+
+
+
+
✓ Utolsó sikeres mentés: 2026-09-14 05:12 (# napja) · 3.0 GB
+ Naprakész
+
+
+
✗ 2026-09-14 05:12 — sikertelen
+
+
+
+
+
+
+
Távoli
+
+
+
+
– Még nincs sikeres mentés Esedékes
+
+
+
+
+
+
+
+
Harmadik
+
+
+ nincs beállítva
+
+
+
+
+
+
+
+
Mentés folyamatban… (fázis: vzdump)
+
+
+
+ Mentés most
+ A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.
+
+
+
+
+
+
+
+
+
+
Mentési időablak
+
A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.
A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.
+
+
+
+
–
+
Utolsó teljes mentés
+
+
+
+
+
–
+
Méret / cél
+
+
+
Esedékes
+
Következő mentés
+
+
+
+
+
–
+
Visszaállítás ellenőrizve
+ Még nem futott
+
+
+
+
+
+
+
+
+
+
+
+
+
Mentési időablak
+
A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.
+
+
+
+ Két hete várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat. Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá.
+
+ Megnézem
+
+
+
+
+
+
+
+
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.
+
+ A vezérlőpult még nincs jelszóval védve — a beállító kódot hamarosan e-mailben küldjük.
+
+
+
+
+
+
+
+
+
+
+
+ A korábbi távoli mentéseidet 12 nap múlva (2026-09-29) véglegesen töröljük, a kérésed szerint.
+ Addig még visszaszerezheted őket a helyreállítási kóddal.
+
+ Megnézem
+
+
+
+
+
+
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.
+
+
+
+
+ A korábbi távoli mentéseidet 3 nap múlva (2026-09-20) véglegesen töröljük, a kérésed szerint.
+ Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha vissza szeretnéd kapni őket, írj nekünk a törlés előtt.
+
+ Megnézem
+
+
+
+
+
+
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.
Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.