返回 DeepSeek-Reasonix
i18n_test.go
根目录 / internal / i18n / i18n_test.go
1 package i18n
2
3 import (
4 "reflect"
5 "strings"
6 "testing"
7 )
8
9 // TestCatalogsComplete reflects over English (the baseline) and asserts every
10 // other catalogue populates the same fields. Empty strings count as missing
11 // translations so drift fails CI instead of surfacing as blank output. As new
12 // languages land they get added to the catalogs map below.
13 func TestCatalogsComplete(t *testing.T) {
14 en := reflect.ValueOf(English)
15 typ := en.Type()
16 catalogs := map[string]reflect.Value{"zh": reflect.ValueOf(Chinese), "zh-TW": reflect.ValueOf(ChineseTraditional)}
17 for tag, cat := range catalogs {
18 for i := 0; i < typ.NumField(); i++ {
19 name := typ.Field(i).Name
20 if strings.TrimSpace(cat.Field(i).String()) == "" {
21 t.Errorf("%s catalogue: field %q is empty", tag, name)
22 }
23 }
24 }
25 }
26
27 // TestCatalogsAgreeOnPlaceholders catches translations that silently drop or
28 // gain %s/%d/%q placeholders — a class of bug that only blows up when the
29 // affected message is rendered. Compares the count per format verb across
30 // languages for any field whose name ends in "Fmt".
31 func TestCatalogsAgreeOnPlaceholders(t *testing.T) {
32 en := reflect.ValueOf(English)
33 typ := en.Type()
34 for i := 0; i < typ.NumField(); i++ {
35 name := typ.Field(i).Name
36 if !strings.HasSuffix(name, "Fmt") {
37 continue
38 }
39 want := countVerbs(en.Field(i).String())
40 got := countVerbs(reflect.ValueOf(Chinese).Field(i).String())
41 if want != got {
42 t.Errorf("%s: en has %d verbs, zh has %d", name, want, got)
43 }
44 gotTW := countVerbs(reflect.ValueOf(ChineseTraditional).Field(i).String())
45 if want != gotTW {
46 t.Errorf("%s: en has %d verbs, zh-TW has %d", name, want, gotTW)
47 }
48 }
49 }
50
51 func TestPlanApprovalChoicesExposeThreeExplicitActions(t *testing.T) {
52 tests := []struct {
53 tag string
54 value string
55 want []string
56 }{
57 {tag: "en", value: English.PlanApprovalChoices, want: []string{"Start execution", "Revise plan", "Exit without executing"}},
58 {tag: "zh", value: Chinese.PlanApprovalChoices, want: []string{"开始执行", "修改计划", "暂不执行,退出计划模式"}},
59 {tag: "zh-TW", value: ChineseTraditional.PlanApprovalChoices, want: []string{"開始執行", "修改計畫", "暫不執行,退出計畫模式"}},
60 }
61 for _, tt := range tests {
62 t.Run(tt.tag, func(t *testing.T) {
63 numbered := 0
64 for _, line := range strings.Split(tt.value, "\n") {
65 line = strings.TrimSpace(line)
66 if len(line) >= 3 && line[0] >= '1' && line[0] <= '9' && line[1] == '.' {
67 numbered++
68 }
69 }
70 if numbered != 3 {
71 t.Fatalf("numbered Plan actions = %d, want 3:\n%s", numbered, tt.value)
72 }
73 for _, want := range tt.want {
74 if !strings.Contains(tt.value, want) {
75 t.Errorf("Plan choices missing %q:\n%s", want, tt.value)
76 }
77 }
78 })
79 }
80 }
81
82 // countVerbs counts unescaped fmt placeholders (%s, %d, %q, %v, …). %% does
83 // not count.
84 func countVerbs(s string) int {
85 n := 0
86 for i := 0; i < len(s); i++ {
87 if s[i] != '%' {
88 continue
89 }
90 if i+1 < len(s) && s[i+1] == '%' {
91 i++
92 continue
93 }
94 n++
95 }
96 return n
97 }
98
99 // TestNormalize covers the locale-string shapes likely to land in $LANG /
100 // $LC_ALL / $REASONIX_LANG. Unknown locales return "" so DetectLanguage falls
101 // through to the next candidate instead of mis-routing.
102 func TestNormalize(t *testing.T) {
103 cases := map[string]string{
104 "": "",
105 "en": "en",
106 "en_US.UTF-8": "en",
107 "zh": "zh",
108 "zh_CN.UTF-8": "zh",
109 "zh-Hans-CN": "zh",
110 "Chinese (China)": "zh",
111 "中文": "zh",
112 "zh-TW": "zh-TW",
113 "zh_TW.UTF-8": "zh-TW",
114 "zh-Hant-TW": "zh-TW",
115 "zh-Hant": "zh-TW",
116 "Chinese Traditional": "zh-TW",
117 "繁體": "zh-TW",
118 "fr_FR.UTF-8": "",
119 " ZH_TW ": "zh-TW",
120 }
121 for in, want := range cases {
122 if got := normalize(in); got != want {
123 t.Errorf("normalize(%q) = %q, want %q", in, got, want)
124 }
125 }
126 }
127
128 // TestDetectLanguagePriority verifies override beats env and that REASONIX_LANG
129 // beats LANG. With a clean env we fall back to English.
130 func TestDetectLanguagePriority(t *testing.T) {
131 t.Setenv("REASONIX_LANG", "")
132 t.Setenv("LC_ALL", "")
133 t.Setenv("LC_MESSAGES", "")
134 t.Setenv("LANG", "")
135 defer DetectLanguage("") // restore default for other tests
136
137 if got := DetectLanguage(""); got != "en" {
138 t.Errorf("clean env: got %q, want en", got)
139 }
140
141 t.Setenv("LANG", "zh_CN.UTF-8")
142 if got := DetectLanguage(""); got != "zh" {
143 t.Errorf("LANG=zh_CN.UTF-8: got %q, want zh", got)
144 }
145
146 t.Setenv("REASONIX_LANG", "en")
147 if got := DetectLanguage(""); got != "en" {
148 t.Errorf("REASONIX_LANG=en overriding LANG=zh: got %q, want en", got)
149 }
150
151 if got := DetectLanguage("zh"); got != "zh" {
152 t.Errorf("override=zh: got %q, want zh", got)
153 }
154 if got := CurrentLanguage(); got != "zh" {
155 t.Errorf("current language = %q, want zh", got)
156 }
157 if got := DetectLanguage("zh-TW"); got != "zh-TW" || CurrentLanguage() != "zh-TW" {
158 t.Errorf("traditional Chinese current language = %q/%q, want zh-TW", got, CurrentLanguage())
159 }
160 }
161
161 lines GO