diff --git a/internal/web/locale/locale_lazy_test.go b/internal/web/locale/locale_lazy_test.go index ff68087..a390a60 100644 --- a/internal/web/locale/locale_lazy_test.go +++ b/internal/web/locale/locale_lazy_test.go @@ -2,6 +2,7 @@ package locale import ( "embed" + "errors" "sync" "testing" @@ -30,7 +31,12 @@ func parsedTags() map[string]bool { return out } -func localize(t *testing.T, lang, key string, templateData map[string]any) string { +// localize mirrors how production localizers are built (language +// ensured first, then NewLocalizer) and returns the raw library +// result: go-i18n answers a key missing in the requested language +// with the default language's text AND a MessageNotFoundErr, so +// callers here assert both halves of that contract. +func localize(t *testing.T, lang, key string, templateData map[string]any) (string, error) { t.Helper() if err := ensureLanguage(lang); err != nil { t.Fatalf("ensureLanguage(%q): %v", lang, err) @@ -38,7 +44,12 @@ func localize(t *testing.T, lang, key string, templateData map[string]any) strin i18nMu.RLock() loc := i18n.NewLocalizer(i18nBundle, lang) i18nMu.RUnlock() - msg, err := loc.Localize(&i18n.LocalizeConfig{MessageID: key, TemplateData: templateData}) + return loc.Localize(&i18n.LocalizeConfig{MessageID: key, TemplateData: templateData}) +} + +func localizeOK(t *testing.T, lang, key string, templateData map[string]any) string { + t.Helper() + msg, err := localize(t, lang, key, templateData) if err != nil { t.Fatalf("Localize(%q, %q): %v", lang, key, err) } @@ -60,10 +71,10 @@ func TestLazyLanguageParsing(t *testing.T) { t.Fatalf("non-startup languages parsed at init: %v", parsed) } - if got := localize(t, "zh-CN", "hello", nil); got != "你好" { + if got := localizeOK(t, "zh-CN", "hello", nil); got != "你好" { t.Fatalf("zh-CN hello = %q, want 你好", got) } - if got := localize(t, "zh-CN", "greet", map[string]any{"Name": "World"}); got != "你好 World" { + if got := localizeOK(t, "zh-CN", "greet", map[string]any{"Name": "World"}); got != "你好 World" { t.Fatalf("zh-CN greet = %q", got) } parsed = parsedTags() @@ -71,13 +82,31 @@ func TestLazyLanguageParsing(t *testing.T) { t.Fatalf("after zh-CN use, parsed = %v", parsed) } - // Missing keys fall back to English, as before. - if got := localize(t, "zh-CN", "only_en", nil); got != "Only in English" { - t.Fatalf("fallback = %q, want English text", got) + // A key missing in the requested language follows go-i18n's + // fallback contract: default-language text plus a + // MessageNotFoundErr — and the production I18n() wrapper maps + // that error to "", exactly as it did when every language was + // parsed eagerly. + msg, err := localize(t, "zh-CN", "only_en", nil) + if msg != "Only in English" { + t.Fatalf("fallback text = %q, want English text", msg) + } + var notFound *i18n.MessageNotFoundErr + if !errors.As(err, ¬Found) { + t.Fatalf("fallback err = %v, want MessageNotFoundErr", err) + } + i18nMu.RLock() + LocalizerWeb = i18n.NewLocalizer(i18nBundle, "zh-CN") + i18nMu.RUnlock() + if got := I18n(Web, "only_en"); got != "" { + t.Fatalf("I18n fallback = %q, want \"\" (wrapper error contract)", got) + } + if got := I18n(Web, "hello"); got != "你好" { + t.Fatalf("I18n zh-CN hello = %q, want 你好", got) } // A bare language preference resolves to the regional file. - if got := localize(t, "fa", "hello", nil); got != "سلام" { + if got := localizeOK(t, "fa", "hello", nil); got != "سلام" { t.Fatalf("fa hello = %q, want سلام", got) } if !parsedTags()["fa-IR"] { @@ -96,9 +125,9 @@ func TestLazyLanguageParsing(t *testing.T) { } // An unsupported language parses nothing and still localizes - // (in the fallback language) without error. + // (in the fallback language, matched directly, so no error). before := parsedTags() - if got := localize(t, "de-DE", "hello", nil); got != "Hello" { + if got := localizeOK(t, "de-DE", "hello", nil); got != "Hello" { t.Fatalf("de-DE hello = %q, want fallback Hello", got) } after := parsedTags()