diff --git a/build/lint-locale-usage/bin/lint-locale-usage.go b/build/lint-locale-usage/bin/lint-locale-usage.go
index d9cdf9f321..311e632461 100644
--- a/build/lint-locale-usage/bin/lint-locale-usage.go
+++ b/build/lint-locale-usage/bin/lint-locale-usage.go
@@ -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))
diff --git a/modules/translation/i18n/i18n_ini_test.go b/modules/translation/i18n/i18n_ini_test.go
new file mode 100644
index 0000000000..64c5d167c0
--- /dev/null
+++ b/modules/translation/i18n/i18n_ini_test.go
@@ -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; %s
+`)
+
+ 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; a&b`, 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=%s`), 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>"},
+ {[]byte(""), "[60 98 121 116 101 115 62]"},
+ {template.HTML(""), ""},
+ {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"},
+ {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"},
+ {stringerStructReceiver{""}, "<stringerStructReceiver>"},
+ {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"},
+ {errorStructReceiver{""}, "<errorStructReceiver>"},
+ {&errorStructReceiver{""}, "<errorStructReceiver ptr>"},
+ {errorPointerReceiver{""}, "{<errorPointerReceiver>}"},
+ {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"},
+ }
+
+ 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`
+}
diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index ac086d75d9..8dfdb36127 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -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; %s
-`)
-
- 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; a&b`, 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=%s`), 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>"},
- {[]byte(""), "[60 98 121 116 101 115 62]"},
- {template.HTML(""), ""},
- {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"},
- {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"},
- {stringerStructReceiver{""}, "<stringerStructReceiver>"},
- {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"},
- {errorStructReceiver{""}, "<errorStructReceiver>"},
- {&errorStructReceiver{""}, "<errorStructReceiver ptr>"},
- {errorPointerReceiver{""}, "{<errorPointerReceiver>}"},
- {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"},
- }
-
- 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..."))
}
diff --git a/modules/translation/plural_rules_test.go b/modules/translation/plural_rules_test.go
new file mode 100644
index 0000000000..d3a9a15905
--- /dev/null
+++ b/modules/translation/plural_rules_test.go
@@ -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)
+ }
+ }
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index 42441115af..fdd71fe608 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -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
diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go
index 7584490941..34b1ebf91f 100644
--- a/modules/translation/translation_test.go
+++ b/modules/translation/translation_test.go
@@ -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)
- }
- }
-}
diff --git a/tests/integration/translations_test.go b/tests/integration/translations_test.go
index d20355aca2..b65c9c2a67 100644
--- a/tests/integration/translations_test.go
+++ b/tests/integration/translations_test.go
@@ -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) {