9167cf53af
gates / gates (push) Successful in 23s
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
150 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|