Skip to content

Commit 4c746ec

Browse files
author
0ko
committed
Initial support for localization and pluralization with go-i18n-JSON-v2 format (go-gitea#6203)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6203 Reviewed-by: Gusted <[email protected]> Reviewed-by: 0ko <[email protected]>
2 parents 401906b + a2787bb commit 4c746ec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1317
-51
lines changed

.deadcode-out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ code.gitea.io/gitea/modules/translation
246246
MockLocale.TrString
247247
MockLocale.Tr
248248
MockLocale.TrN
249+
MockLocale.TrPluralString
249250
MockLocale.TrSize
250251
MockLocale.PrettyNumber
251252

modules/translation/i18n/dummy.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,19 @@ func (k *KeyLocale) HasKey(trKey string) bool {
2222

2323
// TrHTML implements Locale.
2424
func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML {
25-
args := slices.Clone(trArgs)
26-
for i, v := range args {
27-
switch v := v.(type) {
28-
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
29-
// for most basic types (including template.HTML which is safe), just do nothing and use it
30-
case string:
31-
args[i] = template.HTMLEscapeString(v)
32-
case fmt.Stringer:
33-
args[i] = template.HTMLEscapeString(v.String())
34-
default:
35-
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
36-
}
37-
}
38-
return template.HTML(k.TrString(trKey, args...))
25+
return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...))
3926
}
4027

4128
// TrString implements Locale.
4229
func (k *KeyLocale) TrString(trKey string, trArgs ...any) string {
4330
return FormatDummy(trKey, trArgs...)
4431
}
4532

33+
// TrPluralString implements Locale.
34+
func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
35+
return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...))
36+
}
37+
4638
func FormatDummy(trKey string, args ...any) string {
4739
if len(args) == 0 {
4840
return fmt.Sprintf("(%s)", trKey)

modules/translation/i18n/errors.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
)
99

1010
var (
11-
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
12-
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
11+
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
12+
ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist}
13+
ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist}
14+
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
1315
)

modules/translation/i18n/i18n.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,28 @@ import (
88
"io"
99
)
1010

11+
type (
12+
PluralFormIndex uint8
13+
PluralFormRule func(int64) PluralFormIndex
14+
)
15+
16+
const (
17+
PluralFormZero PluralFormIndex = iota
18+
PluralFormOne
19+
PluralFormTwo
20+
PluralFormFew
21+
PluralFormMany
22+
PluralFormOther
23+
)
24+
1125
var DefaultLocales = NewLocaleStore()
1226

1327
type Locale interface {
1428
// TrString translates a given key and arguments for a language
1529
TrString(trKey string, trArgs ...any) string
30+
// TrPluralString translates a given pluralized key and arguments for a language.
31+
// This function returns an error if new-style support for the given key is not available.
32+
TrPluralString(count any, trKey string, trArgs ...any) template.HTML
1633
// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
1734
TrHTML(trKey string, trArgs ...any) template.HTML
1835
// HasKey reports if a locale has a translation for a given key
@@ -31,8 +48,10 @@ type LocaleStore interface {
3148
Locale(langName string) (Locale, bool)
3249
// HasLang returns whether a given language is present in the store
3350
HasLang(langName string) bool
34-
// AddLocaleByIni adds a new language to the store
35-
AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error
51+
// AddLocaleByIni adds a new old-style language to the store
52+
AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error
53+
// AddLocaleByJSON adds new-style content to an existing language to the store
54+
AddToLocaleFromJSON(langName string, source []byte) error
3655
}
3756

3857
// ResetDefaultLocales resets the current default locales

modules/translation/i18n/i18n_test.go

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ import (
1212
"github.com/stretchr/testify/require"
1313
)
1414

15+
var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex {
16+
if n == 0 {
17+
return PluralFormZero
18+
}
19+
if n == 1 {
20+
return PluralFormOne
21+
}
22+
if n >= 2 && n <= 4 {
23+
return PluralFormFew
24+
}
25+
return PluralFormOther
26+
}
27+
28+
var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex {
29+
if n == 1 {
30+
return PluralFormOne
31+
}
32+
return PluralFormOther
33+
}
34+
1535
func TestLocaleStore(t *testing.T) {
1636
testData1 := []byte(`
1737
.dot.name = Dot Name
@@ -27,11 +47,48 @@ fmt = %[2]s %[1]s
2747
2848
[section]
2949
sub = Changed Sub String
50+
commits = fallback value for commits
51+
`)
52+
53+
testDataJSON2 := []byte(`
54+
{
55+
"section.json": "the JSON is %s",
56+
"section.commits": {
57+
"one": "one %d commit",
58+
"few": "some %d commits",
59+
"other": "lots of %d commits"
60+
},
61+
"section.incomplete": {
62+
"few": "some %d objects (translated)"
63+
},
64+
"nested": {
65+
"outer": {
66+
"inner": {
67+
"json": "Hello World",
68+
"issue": {
69+
"one": "one %d issue",
70+
"few": "some %d issues",
71+
"other": "lots of %d issues"
72+
}
73+
}
74+
}
75+
}
76+
}
77+
`)
78+
testDataJSON1 := []byte(`
79+
{
80+
"section.incomplete": {
81+
"one": "[untranslated] some %d object",
82+
"other": "[untranslated] some %d objects"
83+
}
84+
}
3085
`)
3186

3287
ls := NewLocaleStore()
33-
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil))
34-
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
88+
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil))
89+
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil))
90+
require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1))
91+
require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2))
3592
ls.SetDefaultLang("lang1")
3693

