chore(test): separate and move around i18n testing (#10539)
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions

Closes #10534

The primary code change is that `TestMissingTranslationHandling` was converted to a unit test.

However translation unit tests were a mix of mostly unrelated old (INI) and new (JSON) code with unclear licensing. It was cleaned up with help of Git history.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10539
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
0ko 2025-12-23 04:39:26 +01:00
parent b1adc7d931
commit 973ff28f44
7 changed files with 365 additions and 330 deletions

View file

@ -302,7 +302,7 @@ func main() {
if name == "docker" || name == ".git" || name == "node_modules" {
return fs.SkipDir
}
} else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" {
} else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" || fpath == "modules/translation/i18n/i18n_ini_test.go" {
// skip false positives
} else if strings.HasSuffix(name, ".go") {
onError(HandleGoFile(handler, fpath, nil))

View file

@ -0,0 +1,206 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package i18n
import (
"html/template"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLocaleStoreINI(t *testing.T) {
testData1 := []byte(`
.dot.name = Dot Name
fmt = %[1]s %[2]s
[section]
sub = Sub String
mixed = test value; <span style="color: red\; background: none;">%s</span>
`)
testData2 := []byte(`
fmt = %[2]s %[1]s
[section]
sub = Changed Sub String
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, testData1, nil))
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, testData2, nil))
ls.SetDefaultLang("lang1")
lang1, _ := ls.Locale("lang1")
lang2, _ := ls.Locale("lang2")
result := lang1.TrString("fmt", "a", "b")
assert.Equal(t, "a b", result)
result = lang2.TrString("fmt", "a", "b")
assert.Equal(t, "b a", result)
result = lang1.TrString("section.sub")
assert.Equal(t, "Sub String", result)
result = lang2.TrString("section.sub")
assert.Equal(t, "Changed Sub String", result)
langNone, _ := ls.Locale("none")
result = langNone.TrString(".dot.name")
assert.Equal(t, "Dot Name", result)
result2 := lang2.TrHTML("section.mixed", "a&b")
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
found := lang1.HasKey("no-such")
assert.False(t, found)
assert.Equal(t, "no-such", lang1.TrString("no-such"))
require.NoError(t, ls.Close())
}
func TestLocaleStoreMoreSource(t *testing.T) {
testData1 := []byte(`
a=11
b=12
`)
testData2 := []byte(`
b=21
c=22
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, testData1, testData2))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "11", lang1.TrString("a"))
assert.Equal(t, "21", lang1.TrString("b"))
assert.Equal(t, "22", lang1.TrString("c"))
}
type stringerPointerReceiver struct {
s string
}
func (s *stringerPointerReceiver) String() string {
return s.s
}
type stringerStructReceiver struct {
s string
}
func (s stringerStructReceiver) String() string {
return s.s
}
type errorStructReceiver struct {
s string
}
func (e errorStructReceiver) Error() string {
return e.s
}
type errorPointerReceiver struct {
s string
}
func (e *errorPointerReceiver) Error() string {
return e.s
}
func TestLocaleWithTemplate(t *testing.T) {
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, []byte(`key=<a>%s</a>`), nil))
lang1, _ := ls.Locale("lang1")
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
cases := []struct {
in any
want string
}{
{"<str>", "<a>&lt;str&gt;</a>"},
{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
{template.HTML("<html>"), "<a><html></a>"},
{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</a>"},
}
buf := &strings.Builder{}
for _, c := range cases {
buf.Reset()
require.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
assert.Equal(t, c.want, buf.String())
}
}
func TestLocaleStoreQuirks(t *testing.T) {
const nl = "\n"
q := func(q1, s string, q2 ...string) string {
return q1 + s + strings.Join(q2, "")
}
testDataList := []struct {
in string
out string
hint string
}{
{` xx`, `xx`, "simple, no quote"},
{`" xx"`, ` xx`, "simple, double-quote"},
{`' xx'`, ` xx`, "simple, single-quote"},
{"` xx`", ` xx`, "simple, back-quote"},
{`x\"y`, `x\"y`, "no unescape, simple"},
{q(`"`, `x\"y`, `"`), `"x\"y"`, "unescape, double-quote"},
{q(`'`, `x\"y`, `'`), `x\"y`, "no unescape, single-quote"},
{q("`", `x\"y`, "`"), `x\"y`, "no unescape, back-quote"},
{q(`"`, `x\"y`) + nl + "b=", `"x\"y`, "half open, double-quote"},
{q(`'`, `x\"y`) + nl + "b=", `'x\"y`, "half open, single-quote"},
{q("`", `x\"y`) + nl + "b=`", `x\"y` + nl + "b=", "half open, back-quote, multi-line"},
{`x ; y`, `x ; y`, "inline comment (;)"},
{`x # y`, `x # y`, "inline comment (#)"},
{`x \; y`, `x ; y`, `inline comment (\;)`},
{`x \# y`, `x # y`, `inline comment (\#)`},
}
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", nil, nil, []byte("a="+testData.in), nil)
lang1, _ := ls.Locale("lang1")
require.NoError(t, err, testData.hint)
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
require.NoError(t, ls.Close())
}
// TODO: Crowdin needs the strings to be quoted correctly and doesn't like incomplete quotes
// and Crowdin always outputs quoted strings if there are quotes in the strings.
// So, Gitea's `key="quoted" unquoted` content shouldn't be used on Crowdin directly,
// it should be converted to `key="\"quoted\" unquoted"` first.
// TODO: We can not use UnescapeValueDoubleQuotes=true, because there are a lot of back-quotes in en-US.ini,
// then Crowdin will output:
// > key = "`x \" y`"
// Then Gitea will read a string with back-quotes, which is incorrect.
// TODO: Crowdin might generate multi-line strings, quoted by double-quote, it's not supported by LocaleStore
// LocaleStore uses back-quote for multi-line strings, it's not supported by Crowdin.
// TODO: Crowdin doesn't support back-quote as string quoter, it mainly uses double-quote
// so, the following line will be parsed as: value="`first", comment="second`" on Crowdin
// > a = `first; second`
}

