多语言翻译
This commit is contained in:
+333
-33
@@ -1,49 +1,349 @@
|
||||
package langx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/yuninks/langx"
|
||||
)
|
||||
|
||||
const (
|
||||
Lang string = "s"
|
||||
)
|
||||
|
||||
var MapCode = map[string]int{
|
||||
Lang: 200,
|
||||
// reset 保证每个用例都在干净状态下运行(本库是全局单例)
|
||||
func reset(t *testing.T) {
|
||||
t.Helper()
|
||||
langx.Reset()
|
||||
t.Cleanup(langx.Reset)
|
||||
}
|
||||
|
||||
func TestLangx(t *testing.T) {
|
||||
langx.InitLangx(
|
||||
langx.SetDefaultCode(0),
|
||||
langx.SetDefaultLanguage("zh"),
|
||||
)
|
||||
func mustInit(t *testing.T, ops ...langx.Option) {
|
||||
t.Helper()
|
||||
if err := langx.InitLangx(ops...); err != nil {
|
||||
t.Fatalf("InitLangx() 返回错误: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
langx.RegisterCode(map[string]int{
|
||||
"login_success": 200,
|
||||
"error": 400,
|
||||
})
|
||||
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{
|
||||
"login_success": "成功",
|
||||
"error": "错误",
|
||||
"username": "你好 #name#", // 有占位符
|
||||
"greet": "你好 #name#, 欢迎来到 #place#, #name#",
|
||||
"plain": "没有占位符",
|
||||
})
|
||||
langx.RegisterTrans("en", map[string]string{
|
||||
"login_success": "success",
|
||||
"error": "error",
|
||||
"username": "Hello #name#", // 有占位符
|
||||
})
|
||||
// 获取翻译码
|
||||
code, msg := langx.GetTransFormat("zh", "login_success", nil)
|
||||
t.Log(code, msg)
|
||||
code, msg = langx.GetTransFormat("en", "error", nil)
|
||||
t.Log(code, msg)
|
||||
|
||||
// 获取翻译码,有占位符
|
||||
code, msg = langx.GetTransFormat("zh", "username", map[string]string{
|
||||
"name": "张三",
|
||||
})
|
||||
t.Log(code, msg)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user