Files
felhom-controller/controller/internal/web/i18n_parity_test.go
T
admin 48f3336956
gates / gates (push) Successful in 23s
v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
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
2026-09-18 14:19:31 +02:00

553 lines
23 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"bytes"
"flag"
"fmt"
"html/template"
"io/fs"
"net/http/httptest"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// ── i18n (v0.247.0) — THE RELEASE GATE: Hungarian renders byte-for-byte as it did ──────────────────
//
// The fixtures under testdata/i18n_parity/ were captured from the templates BEFORE any of them carried
// a {{T}} marker (`go test -run TestI18nParity -update-i18n-golden` on the unconverted tree, commit
// recorded in felhom-controller/REPORT.md). Every conversion since must reproduce them exactly.
//
// Normalisation, and it is the ONLY one: relative ages („3 napja") are rewritten to „# napja", because
// they are computed from the wall clock and the fixtures carry fixed dates. Nothing else is touched.
//
// RED BEFORE THE FIXTURE EXISTS: a missing fixture file fails the test; it is never silently written.
// RED-PROOF (recorded in REPORT.md): change one byte of one launcher value in locales/hu.json and
// TestI18nParity fails naming the page and the first differing line.
var updateI18nGolden = flag.Bool("update-i18n-golden", false, "write MISSING testdata/i18n_parity fixtures from the current templates (an existing fixture is never overwritten)")
var i18nDumpDir = flag.String("i18n-dump-dir", "", "TestI18nEnglishPages: also write every case's hu and en render to this directory (evidence, per-page review)")
var i18nGoldenOnly = flag.String("i18n-golden-only", "", "with -update-i18n-golden: only cases whose name matches this regexp")
var relativeAgeRe = regexp.MustCompile(`\d+ (napja|órája|perce)`)
type i18nCase struct {
name string
tmpl string
data func() map[string]interface{}
}
func i18nFixtureTime() time.Time { return time.Date(2026, 9, 14, 3, 12, 0, 0, time.UTC) }
// i18nTitleKeys mirrors the handlers' data["TitleKey"]: a page whose Go-side title has a key renders it
// translated in English, exactly as addLanguageData does in production.
var i18nTitleKeys = map[string]string{
"Indítópult": "page.title.launcher",
"Biztonsági mentés": "page.title.backups",
"Vezérlőpult": "page.title.dashboard",
"Alkalmazások": "page.title.stacks",
"Rendszermonitor": "page.title.monitoring",
"Beállítások": "page.title.settings",
"Értesítések": "page.title.settings_notifications",
"Biztonság és hozzáférés": "page.title.settings_security",
"Tárhely": "page.title.storage",
"Hálózati tárhely": "page.title.storage_network",
"Új meghajtó inicializálása": "page.title.storage_init",
"Meglévő meghajtó csatolása": "page.title.storage_attach",
"Hálózati megosztás": "page.title.sharing",
}
func i18nLayoutData(page, title string) map[string]interface{} {
d := map[string]interface{}{
"Page": page, "Title": title, "CustomerName": "Teszt Ügyfél", "Domain": "example.hu",
"Version": "0.247.0", "AuthEnabled": true, "DebugMode": false,
"CSRFField": "", "CSRFToken": "tok",
}
if k, ok := i18nTitleKeys[title]; ok {
d["TitleKey"] = k
}
return d
}
func i18nCases() []i18nCase {
base := []i18nCase{
{"launcher_full", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["ClaimLegacyOpen"] = true
d["RecoveryBanner"] = true
d["RecoveryBannerBack"] = "/launcher"
d["RecoveryAbandonDays"] = 12
d["RecoveryAbandonDate"] = "2026-09-29"
d["RecoveryAbandonRetrievalOffered"] = true
d["Apps"] = []LauncherApp{
{Name: "immich", DisplayName: "Immich", Slug: "immich", State: stacks.StateRunning, Subdomain: "fotok"},
{Name: "privatebin", DisplayName: "PrivateBin", Slug: "privatebin", State: stacks.StateUnhealthy, Subdomain: "paste", OpenPath: "/"},
{Name: "kimai", DisplayName: "Kimai", Slug: "kimai", State: stacks.StateStopped, Subdomain: "ido"},
}
d["ShareEnabled"] = true
d["ShareURL"] = "https://felhom.example.hu/s/abc"
d["SharePasswordSet"] = true
d["ShareFlash"] = "A megosztás bekapcsolva."
return d
}},
{"launcher_empty", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryBannerBack"] = "/launcher"
d["RecoveryReminderTier"] = 14
d["EscrowBanner"] = true
d["EscrowBannerBack"] = "/launcher"
return d
}},
{"launcher_nopassword", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryAbandonDays"] = 3
d["RecoveryAbandonDate"] = "2026-09-20"
d["Alerts"] = []Alert{
{ID: "a", Level: "warning", Message: "A rendszer meghajtója 91%-on áll.", Link: "/monitoring", LinkText: "Rendszermonitor"},
{ID: "b", Level: "info", Message: "Frissítés érhető el."},
{ID: "c", Level: "error", Message: "Csak a műszerfalon.", PageOnly: []string{"dashboard"}},
}
d["Apps"] = []LauncherApp{{Name: "gokapi", DisplayName: "Gokapi", Slug: "gokapi", State: stacks.StateStarting, Subdomain: "fajl"}}
d["ShareEnabled"] = true
d["ShareURL"] = "https://felhom.example.hu/s/xyz"
return d
}},
{"launcher_reminder_tiers", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 7
return d
}},
{"launcher_reminder_tier3", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 3
d["AuthEnabled"] = false
return d
}},
{"launcher_reminder_tier0", "launcher", func() map[string]interface{} {
d := i18nLayoutData("launcher", "Indítópult")
d["RecoveryBanner"] = true
d["RecoveryReminderTier"] = 0 // the handler always sets an int; a missing key cannot reach `ge`
return d
}},
{"backups_full", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{
"FlashSuccess": "A mentés elindult.", "FlashError": "", "SingleCopyWarning": "Csak egy másolat létezik.",
"DumpFiles": []string{"a.sql", "b.sql"},
"LastDBDump": map[string]interface{}{"Success": true, "Results": []int{1, 2, 3}},
}
d["BackupTarget"] = map[string]interface{}{
"Message": "A rendszermentésnek nincs helye.", "OfferPath": "/mnt/hdd2",
"OfferText": "Kijelölheted a második meghajtót.", "OfferLabel": "HDD 2 (1 TB)",
}
d["SystemInfo"] = map[string]interface{}{"DiskPercent": 42.0, "DiskUsedGB": 42.5, "DiskTotalGB": 100.0}
d["StorageBars"] = []map[string]interface{}{
{"Label": "Adatok", "Disconnected": false, "Percent": 71.3, "UsedGB": 713.0, "TotalGB": 1000.0},
{"Label": "Régi lemez", "Disconnected": true},
}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": true, "Success": true, "StartedAt": at, "SizeUnknown": false,
"SizeBytes": int64(5 << 30), "Target": "PBS (helyi)", "Due": false, "AgeHours": 7,
"HasRestoreTest": true, "RestorePass": true, "RestoreTestedAt": at,
"Tiers": []map[string]interface{}{
{"Label": "Helyi tároló (local)", "NotSetUp": false, "HasSuccess": true, "SuccessAt": at, "SizeKnown": true, "SizeBytes": int64(3 << 30), "Current": true, "FailedAfter": true, "FailedAt": at},
{"Label": "Távoli szerver (PBS)", "NotSetUp": false, "HasSuccess": false, "Current": false},
{"Label": "Harmadik szint", "NotSetUp": true},
},
"Running": true, "Phase": "vzdump", "CanTrigger": true, "Offsite": true,
}
d["BackupWindow"] = "01:30"
d["BackupLegDB"] = "01:30"
d["BackupLegTier2"] = "02:00"
d["BackupLegOffbox"] = "02:30"
d["BackupGateFrom"] = "03:00"
d["BackupGateTo"] = "05:00"
d["DBSectionState"] = "dumps"
d["EscrowBanner"] = true
return d
}},
{"backups_empty", "backups", func() map[string]interface{} {
return i18nLayoutData("backups", "Biztonsági mentés")
}},
{"backups_degraded", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{
"FlashSuccess": "", "FlashError": "A mentés nem sikerült.",
"LastDBDump": map[string]interface{}{"Success": false, "Results": []int{}},
}
d["BackupTarget"] = map[string]interface{}{"Message": "A rendszermentés helye nem elérhető."}
d["GuestBackup"] = map[string]interface{}{"Available": false, "Note": "Az ügynök nem elérhető."}
d["DBSectionState"] = "embedded"
_ = at
return d
}},
{"backups_nobackup_yet", "backups", func() map[string]interface{} {
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{"DumpFiles": []string{}}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": false, "Due": true, "HasRestoreTest": false, "CanTrigger": false,
}
d["DBSectionState"] = "pending"
return d
}},
{"backups_tier_due", "backups", func() map[string]interface{} {
at := i18nFixtureTime()
d := i18nLayoutData("backups", "Biztonsági mentés")
d["Backup"] = map[string]interface{}{"DumpFiles": []string{"a.sql"}}
d["GuestBackup"] = map[string]interface{}{
"Available": true, "HasBackup": true, "Success": false, "StartedAt": at, "SizeUnknown": true,
"Target": "PBS (helyi)", "Due": true, "HasRestoreTest": true, "RestorePass": false, "RestoreTestedAt": at,
"Tiers": []map[string]interface{}{
{"Label": "Helyi tároló (local)", "NotSetUp": false, "HasSuccess": true, "SuccessAt": at, "SizeKnown": false, "Current": false},
},
"Running": false, "CanTrigger": true, "Offsite": false,
}
d["DBSectionState"] = "dumps"
return d
}},
{"app_info_installable", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: false, State: stacks.StateNotDeployed}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök",
Resources: stacks.ResourceHints{MemRequest: "64M"}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
return d
}},
{"app_info_deployed", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: true, State: stacks.StateRunning, HoldReason: "A frissítés megállt: nincs friss mentés."}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök", Description: "Titkosított jegyzetek.",
Resources: stacks.ResourceHints{MemRequest: "64M", NeedsHDD: true, PiCompatible: false, HungarianUI: true},
AppInfo: stacks.AppInfo{Tagline: "Titkosított szövegmegosztás.", UseCases: []string{"Jelszó átadása"}, FirstSteps: []string{"Nyisd meg"},
Prerequisites: []string{"Semmi"}, DefaultCreds: "nincs", DocsURL: "https://privatebin.info"}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = true
d["EffectiveSubdomain"] = "paste"
d["MissingStorageLabel"] = "HDD 1"
d["DataPathCards"] = []DataPathCard{{Label: "Feltöltött fájlok", Link: "/files/x", Consequence: "Mentve minden éjjel.", IsImport: true, FreeSpace: "12 GB szabad"}}
d["MigrateTargets"] = []map[string]interface{}{{"Path": "/mnt/hdd2", "Label": "HDD 2"}}
d["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin", Note: "A jelszót a Felhom állította be."}
d["InitialCredsHasPassword"] = true
return d
}},
{"app_info_available", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: false, State: stacks.StateNotDeployed}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin", Category: "Hasznos eszközök", Description: "Titkosított jegyzetek.",
Lifecycle: "abandoned", Resources: stacks.ResourceHints{MemRequest: "64M", PiCompatible: true}}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = false
return d
}},
{"app_info_operator_creds", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "Fájlkezelő app")
st := stacks.Stack{Name: "filebrowser", Deployed: true, State: stacks.StateStopped, Orphaned: true, Updating: false, UpdateError: "A frissítés sikertelen."}
st.Meta = stacks.Metadata{DisplayName: "Fájlkezelő app", Slug: "filebrowser"}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
d["HasAppInfo"] = true
d["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin"}
d["InitialCredsOperatorSet"] = true
return d
}},
{"app_info_updating", "app_info", func() map[string]interface{} {
d := i18nLayoutData("stacks", "PrivateBin")
st := stacks.Stack{Name: "privatebin", Deployed: true, State: stacks.StateDeploying, Updating: true, UpdatePhase: "pull", UpdatePhaseLabel: "Letöltés folyamatban…"}
st.Meta = stacks.Metadata{DisplayName: "PrivateBin", Slug: "privatebin"}
d["Stack"] = st
d["Meta"] = st.Meta
d["AppInfo"] = st.Meta.AppInfo
return d
}},
}
// Slice 1 (R-556): one file of cases per release, each captured from its unconverted templates.
base = append(base, i18nCasesA()...)
base = append(base, i18nCasesB()...)
base = append(base, i18nCasesC()...)
return base
}
func renderI18nCase(t *testing.T, s *Server, lang string, c i18nCase) string {
t.Helper()
var buf bytes.Buffer
data := c.data()
r := httptest.NewRequest("GET", "/i18n-fixture", nil)
// TWO production paths, and the fixture has to be rendered through the RIGHT one or it is a
// picture of something no visitor sees (v0.254.0, R-557 release C).
//
// * a dashboard page goes through executeTemplate → addLanguageData: session CSRF, and its
// language globe posts to the HOUSEHOLD switch;
// * the pages outside the dashboard chrome go through executeTemplateLang: no CSRF, and their
// globe posts to /lang, the visitor's own display cookie.
//
// Rendering a shell through addLanguageData would bake a form the real page never serves — which
// is exactly what the first attempt at this release did, and what the diff caught.
if i18nDirectTemplates[c.tmpl] {
data["Lang"] = lang
addLangOptions(data, lang, langCookiePath, "/i18n-fixture", "")
} else {
s.addLanguageData(data, r, lang)
}
if err := s.templatesFor(lang).ExecuteTemplate(&buf, c.tmpl, data); err != nil {
t.Fatalf("%s [%s]: render: %v", c.name, lang, err)
}
return relativeAgeRe.ReplaceAllString(buf.String(), "# $1")
}
// i18nDirectTemplates are the templates rendered by executeTemplateLang — the pages a request with no
// household session meets. Kept beside the harness that has to branch on it; i18nDirectPages (in
// i18n_wiring_test.go) names the same set with one case and an English probe each.
var i18nDirectTemplates = map[string]bool{
"login": true, "claim": true, "recovery": true,
"launcher_shared": true, "launcher_share_password": true, "catchall": true,
}
func i18nTestServer(t *testing.T) *Server {
s := testServer(t)
s.loadTemplates()
return s
}
// TestI18nParity — the Hungarian pages are byte-identical to the fixtures captured before conversion.
func TestI18nParity(t *testing.T) {
s := i18nTestServer(t)
dir := filepath.Join("testdata", "i18n_parity")
for _, c := range i18nCases() {
got := renderI18nCase(t, s, "hu", c)
p := filepath.Join(dir, c.name+".html")
if *updateI18nGolden {
if *i18nGoldenOnly != "" && !regexp.MustCompile(*i18nGoldenOnly).MatchString(c.name) {
continue
}
if _, err := os.Stat(p); err == nil {
t.Logf("%s: fixture exists — NOT rewritten (fixtures are captured once, from the unconverted template)", c.name)
continue
}
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(got), 0o644); err != nil {
t.Fatal(err)
}
t.Logf("%s: fixture WRITTEN", c.name)
continue
}
want, err := os.ReadFile(p)
if err != nil {
t.Fatalf("%s: no parity fixture (%v) — fixtures are captured from UNCONVERTED templates, never regenerated to make a conversion pass", c.name, err)
}
if got != string(want) {
gl, wl := strings.Split(got, "\n"), strings.Split(string(want), "\n")
for i := 0; i < len(gl) || i < len(wl); i++ {
var g, w string
if i < len(gl) {
g = gl[i]
}
if i < len(wl) {
w = wl[i]
}
if g != w {
t.Errorf("%s: Hungarian page CHANGED at line %d\n fixture: %q\n now: %q", c.name, i+1, w, g)
break
}
}
}
}
}
// huLetter reports a Hungarian accented letter. ASCII source: the letters are escapes.
func huLetter(s string) bool {
return strings.ContainsAny(s, "áéíóöőúüűÁÉÍÓÖŐÚÜŰ")
}
// fixtureStrings collects every string the fixture DATA carries (catalog copy, Go-side messages the
// handler would build). Those are not template copy: they are slices 2 and 5 of the plan, and the
// English page check subtracts them rather than pretending the template could translate them.
func fixtureStrings(v reflect.Value, out map[string]bool) {
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
if !v.IsNil() {
fixtureStrings(v.Elem(), out)
}
case reflect.String:
if v.String() != "" {
out[v.String()] = true
}
case reflect.Map:
for _, k := range v.MapKeys() {
fixtureStrings(v.MapIndex(k), out)
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
fixtureStrings(v.Index(i), out)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).IsExported() {
fixtureStrings(v.Field(i), out)
}
}
}
}
var i18nKeyLikeRe = regexp.MustCompile(`\b[a-z][a-z_]*\.[a-z0-9_.]+\b`)
var emptyElemRe = regexp.MustCompile(`<(h[1-6]|button|label|li|strong|option|p)(\s[^>]*)?>\s*</(h[1-6]|button|label|li|strong|option|p)>`)
// TestI18nEnglishPages — the three pages in English: no Hungarian letter from the TEMPLATE copy, no
// raw key, no marker, and no element that is empty in English but not in Hungarian.
func TestI18nEnglishPages(t *testing.T) {
s := i18nTestServer(t)
// Controls for the letter detector, so a zero below means something.
if !huLetter("Indítópult") {
t.Fatal("control: the detector does not flag a Hungarian word")
}
if huLetter("Backup completed at 03:00, next run tomorrow.") {
t.Fatal("control: the detector flags plain English")
}
for _, c := range i18nCases() {
hu := renderI18nCase(t, s, "hu", c)
en := renderI18nCase(t, s, "en", c)
if *i18nDumpDir != "" {
_ = os.MkdirAll(*i18nDumpDir, 0o755)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".hu.html"), []byte(hu), 0o644)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".en.html"), []byte(en), 0o644)
}
if !huLetter(hu) {
t.Fatalf("%s: positive control failed — the Hungarian render carries no Hungarian letter", c.name)
}
if strings.Contains(en, `{{T `) {
t.Errorf("%s: an unexpanded marker reached the English page", c.name)
}
for _, m := range i18nKeyLikeRe.FindAllString(en, -1) {
if s.i18n.Has(i18n.Default, m) {
t.Errorf("%s: a raw bundle key reached the English page: %q", c.name, m)
break
}
}
if a, b := len(emptyElemRe.FindAllString(hu, -1)), len(emptyElemRe.FindAllString(en, -1)); b > a {
t.Errorf("%s: English has %d empty elements, Hungarian %d — a translation rendered blank", c.name, b, a)
}
data := map[string]bool{}
fixtureStrings(reflect.ValueOf(c.data()), data)
stripped := en
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) })
for _, k := range keys {
// NARROWED (slice 1): only a data string long enough to be unmistakable is masked — two or
// more words, or 12+ characters. A one-word Hungarian value („Indítópult") equal to template
// copy is no longer able to hide that copy; fixture data therefore carries no one-word
// Hungarian values.
if len(strings.Fields(k)) < 2 && len([]rune(k)) < 12 {
continue
}
stripped = strings.ReplaceAll(stripped, k, "")
}
// Script/style bodies are included on purpose: inline JS copy is page copy.
for i, line := range strings.Split(stripped, "\n") {
if huLetter(line) {
t.Errorf("%s: Hungarian template copy on the English page, line %d: %q", c.name, i+1, strings.TrimSpace(line))
}
}
}
}
// TestI18nParityCoversEveryMarker — every {{T}} marker OCCURRENCE in every template is rendered by at
// least one parity case. A branch no case reaches carries copy the parity and English tests never saw,
// so a page is not done while one exists.
//
// How: a probe template set is parsed in which each occurrence is expanded to an invisible, unique
// sentinel (U+2063 … U+2063) followed by its Hungarian text; every case renders through it; any sentinel
// absent from every output names the file and the key. The sentinel is literal template text, so it
// survives html/template untouched in every context.
//
// RED-PROOF (REPORT.md): a marker placed in a launcher.html branch no case reaches → this test fails
// naming launcher.html and the key.
func TestI18nParityCoversEveryMarker(t *testing.T) {
s := i18nTestServer(t)
b, err := i18n.Shared()
if err != nil {
t.Fatal(err)
}
names, err := fs.Glob(templateFS, "templates/*.html")
if err != nil {
t.Fatal(err)
}
probe := template.New("").Funcs(s.templateFuncMap())
occ := map[string]string{}
for _, n := range names {
raw, err := templateFS.ReadFile(n)
if err != nil {
t.Fatal(err)
}
base := path.Base(n)
idx := 0
src := i18nMarkerRe.ReplaceAllStringFunc(string(raw), func(m string) string {
key := i18nMarkerRe.FindStringSubmatch(m)[1]
id := fmt.Sprintf("⁣%s#%d⁣", base, idx)
idx++
occ[id] = base + " " + key
text, _, ok := b.Text(i18n.Default, key)
if !ok {
t.Fatalf("%s: key %q not in hu.json", base, key)
}
return id + text
})
tmpl := probe
if base != probe.Name() {
tmpl = probe.New(base)
}
if _, err := tmpl.Parse(src); err != nil {
t.Fatalf("probe parse %s: %v", base, err)
}
}
if len(occ) < 250 {
t.Fatalf("only %d marker occurrences found — the probe is not reading the converted templates", len(occ))
}
var all strings.Builder
for _, c := range i18nCases() {
data := c.data()
s.addLanguageData(data, httptest.NewRequest("GET", "/i18n-fixture", nil), i18n.Default)
if err := probe.ExecuteTemplate(&all, c.tmpl, data); err != nil {
t.Fatalf("%s: probe render: %v", c.name, err)
}
}
out := all.String()
var missing []string
for id, where := range occ {
if !strings.Contains(out, id) {
missing = append(missing, where)
}
}
sort.Strings(missing)
for _, m := range missing {
t.Errorf("marker never rendered by any parity case: %s", m)
}
}