fix(i18n): seventeen strings had drifted into English only

Reported from a photograph of a French screen: "Spot lifetime" and "Chase new
grids" still in English in the DX Cluster settings. Diffing the two dictionaries
turned up fifteen more — including the ENTIRE update panel, which is why an
operator who reads French had nothing in French to react to when a new version
appeared.

All seventeen translated. Both dictionaries now hold 2685 keys with no
difference either way.

Every user-visible string ships in both languages: that was a rule, and until
now only a rule. A test parses the two dictionary literals and compares their
keys, so the next one is caught by `go test` instead of by an operator
photographing their own screen. Verified it fails when a key is removed.

A key missing from one side is not a blank — it falls back to the key itself,
so the interface reads "clu.spotTtl" where a label belongs, or keeps the English
text, which looks deliberate and is not.
This commit is contained in:
2026-08-16 18:25:26 +02:00
parent 14ac73028e
commit 5293bb18c5
3 changed files with 66 additions and 4 deletions
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"os"
"regexp"
"strings"
"testing"
)
// Every user-visible string ships in BOTH languages. That is a project rule,
// and until now it was only a rule: seventeen keys had drifted into English
// only, among them the whole update panel and two DX-cluster settings, found by
// a French operator photographing his own screen.
//
// A key present in one dictionary and not the other falls back to the key
// itself, so the interface shows "clu.spotTtl" where a label belongs — or, as
// here, the English text, which reads as deliberate and is not.
func TestEveryStringIsInBothLanguages(t *testing.T) {
src, err := os.ReadFile("frontend/src/lib/i18n.tsx")
if err != nil {
t.Fatalf("read i18n.tsx: %v", err)
}
en := dictKeys(t, string(src), "const en: Dict = {")
fr := dictKeys(t, string(src), "const fr: Dict = {")
if len(en) == 0 || len(fr) == 0 {
t.Fatal("one of the dictionaries came back empty — this test has stopped checking anything")
}
for k := range en {
if !fr[k] {
t.Errorf("%q has no French — a French operator sees the English string, or the key itself", k)
}
}
for k := range fr {
if !en[k] {
t.Errorf("%q exists only in French — an English operator sees the key", k)
}
}
}
// dictKeys collects the keys of one dictionary literal: from its opening line
// to the closing brace in column 0.
func dictKeys(t *testing.T, src, opening string) map[string]bool {
t.Helper()
i := strings.Index(src, opening)
if i < 0 {
t.Fatalf("%q not found — the dictionaries have been renamed and this test needs updating", opening)
}
body := src[i+len(opening):]
if j := strings.Index(body, "\n};"); j >= 0 {
body = body[:j]
}
keyRe := regexp.MustCompile(`'([a-zA-Z0-9_.]+)'\s*:`)
out := map[string]bool{}
for _, m := range keyRe.FindAllStringSubmatch(body, -1) {
out[m[1]] = true
}
return out
}