Files
felhom.eu/hub/internal/i18n/i18n_test.go
T
admin 9167cf53af
gates / gates (push) Successful in 23s
hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
The hub has written every customer e-mail in Hungarian whatever the box was set
to. The box has published its language since controller v0.247.0; nothing read
it. Now it does.

Nothing an operator reads changes. The Hungarian mails are byte-identical, and
that is a diff rather than a reading: 56 goldens per language captured from
v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after
every sentence was routed through the new bundle.

- internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0.
- customerMessages/severityLabels are DERIVED from the bundle, so a sentence is
  written in one place and all 40+ tests that read those maps still work.
- Language order: last reported -> created-with -> hu. reports.language defaults
  to EMPTY, never hu: "never told us" is not "chose Hungarian".
- message_customer on POST /api/v1/event, additive and optional forever, for the
  sentences the box composes and the hub cannot translate.
- The bind page is per-language, and its `expired` state stays Hungarian: it is
  the state an unknown token lands in, so rendering a real English customer's
  token in English would make the LANGUAGE answer what the TEXT refuses to.

Two defects found inside the release:
- R-581: the newest report was picked by received_at, which has SECOND
  granularity, so same-second reports tied and the winner was arbitrary. Ordered
  by the autoincrement id now. GetCustomers() still has the shape - row open.
- R-582: the English copy-guard stems, ported word for word from Hungarian,
  convicted 141 honest sentences. The English claim is a phrase with a modal.

R-555 closed: the language allowlist entry is out of wire_contract_gate.py.
hub_copy_gate.py follows the sentences into the bundle - without that it would
have scanned four files that no longer hold any customer text and reported
success. Three new decoys incl. an innocent control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 16:20:11 +02:00

150 lines
5.4 KiB
Go

package i18n
import (
"regexp"
"strings"
"testing"
)
// EnMissingCeiling is the number of Hungarian keys allowed to be missing from English. It is zero
// and it stays zero: a fallback that renders Hungarian into an English household's mail is the
// failure this bundle exists to prevent, so it must never be reachable in a shipped build.
const EnMissingCeiling = 0
func TestEveryLanguageCarriesEveryKey(t *testing.T) {
b := Shared()
for _, lang := range Supported {
missing := b.MissingKeys(lang)
if lang == Default && len(missing) != 0 {
t.Fatalf("%s is the source and cannot be missing keys: %v", lang, missing)
}
if len(missing) > EnMissingCeiling {
t.Errorf("%s is missing %d key(s) (ceiling %d): %v",
lang, len(missing), EnMissingCeiling, missing)
}
}
}
// verbRe matches a Go format verb, including an explicit argument index (%[2]s).
var verbRe = regexp.MustCompile(`%(\[\d+\])?[-+# 0]*\d*(\.\d+)?[a-zA-Z%]`)
// TestFormatVerbsMatchAcrossLanguages is the test that stops a translation corrupting a mail.
//
// Msgf formats the TRANSLATED value with the arguments the Hungarian call site passes. A translation
// that drops a `%s` silently loses a customer's server name; one that adds a `%s` renders
// `%!s(MISSING)` into a real e-mail. Neither is visible to a reviewer reading the JSON, and neither
// would fail any other test here — the mail still sends.
//
// Explicit argument indexes (`%[2]s`) are how English reorders what Hungarian does not, so the check
// is on the MULTISET of verbs, not on their order.
func TestFormatVerbsMatchAcrossLanguages(t *testing.T) {
b := Shared()
for _, key := range b.Keys(Default) {
huVerbs := verbCounts(b.Msg(Default, key))
for _, lang := range Supported {
if lang == Default || !b.Has(lang, key) {
continue
}
got := verbCounts(b.Msg(lang, key))
if len(got) != len(huVerbs) {
t.Errorf("%s/%s has %d format verb(s), hu has %d — the mail would render wrong",
lang, key, len(got), len(huVerbs))
continue
}
for verb, n := range huVerbs {
if got[verb] != n {
t.Errorf("%s/%s uses %q %d time(s), hu uses it %d time(s)",
lang, key, verb, got[verb], n)
}
}
}
}
}
// verbCounts counts each format verb, ignoring the escaped %% which formats nothing.
func verbCounts(s string) map[string]int {
out := map[string]int{}
for _, m := range verbRe.FindAllString(s, -1) {
if m == "%%" {
continue
}
// An indexed verb counts as its bare form: %[2]s and %s consume the same argument type.
if i := strings.Index(m, "]"); i >= 0 {
m = "%" + m[i+1:]
}
out[m]++
}
return out
}
func TestNormalizeAndIsSupported(t *testing.T) {
for in, want := range map[string]string{
"hu": "hu", "en": "en", "EN": "en", " en ": "en",
"": "hu", "de": "hu", "en-GB": "hu", "garbage": "hu",
} {
if got := Normalize(in); got != want {
t.Errorf("Normalize(%q) = %q, want %q", in, got, want)
}
}
// IsSupported REFUSES where Normalize coerces — that difference is the whole reason both exist.
for _, bad := range []string{"", "EN", " en ", "de", "en-GB"} {
if IsSupported(bad) {
t.Errorf("IsSupported(%q) is true — a bad input would be stored as a real language", bad)
}
}
for _, good := range []string{"hu", "en"} {
if !IsSupported(good) {
t.Errorf("IsSupported(%q) is false", good)
}
}
}
// A key that exists nowhere must be LOUD, never blank: an empty string in a mail body is a hole
// nobody notices.
func TestUnknownKeyIsVisible(t *testing.T) {
got := Shared().Msg("en", "mail.event.no_such_thing_exists")
if got == "" {
t.Fatal("an unknown key rendered as an empty string — the mail would have a silent hole")
}
if !strings.Contains(got, "no_such_thing_exists") {
t.Errorf("an unknown key rendered as %q — it does not name the key", got)
}
}
// An English key falls back to Hungarian rather than to nothing. Proven with a key deliberately
// absent from en.json would be circular (there are none), so it is proven on the mechanism: a
// language that carries no file at all.
func TestFallbackIsHungarianNotBlank(t *testing.T) {
b := Shared()
if got := b.Msg("de", "mail.event.backup_completed"); got != b.Msg(Default, "mail.event.backup_completed") {
t.Errorf("an unsupported language did not fall back to Hungarian: %q", got)
}
}
// The English bundle must not be a copy of the Hungarian one. A translation pass that silently
// copied the source would pass every other test in this file.
func TestEnglishIsActuallyTranslated(t *testing.T) {
// The keys that are IDENTICAL IN BOTH LANGUAGES ON PURPOSE, each with the reason. An allowlist
// rather than a count: a count lets the next untranslated key hide inside the budget, and the
// question "why is this one the same?" then has no answer written down anywhere.
sameOnPurpose := map[string]string{
"mail.customer.subject": `"[Felhom] %s: %s" — a format with no words in it; both halves are themselves translated`,
"bind.placeholder.pairing": `"ABC-234" — a sample of a code the box prints, not prose`,
}
b := Shared()
for _, key := range b.Keys(Default) {
hu, en := b.Msg(Default, key), b.Msg("en", key)
if hu != en {
if _, listed := sameOnPurpose[key]; listed {
t.Errorf("%s is allowlisted as identical but the two languages now differ — "+
"remove it from sameOnPurpose", key)
}
continue
}
if _, ok := sameOnPurpose[key]; !ok {
t.Errorf("%s is identical in hu and en — untranslated text would ship: %q", key, hu)
}
}
}