Files
l-ui/internal/web/locale/locale_lazy_test.go
T

189 lines
5.9 KiB
Go

package locale
import (
"embed"
"errors"
"sync"
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
// Test fixtures mirror the production layout (an embed FS whose
// root holds "translation/<tag>.json") with three small languages.
// They live under this package's own translation/ directory and are
// used only by these tests — the panel embeds internal/web's files.
//go:embed translation
var testI18nFS embed.FS
type fakeSettingService struct{ lang string }
func (f fakeSettingService) GetTgLang() (string, error) { return f.lang, nil }
func parsedTags() map[string]bool {
i18nMu.RLock()
defer i18nMu.RUnlock()
out := make(map[string]bool, len(i18nParsed))
for k, v := range i18nParsed {
out[k] = v
}
return out
}
// 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)
}
i18nMu.RLock()
loc := i18n.NewLocalizer(i18nBundle, lang)
i18nMu.RUnlock()
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)
}
return msg
}
// ITEM-P3 M1: startup parses only the fallback (+ the bot's
// language); other languages parse on first use, then serve from
// cache; unsupported languages parse nothing and fall back.
func TestLazyLanguageParsing(t *testing.T) {
if err := InitLocalizer(testI18nFS, fakeSettingService{lang: "en-US"}); err != nil {
t.Fatalf("InitLocalizer: %v", err)
}
parsed := parsedTags()
if !parsed["en-US"] {
t.Fatalf("fallback language not parsed at init: %v", parsed)
}
if parsed["zh-CN"] || parsed["fa-IR"] {
t.Fatalf("non-startup languages parsed at init: %v", parsed)
}
if got := localizeOK(t, "zh-CN", "hello", nil); got != "你好" {
t.Fatalf("zh-CN hello = %q, want 你好", got)
}
if got := localizeOK(t, "zh-CN", "greet", map[string]any{"Name": "World"}); got != "你好 World" {
t.Fatalf("zh-CN greet = %q", got)
}
parsed = parsedTags()
if !parsed["zh-CN"] || parsed["fa-IR"] {
t.Fatalf("after zh-CN use, parsed = %v", parsed)
}
// 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, &notFound) {
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 := localizeOK(t, "fa", "hello", nil); got != "سلام" {
t.Fatalf("fa hello = %q, want سلام", got)
}
if !parsedTags()["fa-IR"] {
t.Fatal("fa-IR not parsed after fa preference")
}
// An Accept-Language header value resolves to its first entry.
if err := InitLocalizer(testI18nFS, fakeSettingService{lang: "en-US"}); err != nil {
t.Fatalf("re-init: %v", err)
}
if err := ensureLanguage("zh-CN,zh;q=0.9,en;q=0.8"); err != nil {
t.Fatalf("ensureLanguage(header): %v", err)
}
if !parsedTags()["zh-CN"] {
t.Fatal("zh-CN not parsed from Accept-Language header")
}
// An unsupported language parses nothing and still localizes
// (in the fallback language, matched directly, so no error).
before := parsedTags()
if got := localizeOK(t, "de-DE", "hello", nil); got != "Hello" {
t.Fatalf("de-DE hello = %q, want fallback Hello", got)
}
after := parsedTags()
if len(after) != len(before) {
t.Fatalf("unsupported language triggered a parse: before %v after %v", before, after)
}
}
// The bot's configured language is a startup language: its
// localizer must work immediately after init.
func TestBotLanguageParsedAtInit(t *testing.T) {
if err := InitLocalizer(testI18nFS, fakeSettingService{lang: "fa-IR"}); err != nil {
t.Fatalf("InitLocalizer: %v", err)
}
if !parsedTags()["fa-IR"] {
t.Fatal("bot language not parsed at init")
}
if got := I18n(Bot, "hello"); got != "سلام" {
t.Fatalf("bot hello = %q, want سلام", got)
}
}
// Concurrent first use of several languages: parses and lookups
// must not race (run with -race) and every lookup must succeed.
func TestConcurrentFirstUse(t *testing.T) {
if err := InitLocalizer(testI18nFS, fakeSettingService{lang: "en-US"}); err != nil {
t.Fatalf("InitLocalizer: %v", err)
}
langs := []string{"zh-CN", "fa-IR", "en-US", "zh", "fa", "de-DE"}
var wg sync.WaitGroup
errs := make(chan error, 256)
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for _, lang := range langs {
if err := ensureLanguage(lang); err != nil {
errs <- err
}
i18nMu.RLock()
loc := i18n.NewLocalizer(i18nBundle, lang)
i18nMu.RUnlock()
if _, err := loc.Localize(&i18n.LocalizeConfig{MessageID: "hello"}); err != nil {
errs <- err
}
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Fatalf("concurrent first use: %v", err)
}
parsed := parsedTags()
if !parsed["zh-CN"] || !parsed["fa-IR"] || !parsed["en-US"] {
t.Fatalf("parsed after concurrent use = %v", parsed)
}
}