3794
lang1, _ := ls.Locale("lang1")
@@ -56,6 +113,45 @@ sub = Changed Sub String
56113
result2 := lang2.TrHTML("section.mixed", "a&b")
57114
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
58115

116+
result = lang2.TrString("section.json", "valid")
117+
assert.Equal(t, "the JSON is valid", result)
118+
119+
result = lang2.TrString("nested.outer.inner.json")
120+
assert.Equal(t, "Hello World", result)
121+
122+
result = lang2.TrString("section.commits")
123+
assert.Equal(t, "lots of %d commits", result)
124+
125+
result2 = lang2.TrPluralString(1, "section.commits", 1)
126+
assert.EqualValues(t, "one 1 commit", result2)
127+
128+
result2 = lang2.TrPluralString(3, "section.commits", 3)
129+
assert.EqualValues(t, "some 3 commits", result2)
130+
131+
result2 = lang2.TrPluralString(8, "section.commits", 8)
132+
assert.EqualValues(t, "lots of 8 commits", result2)
133+
134+
result2 = lang2.TrPluralString(0, "section.commits")
135+
assert.EqualValues(t, "section.commits", result2)
136+
137+
result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1)
138+
assert.EqualValues(t, "one 1 issue", result2)
139+
140+
result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3)
141+
assert.EqualValues(t, "some 3 issues", result2)
142+
143+
result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9)
144+
assert.EqualValues(t, "lots of 9 issues", result2)
145+
146+
result2 = lang2.TrPluralString(3, "section.incomplete", 3)
147+
assert.EqualValues(t, "some 3 objects (translated)", result2)
148+
149+
result2 = lang2.TrPluralString(1, "section.incomplete", 1)
150+
assert.EqualValues(t, "[untranslated] some 1 object", result2)
151+
152+
result2 = lang2.TrPluralString(7, "section.incomplete", 7)
153+
assert.EqualValues(t, "[untranslated] some 7 objects", result2)
154+
59155
langs, descs := ls.ListLangNameDesc()
60156
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
61157
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
@@ -77,7 +173,7 @@ c=22
77173
`)
78174

79175
ls := NewLocaleStore()
80-
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
176+
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2))
81177
lang1, _ := ls.Locale("lang1")
82178
assert.Equal(t, "11", lang1.TrString("a"))
83179
assert.Equal(t, "21", lang1.TrString("b"))
@@ -118,7 +214,7 @@ func (e *errorPointerReceiver) Error() string {
118214

119215
func TestLocaleWithTemplate(t *testing.T) {
120216
ls := NewLocaleStore()
121-
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
217+
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), nil))
122218
lang1, _ := ls.Locale("lang1")
123219

124220
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
@@ -181,7 +277,7 @@ func TestLocaleStoreQuirks(t *testing.T) {
181277

182278
for _, testData := range testDataList {
183279
ls := NewLocaleStore()
184-
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
280+
err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil)
185281
lang1, _ := ls.Locale("lang1")
186282
require.NoError(t, err, testData.hint)
187283
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)

0 commit comments

Comments
 (0)