code.gitea.io/gitea@v1.22.3/modules/translation/i18n/i18n_test.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package i18n 5 6 import ( 7 "html/template" 8 "strings" 9 "testing" 10 11 "github.com/stretchr/testify/assert" 12 ) 13 14 func TestLocaleStore(t *testing.T) { 15 testData1 := []byte(` 16 .dot.name = Dot Name 17 fmt = %[1]s %[2]s 18 19 [section] 20 sub = Sub String 21 mixed = test value; <span style="color: red\; background: none;">%s</span> 22 `) 23 24 testData2 := []byte(` 25 fmt = %[2]s %[1]s 26 27 [section] 28 sub = Changed Sub String 29 `) 30 31 ls := NewLocaleStore() 32 assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil)) 33 assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) 34 ls.SetDefaultLang("lang1") 35 36 lang1, _ := ls.Locale("lang1") 37 lang2, _ := ls.Locale("lang2") 38 39 result := lang1.TrString("fmt", "a", "b") 40 assert.Equal(t, "a b", result) 41 42 result = lang2.TrString("fmt", "a", "b") 43 assert.Equal(t, "b a", result) 44 45 result = lang1.TrString("section.sub") 46 assert.Equal(t, "Sub String", result) 47 48 result = lang2.TrString("section.sub") 49 assert.Equal(t, "Changed Sub String", result) 50 51 langNone, _ := ls.Locale("none") 52 result = langNone.TrString(".dot.name") 53 assert.Equal(t, "Dot Name", result) 54 55 result2 := lang2.TrHTML("section.mixed", "a&b") 56 assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&b</span>`, result2) 57 58 langs, descs := ls.ListLangNameDesc() 59 assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) 60 assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) 61 62 found := lang1.HasKey("no-such") 63 assert.False(t, found) 64 assert.NoError(t, ls.Close()) 65 } 66 67 func TestLocaleStoreMoreSource(t *testing.T) { 68 testData1 := []byte(` 69 a=11 70 b=12 71 `) 72 73 testData2 := []byte(` 74 b=21 75 c=22 76 `) 77 78 ls := NewLocaleStore() 79 assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) 80 lang1, _ := ls.Locale("lang1") 81 assert.Equal(t, "11", lang1.TrString("a")) 82 assert.Equal(t, "21", lang1.TrString("b")) 83 assert.Equal(t, "22", lang1.TrString("c")) 84 } 85 86 type stringerPointerReceiver struct { 87 s string 88 } 89 90 func (s *stringerPointerReceiver) String() string { 91 return s.s 92 } 93 94 type stringerStructReceiver struct { 95 s string 96 } 97 98 func (s stringerStructReceiver) String() string { 99 return s.s 100 } 101 102 type errorStructReceiver struct { 103 s string 104 } 105 106 func (e errorStructReceiver) Error() string { 107 return e.s 108 } 109 110 type errorPointerReceiver struct { 111 s string 112 } 113 114 func (e *errorPointerReceiver) Error() string { 115 return e.s 116 } 117 118 func TestLocaleWithTemplate(t *testing.T) { 119 ls := NewLocaleStore() 120 assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil)) 121 lang1, _ := ls.Locale("lang1") 122 123 tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) 124 tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`)) 125 126 cases := []struct { 127 in any 128 want string 129 }{ 130 {"<str>", "<a><str></a>"}, 131 {[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"}, 132 {template.HTML("<html>"), "<a><html></a>"}, 133 {stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{<stringerPointerReceiver>}</a>"}, 134 {&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a><stringerPointerReceiver ptr></a>"}, 135 {stringerStructReceiver{"<stringerStructReceiver>"}, "<a><stringerStructReceiver></a>"}, 136 {&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a><stringerStructReceiver ptr></a>"}, 137 {errorStructReceiver{"<errorStructReceiver>"}, "<a><errorStructReceiver></a>"}, 138 {&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a><errorStructReceiver ptr></a>"}, 139 {errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{<errorPointerReceiver>}</a>"}, 140 {&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a><errorPointerReceiver ptr></a>"}, 141 } 142 143 buf := &strings.Builder{} 144 for _, c := range cases { 145 buf.Reset() 146 assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in})) 147 assert.Equal(t, c.want, buf.String()) 148 } 149 } 150 151 func TestLocaleStoreQuirks(t *testing.T) { 152 const nl = "\n" 153 q := func(q1, s string, q2 ...string) string { 154 return q1 + s + strings.Join(q2, "") 155 } 156 testDataList := []struct { 157 in string 158 out string 159 hint string 160 }{ 161 {` xx`, `xx`, "simple, no quote"}, 162 {`" xx"`, ` xx`, "simple, double-quote"}, 163 {`' xx'`, ` xx`, "simple, single-quote"}, 164 {"` xx`", ` xx`, "simple, back-quote"}, 165 166 {`x\"y`, `x\"y`, "no unescape, simple"}, 167 {q(`"`, `x\"y`, `"`), `"x\"y"`, "unescape, double-quote"}, 168 {q(`'`, `x\"y`, `'`), `x\"y`, "no unescape, single-quote"}, 169 {q("`", `x\"y`, "`"), `x\"y`, "no unescape, back-quote"}, 170 171 {q(`"`, `x\"y`) + nl + "b=", `"x\"y`, "half open, double-quote"}, 172 {q(`'`, `x\"y`) + nl + "b=", `'x\"y`, "half open, single-quote"}, 173 {q("`", `x\"y`) + nl + "b=`", `x\"y` + nl + "b=", "half open, back-quote, multi-line"}, 174 175 {`x ; y`, `x ; y`, "inline comment (;)"}, 176 {`x # y`, `x # y`, "inline comment (#)"}, 177 {`x \; y`, `x ; y`, `inline comment (\;)`}, 178 {`x \# y`, `x # y`, `inline comment (\#)`}, 179 } 180 181 for _, testData := range testDataList { 182 ls := NewLocaleStore() 183 err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) 184 lang1, _ := ls.Locale("lang1") 185 assert.NoError(t, err, testData.hint) 186 assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) 187 assert.NoError(t, ls.Close()) 188 } 189 190 // TODO: Crowdin needs the strings to be quoted correctly and doesn't like incomplete quotes 191 // and Crowdin always outputs quoted strings if there are quotes in the strings. 192 // So, Gitea's `key="quoted" unquoted` content shouldn't be used on Crowdin directly, 193 // it should be converted to `key="\"quoted\" unquoted"` first. 194 // TODO: We can not use UnescapeValueDoubleQuotes=true, because there are a lot of back-quotes in en-US.ini, 195 // then Crowdin will output: 196 // > key = "`x \" y`" 197 // Then Gitea will read a string with back-quotes, which is incorrect. 198 // TODO: Crowdin might generate multi-line strings, quoted by double-quote, it's not supported by LocaleStore 199 // LocaleStore uses back-quote for multi-line strings, it's not supported by Crowdin. 200 // TODO: Crowdin doesn't support back-quote as string quoter, it mainly uses double-quote 201 // so, the following line will be parsed as: value="`first", comment="second`" on Crowdin 202 // > a = `first; second` 203 }