package langx_test import ( "context" "fmt" "strings" "sync" "testing" "github.com/yuninks/langx" ) // reset 保证每个用例都在干净状态下运行(本库是全局单例) func reset(t *testing.T) { t.Helper() langx.Reset() t.Cleanup(langx.Reset) } func mustInit(t *testing.T, ops ...langx.Option) { t.Helper() if err := langx.InitLangx(ops...); err != nil { t.Fatalf("InitLangx() 返回错误: %v", err) } } func assertStrings(t *testing.T, what string, got, want []string) { t.Helper() if len(got) != len(want) { t.Fatalf("%s = %v, 期望 %v", what, got, want) } for i := range got { if got[i] != want[i] { t.Fatalf("%s = %v, 期望 %v", what, got, want) } } } func TestRegisterAndGet(t *testing.T) { reset(t) langx.RegisterCode(map[string]int{"login_success": 200, "error": 400}) langx.RegisterTrans("zh", map[string]string{"login_success": "成功", "username": "你好 #name#"}) langx.RegisterTrans("en", map[string]string{"login_success": "success"}) code, msg := langx.GetTrans("zh", "login_success") if code != 200 || msg != "成功" { t.Fatalf("GetTrans(zh) = (%d, %q), 期望 (200, 成功)", code, msg) } // 指定语言缺失时回落默认语言 if got := langx.GetMsg("en", "username"); got != "你好 #name#" { t.Fatalf("默认语言兜底 = %q", got) } // 语言名归一化: 大小写与首尾空格 if got := langx.GetMsg(" ZH ", "login_success"); got != "成功" { t.Fatalf("语言名归一化 = %q", got) } // 区域回退: zh-CN -> zh, zh-Hans-CN -> zh-hans -> zh if got := langx.GetMsg("zh-CN", "login_success"); got != "成功" { t.Fatalf("zh-CN 回退 = %q", got) } if got := langx.GetMsg("zh-Hans-CN", "login_success"); got != "成功" { t.Fatalf("zh-Hans-CN 回退 = %q", got) } // 未注册语言走默认语言 if got := langx.GetMsg("fr", "login_success"); got != "成功" { t.Fatalf("未注册语言 = %q", got) } // 未注册 key 返回 key 本身 if got := langx.GetMsg("zh", "nope"); got != "nope" { t.Fatalf("未注册 key = %q", got) } if langx.Has("zh", "nope") || !langx.Has("en", "login_success") { t.Fatal("Has 判断不正确") } } func TestUnknownKeyCode(t *testing.T) { reset(t) langx.RegisterCode(map[string]int{"ok": 200}) // 默认不再返回成功码 if got := langx.GetCode("nope"); got != -1 { t.Fatalf("未知 key 的 code = %d, 期望 -1", got) } if code, msg := langx.GetTrans("zh", "nope"); code != -1 || msg != "nope" { t.Fatalf("GetTrans(未知 key) = (%d, %q)", code, msg) } if langx.HasCode("nope") { t.Fatal("HasCode(未知 key) 应为 false") } if !langx.HasCode("ok") { t.Fatal("HasCode(ok) 应为 true") } // 可配置为失败码 mustInit(t, langx.SetDefaultCode(500)) if got := langx.GetCode("nope"); got != 500 { t.Fatalf("SetDefaultCode(500) 后 = %d", got) } if got := langx.GetDefaultCode(); got != 500 { t.Fatalf("GetDefaultCode() = %d", got) } } func TestMissingHandler(t *testing.T) { reset(t) var calls []string mustInit(t, langx.SetMissingHandler(func(lang, key string) { // 回调内再次调用本库不应死锁 _ = langx.Langs() calls = append(calls, lang+"|"+key) })) langx.RegisterTrans("zh", map[string]string{"a": "A"}) if got := langx.GetMsg("zh", "missing"); got != "missing" { t.Fatalf("GetMsg = %q", got) } if len(calls) != 1 || calls[0] != "zh|missing" { t.Fatalf("缺失回调 = %v", calls) } // 命中时不触发 _ = langx.GetMsg("zh", "a") _, _ = langx.GetTransFormat("zh", "a", nil) if len(calls) != 1 { t.Fatalf("命中时不应触发回调, 实际 %v", calls) } // 取消回调 mustInit(t, langx.SetMissingHandler(nil)) _ = langx.GetMsg("zh", "missing2") if len(calls) != 1 { t.Fatalf("取消回调后仍被调用: %v", calls) } } func TestFormat(t *testing.T) { reset(t) langx.RegisterTrans("zh", map[string]string{ "greet": "你好 #name#, 欢迎来到 #place#, #name#", "plain": "没有占位符", }) if got := langx.GetFormat("zh", "greet", map[string]string{"name": "张三", "place": "北京"}); got != "你好 张三, 欢迎来到 北京, 张三" { t.Fatalf("GetFormat = %q", got) } // 没有提供值的占位符原样保留, 便于发现问题 if got := langx.GetFormat("zh", "greet", map[string]string{"name": "张三"}); !strings.Contains(got, "#place#") { t.Fatalf("缺失参数时应保留占位符, 实际 %q", got) } // 模板没有占位符 / 参数为 nil / 空参数 if got := langx.GetFormat("zh", "plain", map[string]string{"name": "张三"}); got != "没有占位符" { t.Fatalf("无占位符模板 = %q", got) } if got := langx.GetFormat("zh", "plain", nil); got != "没有占位符" { t.Fatalf("nil 参数 = %q", got) } // 未注册的 key 也能得到替换后的 key(保持向后兼容) if got := langx.GetFormat("zh", "who_#name#", map[string]string{"name": "x"}); got != "who_x" { t.Fatalf("未注册 key = %q", got) } // 自定义占位符模式 mustInit(t, langx.SetReplaceKey("{{%s}}")) langx.RegisterTrans("zh", map[string]string{"t": "hi {{name}}"}) if got := langx.GetFormat("zh", "t", map[string]string{"name": "x"}); got != "hi x" { t.Fatalf("自定义模式 = %q", got) } // 模式只写占位符本身 mustInit(t, langx.SetReplaceKey("%s")) if got := langx.GetFormat("zh", "t", map[string]string{"{{name}}": "x"}); got != "hi x" { t.Fatalf("裸 %%s 模式 = %q", got) } } func TestInitLangxRejectsInvalidOption(t *testing.T) { reset(t) mustInit(t, langx.SetReplaceKey("{{%s}}")) langx.RegisterTrans("zh", map[string]string{"t": "hi {{name}}"}) for _, bad := range []string{"", "#{}#", "#%d#", "%s%s", "no-verb"} { if err := langx.InitLangx(langx.SetReplaceKey(bad)); err == nil { t.Fatalf("SetReplaceKey(%q) 应当返回错误", bad) } // 非法配置必须完全不生效 if got := langx.GetFormat("zh", "t", map[string]string{"name": "x"}); got != "hi x" { t.Fatalf("非法配置生效了: replaceKey=%q, 结果 %q", bad, got) } } // 合法模式仍然可用 mustInit(t, langx.SetReplaceKey("#%s#")) if err := langx.InitLangx(langx.SetDefaultLanguage("zh")); err != nil { t.Fatalf("合法配置报错: %v", err) } if err := langx.InitLangx(langx.SetDefaultLanguage(" ")); err == nil { t.Fatal("空默认语言应当报错") } if got := langx.GetDefaultLang(); got != "zh" { t.Fatalf("非法默认语言生效了: %q", got) } } func TestRegistrationDoesNotAliasCaller(t *testing.T) { reset(t) trans := map[string]string{"k": "v1"} langx.RegisterTrans("zh", trans) trans["k"] = "v2" trans["added"] = "x" if got := langx.GetMsg("zh", "k"); got != "v1" { t.Fatalf("注册后调用方修改 map 影响了库内数据: %q", got) } if langx.Has("zh", "added") { t.Fatal("注册后调用方新增的 key 进入了库内") } codes := map[string]int{"a": 1} langx.RegisterCode(codes) codes["a"] = 2 if got := langx.GetCode("a"); got != 1 { t.Fatalf("RegisterCode 后调用方修改 map 影响了库内数据: %d", got) } // AppendTrans 不得污染调用方传入的 map caller := map[string]string{"a": "1"} langx.RegisterTrans("en", caller) langx.AppendTrans("en", map[string]string{"b": "2"}) if _, ok := caller["b"]; ok { t.Fatalf("AppendTrans 污染了调用方的 map: %v", caller) } if got := langx.GetMsg("en", "b"); got != "2" { t.Fatalf("AppendTrans 未生效: %q", got) } } func TestRegisterSemantics(t *testing.T) { reset(t) langx.AppendTrans("zh", map[string]string{"a": "1"}) langx.AppendTrans("zh", map[string]string{"b": "2"}) assertStrings(t, "Langs()", langx.Langs(), []string{"zh"}) if langx.GetMsg("zh", "a") != "1" || langx.GetMsg("zh", "b") != "2" { t.Fatal("AppendTrans 应累加") } // RegisterTrans 整体替换 langx.RegisterTrans("zh", map[string]string{"c": "3"}) if langx.Has("zh", "a") || langx.GetMsg("zh", "c") != "3" { t.Fatal("RegisterTrans 应整体替换该语言") } // 传 nil 清空 langx.RegisterTrans("zh", nil) assertStrings(t, "Langs()", langx.Langs(), []string{"zh"}) if langx.Has("zh", "c") { t.Fatal("RegisterTrans(nil) 应清空该语言") } // AppendCode 累加, RegisterCode 替换 langx.AppendCode(map[string]int{"a": 1}) langx.AppendCode(map[string]int{"b": 2}) if langx.GetCode("a") != 1 || langx.GetCode("b") != 2 { t.Fatal("AppendCode 应累加") } langx.RegisterCode(map[string]int{"c": 3}) if langx.HasCode("a") || langx.GetCode("c") != 3 { t.Fatal("RegisterCode 应整体替换") } // 多语言按名排序 langx.RegisterTrans("en", map[string]string{"x": "y"}) langx.RegisterTrans("ZH-CN", map[string]string{"x": "z"}) assertStrings(t, "Langs()", langx.Langs(), []string{"en", "zh", "zh-cn"}) } func TestContextPriority(t *testing.T) { reset(t) langx.RegisterTrans("zh", map[string]string{"k": "中文"}) langx.RegisterTrans("en", map[string]string{"k": "English"}) mustInit(t, langx.SetDefaultLanguage("zh")) // 显式设置优先于请求头 ctx := langx.SetCtxLang(context.Background(), "en") ctx = context.WithValue(ctx, "Accept-Language", "zh-CN,zh;q=0.9") if got := langx.GetMsgCtx(ctx, "k"); got != "English" { t.Fatalf("显式语言应优先, 实际 %q", got) } if _, got := langx.GetTransFormatCtx(ctx, "k", nil); got != "English" { t.Fatalf("GetTransFormatCtx = %q", got) } // ctx 中显式指定的是 en, 因此取到英文 if got := langx.GetFormatCtx(ctx, "k", map[string]string{"n": "1"}); got != "English" { t.Fatalf("GetFormatCtx = %q", got) } } func TestConcurrentAccess(t *testing.T) { reset(t) langx.RegisterTrans("zh", map[string]string{"k": "v0"}) langx.RegisterCode(map[string]int{"ok": 200}) var wg sync.WaitGroup stop := make(chan struct{}) for i := 0; i < 4; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: } _, msg := langx.GetTrans("zh-CN", "k") if msg == "" { t.Error("读取到空译文") return } _ = langx.GetCode("ok") _ = langx.Langs() _ = langx.GetDefaultLang() _ = langx.Has("zh", "k") } }() } for i := 0; i < 200; i++ { langx.AppendTrans("zh", map[string]string{"k": fmt.Sprintf("v%d", i)}) langx.AppendCode(map[string]int{"n": i}) langx.AppendTrans("en", map[string]string{"k": fmt.Sprintf("e%d", i)}) if err := langx.InitLangx(langx.SetDefaultCode(i)); err != nil { t.Errorf("InitLangx: %v", err) break } } close(stop) wg.Wait() } func TestReset(t *testing.T) { reset(t) langx.RegisterTrans("zh", map[string]string{"k": "v"}) langx.RegisterCode(map[string]int{"c": 1}) mustInit(t, langx.SetDefaultCode(500), langx.SetDefaultLanguage("en")) langx.Reset() assertStrings(t, "Langs()", langx.Langs(), nil) if langx.HasCode("c") { t.Fatal("Reset 未清空响应码") } if got := langx.GetDefaultLang(); got != "zh" { t.Fatalf("Reset 未恢复默认语言: %q", got) } if got := langx.GetDefaultCode(); got != -1 { t.Fatalf("Reset 未恢复默认码: %d", got) } }