View file

@ -1,10 +1,9 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Copyright 2024-2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package i18n
import (
"html/template"
"strings"
"testing"
@ -37,24 +36,7 @@ var (
UsedPluralFormsMock = []PluralFormIndex{PluralFormZero, PluralFormOne, PluralFormFew, PluralFormOther}
)
func TestLocaleStore(t *testing.T) {
testData1 := []byte(`
.dot.name = Dot Name
fmt = %[1]s %[2]s
[section]
sub = Sub String
mixed = test value; <span style="color: red\; background: none;">%s</span>
`)
testData2 := []byte(`
fmt = %[2]s %[1]s
[section]
sub = Changed Sub String
commits = fallback value for commits
`)
func TestLocaleStoreJSON(t *testing.T) {
testDataJSON2 := []byte(`
{
"section.json": "the JSON is %s",
@ -90,35 +72,18 @@ commits = fallback value for commits
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, testData1, nil))
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, testData2, nil))
// Currently LocaleStore has to be first populated with langcodes via AddLocaleByIni
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, []byte(""), nil))
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, []byte(""), nil))
require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1))
require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2))
ls.SetDefaultLang("lang1")
lang1, _ := ls.Locale("lang1")
ls.SetDefaultLang("lang1")
lang2, _ := ls.Locale("lang2")
result := lang1.TrString("fmt", "a", "b")
assert.Equal(t, "a b", result)
result = lang2.TrString("fmt", "a", "b")
assert.Equal(t, "b a", result)
result = lang1.TrString("section.sub")
assert.Equal(t, "Sub String", result)
result = lang2.TrString("section.sub")
assert.Equal(t, "Changed Sub String", result)
langNone, _ := ls.Locale("none")
result = langNone.TrString(".dot.name")
assert.Equal(t, "Dot Name", result)
result2 := lang2.TrHTML("section.mixed", "a&b")
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
result = lang2.TrString("section.json", "valid")
result := lang2.TrString("section.json", "valid")
assert.Equal(t, "the JSON is valid", result)
result = lang2.TrString("nested.outer.inner.json")
@ -127,7 +92,7 @@ commits = fallback value for commits
result = lang2.TrString("section.commits")
assert.Equal(t, "lots of %d commits", result)
result2 = lang2.TrPluralString(1, "section.commits", 1)
result2 := lang2.TrPluralString(1, "section.commits", 1)
assert.EqualValues(t, "one 1 commit", result2)
result2 = lang2.TrPluralString(3, "section.commits", 3)
@ -156,159 +121,34 @@ commits = fallback value for commits
result2 = lang2.TrPluralString(7, "section.incomplete", 7)
assert.EqualValues(t, "[untranslated] some 7 objects", result2)
}
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
func TestMissingTranslationHandling(t *testing.T) {
ls := NewLocaleStore()
// Test HasKey for JSON
found := lang2.HasKey("section.json")
// Currently LocaleStore has to be first populated with langcodes via AddLocaleByIni
require.NoError(t, ls.AddLocaleByIni("en-US", "English", MockPluralRuleEnglish, UsedPluralFormsEnglish, []byte(""), nil))
require.NoError(t, ls.AddLocaleByIni("fun", "Funlang", MockPluralRule, UsedPluralFormsMock, []byte(""), nil))
require.NoError(t, ls.AddToLocaleFromJSON("en-US", []byte(`
{
"incorrect_root_url": "This Forgejo instance...",
"meta.last_line": "Hi there!"
}`)))
require.NoError(t, ls.AddToLocaleFromJSON("fun", []byte(`
{
"meta.last_line": "This language only has one line that is never used by the UI. It will never have a translation for incorrect_root_url"
}`)))
ls.SetDefaultLang("en-US")
// Get "fun" locale, make sure it's available
funLocale, found := ls.Locale("fun")
assert.True(t, found)
// Test HasKey for INI
found = lang2.HasKey("section.sub")
assert.True(t, found)
// Get translation for a string that this locale doesn't have
s := funLocale.TrString("incorrect_root_url")
found = lang1.HasKey("no-such")
assert.False(t, found)
assert.Equal(t, "no-such", lang1.TrString("no-such"))
require.NoError(t, ls.Close())
}
func TestLocaleStoreMoreSource(t *testing.T) {
testData1 := []byte(`
a=11
b=12
`)
testData2 := []byte(`
b=21
c=22
`)
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, testData1, testData2))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "11", lang1.TrString("a"))
assert.Equal(t, "21", lang1.TrString("b"))
assert.Equal(t, "22", lang1.TrString("c"))
}
type stringerPointerReceiver struct {
s string
}
func (s *stringerPointerReceiver) String() string {
return s.s
}
type stringerStructReceiver struct {
s string
}
func (s stringerStructReceiver) String() string {
return s.s
}
type errorStructReceiver struct {
s string
}
func (e errorStructReceiver) Error() string {
return e.s
}
type errorPointerReceiver struct {
s string
}
func (e *errorPointerReceiver) Error() string {
return e.s
}
func TestLocaleWithTemplate(t *testing.T) {
ls := NewLocaleStore()
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, []byte(`key=<a>%s</a>`), nil))
lang1, _ := ls.Locale("lang1")
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
cases := []struct {
in any
want string
}{
{"<str>", "<a>&lt;str&gt;</a>"},
{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
{template.HTML("<html>"), "<a><html></a>"},
{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</a>"},
}
buf := &strings.Builder{}
for _, c := range cases {
buf.Reset()
require.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
assert.Equal(t, c.want, buf.String())
}
}
func TestLocaleStoreQuirks(t *testing.T) {
const nl = "\n"
q := func(q1, s string, q2 ...string) string {
return q1 + s + strings.Join(q2, "")
}
testDataList := []struct {
in string
out string
hint string
}{
{` xx`, `xx`, "simple, no quote"},
{`" xx"`, ` xx`, "simple, double-quote"},
{`' xx'`, ` xx`, "simple, single-quote"},
{"` xx`", ` xx`, "simple, back-quote"},
{`x\"y`, `x\"y`, "no unescape, simple"},
{q(`"`, `x\"y`, `"`), `"x\"y"`, "unescape, double-quote"},
{q(`'`, `x\"y`, `'`), `x\"y`, "no unescape, single-quote"},
{q("`", `x\"y`, "`"), `x\"y`, "no unescape, back-quote"},
{q(`"`, `x\"y`) + nl + "b=", `"x\"y`, "half open, double-quote"},
{q(`'`, `x\"y`) + nl + "b=", `'x\"y`, "half open, single-quote"},
{q("`", `x\"y`) + nl + "b=`", `x\"y` + nl + "b=", "half open, back-quote, multi-line"},
{`x ; y`, `x ; y`, "inline comment (;)"},
{`x # y`, `x # y`, "inline comment (#)"},
{`x \; y`, `x ; y`, `inline comment (\;)`},
{`x \# y`, `x # y`, `inline comment (\#)`},
}
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", nil, nil, []byte("a="+testData.in), nil)
lang1, _ := ls.Locale("lang1")
require.NoError(t, err, testData.hint)
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
require.NoError(t, ls.Close())
}
// TODO: Crowdin needs the strings to be quoted correctly and doesn't like incomplete quotes
// and Crowdin always outputs quoted strings if there are quotes in the strings.
// So, Gitea's `key="quoted" unquoted` content shouldn't be used on Crowdin directly,
// it should be converted to `key="\"quoted\" unquoted"` first.
// TODO: We can not use UnescapeValueDoubleQuotes=true, because there are a lot of back-quotes in en-US.ini,
// then Crowdin will output:
// > key = "`x \" y`"
// Then Gitea will read a string with back-quotes, which is incorrect.
// TODO: Crowdin might generate multi-line strings, quoted by double-quote, it's not supported by LocaleStore
// LocaleStore uses back-quote for multi-line strings, it's not supported by Crowdin.
// TODO: Crowdin doesn't support back-quote as string quoter, it mainly uses double-quote
// so, the following line will be parsed as: value="`first", comment="second`" on Crowdin
// > a = `first; second`
// Verify fallback to English
assert.True(t, strings.HasPrefix(s, "This Forgejo instance..."))
}

View file

@ -0,0 +1,120 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package translation
import (
"testing"
"forgejo.org/modules/translation/i18n"
"github.com/stretchr/testify/assert"
)
func TestGetPluralRule(t *testing.T) {
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja"))
assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn"))
assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is"))
assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil"))
assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs"))
assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru"))
assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl"))
assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv"))
assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca"))
assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl"))
assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid"))
}
func TestApplyPluralRule(t *testing.T) {
testCases := []struct {
expect i18n.PluralFormIndex
pluralRule int
values []int64
}{
{i18n.PluralFormOne, PluralRuleDefault, []int64{1}},
{i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}},
{i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}},
{i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}},
{i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}},
{i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}},
{i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}},
{i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}},
{i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}},
{i18n.PluralFormOne, PluralRuleCzech, []int64{1}},
{i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}},
{i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}},
{i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}},
{i18n.PluralFormOne, PluralRulePolish, []int64{1}},
{i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}},
{i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}},
{i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}},
{i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}},
{i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}},
{i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}},
{i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}},
{i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleCatalan, []int64{1}},
{i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}},
{i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}},
{i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}},
{i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}},
{i18n.PluralFormZero, PluralRuleArabic, []int64{0}},
{i18n.PluralFormOne, PluralRuleArabic, []int64{1}},
{i18n.PluralFormTwo, PluralRuleArabic, []int64{2}},
{i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}},
{i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}},
{i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}},
}
for _, tc := range testCases {
for _, n := range tc.values {
assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n)
}
}
}

