189 lines
5.9 KiB
Go
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, ¬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 := 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)
|
|
}
|
|
}
|