From 973ff28f44e6df8cc943308ca2d3e24d4e858ecc Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Tue, 23 Dec 2025 04:39:26 +0100 Subject: [PATCH] chore(test): separate and move around i18n testing (#10539) 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 --- .../bin/lint-locale-usage.go | 2 +- modules/translation/i18n/i18n_ini_test.go | 206 ++++++++++++++++ modules/translation/i18n/i18n_test.go | 232 +++--------------- modules/translation/plural_rules_test.go | 120 +++++++++ modules/translation/translation.go | 1 + modules/translation/translation_test.go | 111 +-------- tests/integration/translations_test.go | 23 -- 7 files changed, 365 insertions(+), 330 deletions(-) create mode 100644 modules/translation/i18n/i18n_ini_test.go create mode 100644 modules/translation/plural_rules_test.go 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) {