View file

@ -1,4 +1,5 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package translation

View file

@ -1,10 +1,9 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package translation
// TODO: make this package friendly to testing
import (
"testing"
@ -48,111 +47,3 @@ func TestPrettyNumber(t *testing.T) {
assert.Equal(t, "1,000,000", l.PrettyNumber(1000000))
assert.Equal(t, "1,000,000.1", l.PrettyNumber(1000000.1))
}
func TestGetPluralRule(t *testing.T) {
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh"))
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja"))
assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn"))
assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is"))
assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil"))
assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs"))
assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru"))
assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl"))
assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv"))
assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca"))
assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl"))
assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar"))
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT"))
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR"))
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid"))
}
func TestApplyPluralRule(t *testing.T) {
testCases := []struct {
expect i18n.PluralFormIndex
pluralRule int
values []int64
}{
{i18n.PluralFormOne, PluralRuleDefault, []int64{1}},
{i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}},
{i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}},
{i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}},
{i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}},
{i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}},
{i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}},
{i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}},
{i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}},
{i18n.PluralFormOne, PluralRuleCzech, []int64{1}},
{i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}},
{i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}},
{i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}},
{i18n.PluralFormOne, PluralRulePolish, []int64{1}},
{i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}},
{i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}},
{i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}},
{i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}},
{i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}},
{i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}},
{i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}},
{i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}},
{i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}},
{i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleCatalan, []int64{1}},
{i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}},
{i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}},
{i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}},
{i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}},
{i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}},
{i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}},
{i18n.PluralFormZero, PluralRuleArabic, []int64{0}},
{i18n.PluralFormOne, PluralRuleArabic, []int64{1}},
{i18n.PluralFormTwo, PluralRuleArabic, []int64{2}},
{i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}},
{i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}},
{i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}},
}
for _, tc := range testCases {
for _, n := range tc.values {
assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n)
}
}
}

View file

@ -12,7 +12,6 @@ import (
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/translation/i18n"
files_service "forgejo.org/services/repository/files"
"forgejo.org/tests"
@ -20,28 +19,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestMissingTranslationHandling(t *testing.T) {
// Currently new languages can only be added to localestore via AddLocaleByIni
// so this line is here to make the other one work. When INI locales are removed,
// it will not be needed by this test.
i18n.DefaultLocales.AddLocaleByIni("fun", "Funlang", nil, nil, []byte(""), nil)
// Add a testing locale to the store
i18n.DefaultLocales.AddToLocaleFromJSON("fun", []byte(`{
"meta.last_line": "This language only has one line that is never used by the UI. It will never have a translation for incorrect_root_url"
}`))
// Get "fun" locale, make sure it's available
funLocale, found := i18n.DefaultLocales.Locale("fun")
assert.True(t, found)
// Get translation for a string that this locale doesn't have
s := funLocale.TrString("incorrect_root_url")
// Verify fallback to English
assert.True(t, strings.HasPrefix(s, "This Forgejo instance"))
}
// TestDataSizeTranslation is a test for usage of TrSize in file size display
func TestDataSizeTranslation(t *testing.T) {
onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) {