From a2787bb09edcf00f29a65c4f405de6371b4d434e Mon Sep 17 00:00:00 2001 From: Benedikt Straub Date: Mon, 30 Dec 2024 20:03:37 +0100 Subject: [PATCH] Initial support for localization and pluralization with go-i18n-JSON-v2 format --- .deadcode-out | 1 + modules/translation/i18n/dummy.go | 20 +- modules/translation/i18n/errors.go | 6 +- modules/translation/i18n/i18n.go | 23 ++- modules/translation/i18n/i18n_test.go | 106 +++++++++- modules/translation/i18n/localestore.go | 180 +++++++++++++++-- modules/translation/mock.go | 4 + modules/translation/plural_rules.go | 253 ++++++++++++++++++++++++ modules/translation/translation.go | 16 +- modules/translation/translation_test.go | 108 ++++++++++ options/locale/locale_en-US.ini | 5 - options/locale_next/locale_ar.json | 1 + options/locale_next/locale_be.json | 1 + options/locale_next/locale_bg.json | 14 ++ options/locale_next/locale_bn.json | 1 + options/locale_next/locale_bs.json | 1 + options/locale_next/locale_ca.json | 5 + options/locale_next/locale_cs-CZ.json | 17 ++ options/locale_next/locale_da.json | 5 + options/locale_next/locale_de-DE.json | 17 ++ options/locale_next/locale_el-GR.json | 17 ++ options/locale_next/locale_en-US.json | 17 ++ options/locale_next/locale_eo.json | 5 + options/locale_next/locale_es-ES.json | 17 ++ options/locale_next/locale_et.json | 5 + options/locale_next/locale_fa-IR.json | 12 ++ options/locale_next/locale_fi-FI.json | 15 ++ options/locale_next/locale_fil.json | 17 ++ options/locale_next/locale_fr-FR.json | 17 ++ options/locale_next/locale_gl.json | 1 + options/locale_next/locale_hi.json | 1 + options/locale_next/locale_hu-HU.json | 15 ++ options/locale_next/locale_id-ID.json | 12 ++ options/locale_next/locale_is-IS.json | 9 + options/locale_next/locale_it-IT.json | 17 ++ options/locale_next/locale_ja-JP.json | 15 ++ options/locale_next/locale_ko-KR.json | 12 ++ options/locale_next/locale_lt.json | 5 + options/locale_next/locale_lv-LV.json | 17 ++ options/locale_next/locale_ml-IN.json | 1 + options/locale_next/locale_nb_NO.json | 1 + options/locale_next/locale_nds.json | 17 ++ options/locale_next/locale_nl-NL.json | 17 ++ options/locale_next/locale_pl-PL.json | 15 ++ options/locale_next/locale_pt-BR.json | 17 ++ options/locale_next/locale_pt-PT.json | 17 ++ options/locale_next/locale_ru-RU.json | 17 ++ options/locale_next/locale_si-LK.json | 12 ++ options/locale_next/locale_sk-SK.json | 1 + options/locale_next/locale_sl.json | 1 + options/locale_next/locale_sr-SP.json | 1 + options/locale_next/locale_sv-SE.json | 15 ++ options/locale_next/locale_tr-TR.json | 15 ++ options/locale_next/locale_uk-UA.json | 17 ++ options/locale_next/locale_vi.json | 1 + options/locale_next/locale_yi.json | 1 + options/locale_next/locale_zh-CN.json | 15 ++ options/locale_next/locale_zh-HK.json | 9 + options/locale_next/locale_zh-TW.json | 15 ++ templates/repo/issue/view_title.tmpl | 8 +- tools/migrate_locales.sh | 145 ++++++++++++++ 61 files changed, 1317 insertions(+), 51 deletions(-) create mode 100644 modules/translation/plural_rules.go create mode 100644 options/locale_next/locale_ar.json create mode 100644 options/locale_next/locale_be.json create mode 100644 options/locale_next/locale_bg.json create mode 100644 options/locale_next/locale_bn.json create mode 100644 options/locale_next/locale_bs.json create mode 100644 options/locale_next/locale_ca.json create mode 100644 options/locale_next/locale_cs-CZ.json create mode 100644 options/locale_next/locale_da.json create mode 100644 options/locale_next/locale_de-DE.json create mode 100644 options/locale_next/locale_el-GR.json create mode 100644 options/locale_next/locale_en-US.json create mode 100644 options/locale_next/locale_eo.json create mode 100644 options/locale_next/locale_es-ES.json create mode 100644 options/locale_next/locale_et.json create mode 100644 options/locale_next/locale_fa-IR.json create mode 100644 options/locale_next/locale_fi-FI.json create mode 100644 options/locale_next/locale_fil.json create mode 100644 options/locale_next/locale_fr-FR.json create mode 100644 options/locale_next/locale_gl.json create mode 100644 options/locale_next/locale_hi.json create mode 100644 options/locale_next/locale_hu-HU.json create mode 100644 options/locale_next/locale_id-ID.json create mode 100644 options/locale_next/locale_is-IS.json create mode 100644 options/locale_next/locale_it-IT.json create mode 100644 options/locale_next/locale_ja-JP.json create mode 100644 options/locale_next/locale_ko-KR.json create mode 100644 options/locale_next/locale_lt.json create mode 100644 options/locale_next/locale_lv-LV.json create mode 100644 options/locale_next/locale_ml-IN.json create mode 100644 options/locale_next/locale_nb_NO.json create mode 100644 options/locale_next/locale_nds.json create mode 100644 options/locale_next/locale_nl-NL.json create mode 100644 options/locale_next/locale_pl-PL.json create mode 100644 options/locale_next/locale_pt-BR.json create mode 100644 options/locale_next/locale_pt-PT.json create mode 100644 options/locale_next/locale_ru-RU.json create mode 100644 options/locale_next/locale_si-LK.json create mode 100644 options/locale_next/locale_sk-SK.json create mode 100644 options/locale_next/locale_sl.json create mode 100644 options/locale_next/locale_sr-SP.json create mode 100644 options/locale_next/locale_sv-SE.json create mode 100644 options/locale_next/locale_tr-TR.json create mode 100644 options/locale_next/locale_uk-UA.json create mode 100644 options/locale_next/locale_vi.json create mode 100644 options/locale_next/locale_yi.json create mode 100644 options/locale_next/locale_zh-CN.json create mode 100644 options/locale_next/locale_zh-HK.json create mode 100644 options/locale_next/locale_zh-TW.json create mode 100755 tools/migrate_locales.sh diff --git a/.deadcode-out b/.deadcode-out index 64741ec7ac..a44599b6f1 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -246,6 +246,7 @@ code.gitea.io/gitea/modules/translation MockLocale.TrString MockLocale.Tr MockLocale.TrN + MockLocale.TrPluralString MockLocale.TrSize MockLocale.PrettyNumber diff --git a/modules/translation/i18n/dummy.go b/modules/translation/i18n/dummy.go index fe15c250f4..861672c619 100644 --- a/modules/translation/i18n/dummy.go +++ b/modules/translation/i18n/dummy.go @@ -22,20 +22,7 @@ func (k *KeyLocale) HasKey(trKey string) bool { // TrHTML implements Locale. func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML { - args := slices.Clone(trArgs) - for i, v := range args { - switch v := v.(type) { - case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: - // for most basic types (including template.HTML which is safe), just do nothing and use it - case string: - args[i] = template.HTMLEscapeString(v) - case fmt.Stringer: - args[i] = template.HTMLEscapeString(v.String()) - default: - args[i] = template.HTMLEscapeString(fmt.Sprint(v)) - } - } - return template.HTML(k.TrString(trKey, args...)) + return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) } // TrString implements Locale. @@ -43,6 +30,11 @@ func (k *KeyLocale) TrString(trKey string, trArgs ...any) string { return FormatDummy(trKey, trArgs...) } +// TrPluralString implements Locale. +func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...)) +} + func FormatDummy(trKey string, args ...any) string { if len(args) == 0 { return fmt.Sprintf("(%s)", trKey) diff --git a/modules/translation/i18n/errors.go b/modules/translation/i18n/errors.go index 7f64ccf908..ee9436a8f7 100644 --- a/modules/translation/i18n/errors.go +++ b/modules/translation/i18n/errors.go @@ -8,6 +8,8 @@ import ( ) var ( - ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} - ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} + ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} + ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist} + ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist} + ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} ) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 1555cd961e..e447502a3b 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -8,11 +8,28 @@ import ( "io" ) +type ( + PluralFormIndex uint8 + PluralFormRule func(int64) PluralFormIndex +) + +const ( + PluralFormZero PluralFormIndex = iota + PluralFormOne + PluralFormTwo + PluralFormFew + PluralFormMany + PluralFormOther +) + var DefaultLocales = NewLocaleStore() type Locale interface { // TrString translates a given key and arguments for a language TrString(trKey string, trArgs ...any) string + // TrPluralString translates a given pluralized key and arguments for a language. + // This function returns an error if new-style support for the given key is not available. + TrPluralString(count any, trKey string, trArgs ...any) template.HTML // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML TrHTML(trKey string, trArgs ...any) template.HTML // HasKey reports if a locale has a translation for a given key @@ -31,8 +48,10 @@ type LocaleStore interface { Locale(langName string) (Locale, bool) // HasLang returns whether a given language is present in the store HasLang(langName string) bool - // AddLocaleByIni adds a new language to the store - AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error + // AddLocaleByIni adds a new old-style language to the store + AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error + // AddLocaleByJSON adds new-style content to an existing language to the store + AddToLocaleFromJSON(langName string, source []byte) error } // ResetDefaultLocales resets the current default locales diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index 244f6ffbb3..41f85931aa 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -12,6 +12,26 @@ import ( "github.com/stretchr/testify/require" ) +var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex { + if n == 0 { + return PluralFormZero + } + if n == 1 { + return PluralFormOne + } + if n >= 2 && n <= 4 { + return PluralFormFew + } + return PluralFormOther +} + +var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex { + if n == 1 { + return PluralFormOne + } + return PluralFormOther +} + func TestLocaleStore(t *testing.T) { testData1 := []byte(` .dot.name = Dot Name @@ -27,11 +47,48 @@ fmt = %[2]s %[1]s [section] sub = Changed Sub String +commits = fallback value for commits +`) + + testDataJSON2 := []byte(` +{ + "section.json": "the JSON is %s", + "section.commits": { + "one": "one %d commit", + "few": "some %d commits", + "other": "lots of %d commits" + }, + "section.incomplete": { + "few": "some %d objects (translated)" + }, + "nested": { + "outer": { + "inner": { + "json": "Hello World", + "issue": { + "one": "one %d issue", + "few": "some %d issues", + "other": "lots of %d issues" + } + } + } + } +} +`) + testDataJSON1 := []byte(` +{ + "section.incomplete": { + "one": "[untranslated] some %d object", + "other": "[untranslated] some %d objects" + } +} `) ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil)) - require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil)) + require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil)) + require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1)) + require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2)) ls.SetDefaultLang("lang1") lang1, _ := ls.Locale("lang1") @@ -56,6 +113,45 @@ sub = Changed Sub String result2 := lang2.TrHTML("section.mixed", "a&b") assert.EqualValues(t, `test value; a&b`, result2) + result = lang2.TrString("section.json", "valid") + assert.Equal(t, "the JSON is valid", result) + + result = lang2.TrString("nested.outer.inner.json") + assert.Equal(t, "Hello World", result) + + result = lang2.TrString("section.commits") + assert.Equal(t, "lots of %d commits", result) + + result2 = lang2.TrPluralString(1, "section.commits", 1) + assert.EqualValues(t, "one 1 commit", result2) + + result2 = lang2.TrPluralString(3, "section.commits", 3) + assert.EqualValues(t, "some 3 commits", result2) + + result2 = lang2.TrPluralString(8, "section.commits", 8) + assert.EqualValues(t, "lots of 8 commits", result2) + + result2 = lang2.TrPluralString(0, "section.commits") + assert.EqualValues(t, "section.commits", result2) + + result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1) + assert.EqualValues(t, "one 1 issue", result2) + + result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3) + assert.EqualValues(t, "some 3 issues", result2) + + result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9) + assert.EqualValues(t, "lots of 9 issues", result2) + + result2 = lang2.TrPluralString(3, "section.incomplete", 3) + assert.EqualValues(t, "some 3 objects (translated)", result2) + + result2 = lang2.TrPluralString(1, "section.incomplete", 1) + assert.EqualValues(t, "[untranslated] some 1 object", result2) + + 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) @@ -77,7 +173,7 @@ c=22 `) ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2)) lang1, _ := ls.Locale("lang1") assert.Equal(t, "11", lang1.TrString("a")) assert.Equal(t, "21", lang1.TrString("b")) @@ -118,7 +214,7 @@ func (e *errorPointerReceiver) Error() string { func TestLocaleWithTemplate(t *testing.T) { ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=%s`), nil)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=%s`), nil)) lang1, _ := ls.Locale("lang1") tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) @@ -181,7 +277,7 @@ func TestLocaleStoreQuirks(t *testing.T) { for _, testData := range testDataList { ls := NewLocaleStore() - err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) + err := ls.AddLocaleByIni("lang1", "Lang1", 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) diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index 0e6ddab401..e80b2592ae 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -8,8 +8,10 @@ import ( "html/template" "slices" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // This file implements the static LocaleStore that will not watch for changes @@ -18,6 +20,9 @@ type locale struct { store *localeStore langName string idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap + + newStyleMessages map[string]string + pluralRule PluralFormRule } var _ Locale = (*locale)(nil) @@ -38,8 +43,19 @@ func NewLocaleStore() LocaleStore { return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)} } +const ( + PluralFormSeparator string = "\036" +) + +// A note about pluralization rules. +// go-i18n supports plural rules in theory. +// In practice, it relies on another library that hardcodes a list of common languages +// and their plural rules, and does not support languages not hardcoded there. +// So we pretend that all languages are English and use our own function to extract +// the correct plural form for a given count and language. + // AddLocaleByIni adds locale by ini into the store -func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error { +func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error { if _, ok := store.localeMap[langName]; ok { return ErrLocaleAlreadyExist } @@ -47,7 +63,7 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more store.langNames = append(store.langNames, langName) store.langDescs = append(store.langDescs, langDesc) - l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)} + l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)} store.localeMap[l.langName] = l iniFile, err := setting.NewConfigProviderForLocale(source, moreSource) @@ -78,6 +94,98 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more return nil } +func RecursivelyAddTranslationsFromJSON(locale *locale, object map[string]any, prefix string) error { + for key, value := range object { + var fullkey string + if prefix != "" { + fullkey = prefix + "." + key + } else { + fullkey = key + } + + switch v := value.(type) { + case string: + // Check whether we are adding a plural form to the parent object, or a new nested JSON object. + + if key == "zero" || key == "one" || key == "two" || key == "few" || key == "many" { + locale.newStyleMessages[prefix+PluralFormSeparator+key] = v + } else if key == "other" { + locale.newStyleMessages[prefix] = v + } else { + locale.newStyleMessages[fullkey] = v + } + + case map[string]any: + err := RecursivelyAddTranslationsFromJSON(locale, v, fullkey) + if err != nil { + return err + } + + case nil: + default: + return fmt.Errorf("Unrecognized JSON value '%s'", value) + } + } + + return nil +} + +func (store *localeStore) AddToLocaleFromJSON(langName string, source []byte) error { + locale, ok := store.localeMap[langName] + if !ok { + return ErrLocaleDoesNotExist + } + + var result map[string]any + if err := json.Unmarshal(source, &result); err != nil { + return err + } + + return RecursivelyAddTranslationsFromJSON(locale, result, "") +} + +func (l *locale) LookupNewStyleMessage(trKey string) string { + if msg, ok := l.newStyleMessages[trKey]; ok { + return msg + } + return "" +} + +func (l *locale) LookupPlural(trKey string, count any) string { + n, err := util.ToInt64(count) + if err != nil { + log.Error("Invalid plural count '%s'", count) + return "" + } + + pluralForm := l.pluralRule(n) + suffix := "" + switch pluralForm { + case PluralFormZero: + suffix = PluralFormSeparator + "zero" + case PluralFormOne: + suffix = PluralFormSeparator + "one" + case PluralFormTwo: + suffix = PluralFormSeparator + "two" + case PluralFormFew: + suffix = PluralFormSeparator + "few" + case PluralFormMany: + suffix = PluralFormSeparator + "many" + case PluralFormOther: + // No suffix for the "other" string. + default: + log.Error("Invalid plural form index %d for count %d", pluralForm, count) + return "" + } + + if result, ok := l.newStyleMessages[trKey+suffix]; ok { + return result + } + + log.Error("Missing translation for plural form index %d for count %d", pluralForm, count) + return "" +} + func (store *localeStore) HasLang(langName string) bool { _, ok := store.localeMap[langName] return ok @@ -113,22 +221,37 @@ func (store *localeStore) Close() error { func (l *locale) TrString(trKey string, trArgs ...any) string { format := trKey - idx, ok := l.store.trKeyToIdxMap[trKey] - found := false - if ok { - if msg, ok := l.idxToMsgMap[idx]; ok { - format = msg // use the found translation - found = true - } else if def, ok := l.store.localeMap[l.store.defaultLang]; ok { - // try to use default locale's translation - if msg, ok := def.idxToMsgMap[idx]; ok { - format = msg + if msg := l.LookupNewStyleMessage(trKey); msg != "" { + format = msg + } else { + // First fallback: old-style translation + idx, ok := l.store.trKeyToIdxMap[trKey] + found := false + if ok { + if msg, ok := l.idxToMsgMap[idx]; ok { + format = msg // use the found translation found = true } } - } - if !found { - log.Error("Missing translation %q", trKey) + + if !found { + // Second fallback: new-style default language + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { + if msg := defaultLang.LookupNewStyleMessage(trKey); msg != "" { + format = msg + } else { + // Third fallback: old-style default language + if msg, ok := defaultLang.idxToMsgMap[idx]; ok { + format = msg + found = true + } + } + } + + if !found { + log.Error("Missing translation %q", trKey) + } + } } msg, err := Format(format, trArgs...) @@ -138,7 +261,7 @@ func (l *locale) TrString(trKey string, trArgs ...any) string { return msg } -func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { +func PrepareArgsForHTML(trArgs ...any) []any { args := slices.Clone(trArgs) for i, v := range args { switch v := v.(type) { @@ -152,7 +275,30 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(l.TrString(trKey, args...)) + return args +} + +func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { + return template.HTML(l.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) +} + +func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + message := l.LookupPlural(trKey, count) + + if message == "" { + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { + message = defaultLang.LookupPlural(trKey, count) + } + if message == "" { + message = trKey + } + } + + message, err := Format(message, PrepareArgsForHTML(trArgs...)...) + if err != nil { + log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err) + } + return template.HTML(message) } // HasKey returns whether a key is present in this locale or not diff --git a/modules/translation/mock.go b/modules/translation/mock.go index fe3a1502ea..4d9acce26f 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -31,6 +31,10 @@ func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return template.HTML(key1) } +func (l MockLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + return template.HTML(trKey) +} + func (l MockLocale) TrSize(s int64) ReadableSize { return ReadableSize{fmt.Sprint(s), ""} } diff --git a/modules/translation/plural_rules.go b/modules/translation/plural_rules.go new file mode 100644 index 0000000000..b8c00ceef7 --- /dev/null +++ b/modules/translation/plural_rules.go @@ -0,0 +1,253 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Some useful links: +// https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html +// https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information +// https://github.com/WeblateOrg/language-data/blob/main/languages.csv +// Note that in some cases there is ambiguity about the correct form for a given language. In this case, ask the locale's translators. + +package translation + +import ( + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/translation/i18n" +) + +// The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync! +const ( + PluralRuleDefault = 0 + PluralRuleBengali = 1 + PluralRuleIcelandic = 2 + PluralRuleFilipino = 3 + PluralRuleOneForm = 4 + PluralRuleCzech = 5 + PluralRuleRussian = 6 + PluralRulePolish = 7 + PluralRuleLatvian = 8 + PluralRuleLithuanian = 9 + PluralRuleFrench = 10 + PluralRuleCatalan = 11 + PluralRuleSlovenian = 12 + PluralRuleArabic = 13 +) + +func GetPluralRuleImpl(langName string) int { + // First, check for languages with country-specific plural rules. + switch langName { + case "pt-BR": + return PluralRuleFrench + + case "pt-PT": + return PluralRuleCatalan + + default: + break + } + + // Remove the country portion of the locale name. + langName = strings.Split(strings.Split(langName, "_")[0], "-")[0] + + // When adding a new language not in the list, add its plural rule definition here. + switch langName { + case "en", "aa", "ab", "abr", "ada", "ae", "aeb", "af", "afh", "aii", "ain", "akk", "ale", "aln", "alt", "ami", "an", "ang", "anp", "apc", "arc", "arp", "arq", "arw", "arz", "asa", "ast", "av", "avk", "awa", "ayc", "az", "azb", "ba", "bal", "ban", "bar", "bas", "bbc", "bci", "bej", "bem", "ber", "bew", "bez", "bg", "bgc", "bgn", "bhb", "bhi", "bi", "bik", "bin", "bjj", "bjn", "bla", "bnt", "bqi", "bra", "brb", "brh", "brx", "bua", "bug", "bum", "byn", "cad", "cak", "car", "ce", "cgg", "ch", "chb", "chg", "chk", "chm", "chn", "cho", "chp", "chr", "chy", "ckb", "co", "cop", "cpe", "cpf", "cr", "crp", "cu", "cv", "da", "dak", "dar", "dcc", "de", "del", "den", "dgr", "din", "dje", "dnj", "dnk", "dru", "dry", "dua", "dum", "dv", "dyu", "ee", "efi", "egl", "egy", "eka", "el", "elx", "enm", "eo", "et", "eu", "ewo", "ext", "fan", "fat", "fbl", "ffm", "fi", "fj", "fo", "fon", "frk", "frm", "fro", "frr", "frs", "fuq", "fur", "fuv", "fvr", "fy", "gaa", "gay", "gba", "gbm", "gez", "gil", "gl", "glk", "gmh", "gn", "goh", "gom", "gon", "gor", "got", "grb", "gsw", "guc", "gum", "gur", "guz", "gwi", "ha", "hai", "haw", "haz", "hil", "hit", "hmn", "hnd", "hne", "hno", "ho", "hoc", "hoj", "hrx", "ht", "hu", "hup", "hus", "hz", "ia", "iba", "ibb", "ie", "ik", "ilo", "inh", "io", "jam", "jgo", "jmc", "jpr", "jrb", "ka", "kaa", "kac", "kaj", "kam", "kaw", "kbd", "kcg", "kfr", "kfy", "kg", "kha", "khn", "kho", "ki", "kj", "kk", "kkj", "kl", "kln", "kmb", "kmr", "kok", "kpe", "kr", "krc", "kri", "krl", "kru", "ks", "ksb", "ku", "kum", "kut", "kv", "kxm", "ky", "la", "lad", "laj", "lam", "lb", "lez", "lfn", "lg", "li", "lij", "ljp", "lki", "lmn", "lmo", "lol", "loz", "lrc", "lu", "lua", "lui", "lun", "luo", "lus", "luy", "luz", "mad", "mag", "mai", "mak", "man", "mas", "mdf", "mdh", "mdr", "men", "mer", "mfa", "mga", "mgh", "mgo", "mh", "mhr", "mic", "min", "mjw", "ml", "mn", "mnc", "mni", "mnw", "moe", "moh", "mos", "mr", "mrh", "mtr", "mus", "mwk", "mwl", "mwr", "mxc", "myv", "myx", "mzn", "na", "nah", "nap", "nb", "nd", "ndc", "nds", "ne", "new", "ng", "ngl", "nia", "nij", "niu", "nl", "nn", "nnh", "nod", "noe", "nog", "non", "nr", "nuk", "nv", "nwc", "ny", "nym", "nyn", "nyo", "nzi", "oj", "om", "or", "os", "ota", "otk", "ovd", "pag", "pal", "pam", "pap", "pau", "pbb", "pdt", "peo", "phn", "pi", "pms", "pon", "pro", "ps", "pwn", "qu", "quc", "qug", "qya", "raj", "rap", "rar", "rcf", "rej", "rhg", "rif", "rkt", "rm", "rmt", "rn", "rng", "rof", "rom", "rue", "rup", "rw", "rwk", "sad", "sai", "sam", "saq", "sas", "sc", "sck", "sco", "sd", "sdh", "sef", "seh", "sel", "sga", "sgn", "sgs", "shn", "sid", "sjd", "skr", "sm", "sml", "sn", "snk", "so", "sog", "sou", "sq", "srn", "srr", "ss", "ssy", "st", "suk", "sus", "sux", "sv", "sw", "swg", "swv", "sxu", "syc", "syl", "syr", "szy", "ta", "tay", "tcy", "te", "tem", "teo", "ter", "tet", "tig", "tiv", "tk", "tkl", "tli", "tly", "tmh", "tn", "tog", "tr", "trv", "ts", "tsg", "tsi", "tsj", "tts", "tum", "tvl", "tw", "ty", "tyv", "tzj", "tzl", "udm", "ug", "uga", "umb", "und", "unr", "ur", "uz", "vai", "ve", "vls", "vmf", "vmw", "vo", "vot", "vro", "vun", "wae", "wal", "war", "was", "wbq", "wbr", "wep", "wtm", "xal", "xh", "xnr", "xog", "yao", "yap", "yi", "yua", "za", "zap", "zbl", "zen", "zgh", "zun", "zza": + return PluralRuleDefault + + case "ach", "ady", "ak", "am", "arn", "as", "bh", "bho", "bn", "csw", "doi", "fa", "ff", "frc", "frp", "gu", "gug", "gun", "guw", "hi", "hy", "kab", "kn", "ln", "mfe", "mg", "mi", "mia", "nso", "oc", "pa", "pcm", "pt", "qdt", "qtp", "si", "tg", "ti", "wa", "zu": + return PluralRuleBengali + + case "is": + return PluralRuleIcelandic + + case "fil": + return PluralRuleFilipino + + case "ace", "ay", "bm", "bo", "cdo", "cpx", "crh", "dz", "gan", "hak", "hnj", "hsn", "id", "ig", "ii", "ja", "jbo", "jv", "kde", "kea", "km", "ko", "kos", "lkt", "lo", "lzh", "ms", "my", "nan", "nqo", "osa", "sah", "ses", "sg", "son", "su", "th", "tlh", "to", "tok", "tpi", "tt", "vi", "wo", "wuu", "yo", "yue", "zh": + return PluralRuleOneForm + + case "cpp", "cs", "sk": + return PluralRuleCzech + + case "be", "bs", "cnr", "hr", "ru", "sr", "uk", "wen": + return PluralRuleRussian + + case "csb", "pl", "szl": + return PluralRulePolish + + case "lv", "prg": + return PluralRuleLatvian + + case "lt": + return PluralRuleLithuanian + + case "fr": + return PluralRuleFrench + + case "ca", "es", "it": + return PluralRuleCatalan + + case "sl": + return PluralRuleSlovenian + + case "ar": + return PluralRuleArabic + + default: + break + } + + log.Error("No plural rule defined for language %s", langName) + return PluralRuleDefault +} + +var PluralRules = []i18n.PluralFormRule{ + // [ 0] Common 2-form, e.g. English, German + func(n int64) i18n.PluralFormIndex { + if n != 1 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 1] Bengali + func(n int64) i18n.PluralFormIndex { + if n > 1 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 2] Icelandic + func(n int64) i18n.PluralFormIndex { + if n%10 != 1 || n%100 == 11 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 3] Filipino + func(n int64) i18n.PluralFormIndex { + if n != 1 && n != 2 && n != 3 && (n%10 == 4 || n%10 == 6 || n%10 == 9) { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 4] OneForm + func(n int64) i18n.PluralFormIndex { + return i18n.PluralFormOther + }, + + // [ 5] Czech + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n >= 2 && n <= 4 { + return i18n.PluralFormFew + } + return i18n.PluralFormOther + }, + + // [ 6] Russian + func(n int64) i18n.PluralFormIndex { + if n%10 == 1 && n%100 != 11 { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [ 7] Polish + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [ 8] Latvian + func(n int64) i18n.PluralFormIndex { + if n%10 == 0 || n%100 >= 11 && n%100 <= 19 { + return i18n.PluralFormZero + } + if n%10 == 1 && n%100 != 11 { + return i18n.PluralFormOne + } + return i18n.PluralFormOther + }, + + // [ 9] Lithuanian + func(n int64) i18n.PluralFormIndex { + if n%10 == 1 && (n%100 < 11 || n%100 > 19) { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 9 && (n%100 < 11 || n%100 > 19) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [10] French + func(n int64) i18n.PluralFormIndex { + if n == 0 || n == 1 { + return i18n.PluralFormOne + } + if n != 0 && n%1000000 == 0 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, + + // [11] Catalan + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n != 0 && n%1000000 == 0 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, + + // [12] Slovenian + func(n int64) i18n.PluralFormIndex { + if n%100 == 1 { + return i18n.PluralFormOne + } + if n%100 == 2 { + return i18n.PluralFormTwo + } + if n%100 == 3 || n%100 == 4 { + return i18n.PluralFormFew + } + return i18n.PluralFormOther + }, + + // [13] Arabic + func(n int64) i18n.PluralFormIndex { + if n == 0 { + return i18n.PluralFormZero + } + if n == 1 { + return i18n.PluralFormOne + } + if n == 2 { + return i18n.PluralFormTwo + } + if n%100 >= 3 && n%100 <= 10 { + return i18n.PluralFormFew + } + if n%100 >= 11 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, +} diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 6687d3d817..7d1c627c84 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -32,6 +32,9 @@ type Locale interface { TrString(string, ...any) string Tr(key string, args ...any) template.HTML + // New-style pluralized strings + TrPluralString(count any, trKey string, trArgs ...any) template.HTML + // Old-style pseudo-pluralized strings, deprecated TrN(cnt any, key1, keyN string, args ...any) template.HTML TrSize(size int64) ReadableSize @@ -100,8 +103,17 @@ func InitLocales(ctx context.Context) { } key := "locale_" + setting.Langs[i] + ".ini" - if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil { - log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) + if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil { + log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err) + } + + key = "locale_next/locale_" + setting.Langs[i] + ".json" + if bytes, err := options.AssetFS().ReadFile(key); err == nil { + if err = i18n.DefaultLocales.AddToLocaleFromJSON(setting.Langs[i], bytes); err != nil { + log.Error("Failed to add new-style messages to %s: %v", setting.Langs[i], err) + } + } else { + log.Error("Failed to open new-style messages for %s: %v", setting.Langs[i], err) } } if len(setting.Langs) != 0 { diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go index bffbb155ca..5b3eefb355 100644 --- a/modules/translation/translation_test.go +++ b/modules/translation/translation_test.go @@ -48,3 +48,111 @@ func TestPrettyNumber(t *testing.T) { assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000)) assert.EqualValues(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/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eaeab11a9d..127e629e80 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -190,7 +190,6 @@ commit_kind = Search commits... runner_kind = Search runners... no_results = No matching results found. issue_kind = Search issues... -milestone_kind = Search milestones... pull_kind = Search pulls... keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator. @@ -1887,10 +1886,6 @@ pulls.nothing_to_compare_have_tag = The selected branch/tag are equal. pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` pulls.create = Create pull request -pulls.title_desc_one = wants to merge %[1]d commit from %[2]s into %[3]s -pulls.title_desc_few = wants to merge %[1]d commits from %[2]s into %[3]s -pulls.merged_title_desc_one = merged %[1]d commit from %[2]s into %[3]s %[4]s -pulls.merged_title_desc_few = merged %[1]d commits from %[2]s into %[3]s %[4]s pulls.change_target_branch_at = `changed target branch from %s to %s %s` pulls.tab_conversation = Conversation pulls.tab_commits = Commits diff --git a/options/locale_next/locale_ar.json b/options/locale_next/locale_ar.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_ar.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_be.json b/options/locale_next/locale_be.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_be.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_bg.json b/options/locale_next/locale_bg.json new file mode 100644 index 0000000000..bec72c556a --- /dev/null +++ b/options/locale_next/locale_bg.json @@ -0,0 +1,14 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "сля %[1]d подаване от %[2]s в %[3]s %[4]s", + "other": "сля %[1]d подавания от %[2]s в %[3]s %[4]s" + }, + "title_desc": { + "one": "иска да слее %[1]d подаване от %[2]s в %[3]s", + "other": "иска да слее %[1]d подавания от %[2]s в %[3]s" + } + } + } +} diff --git a/options/locale_next/locale_bn.json b/options/locale_next/locale_bn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_bn.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_bs.json b/options/locale_next/locale_bs.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_bs.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_ca.json b/options/locale_next/locale_ca.json new file mode 100644 index 0000000000..8aee80092d --- /dev/null +++ b/options/locale_next/locale_ca.json @@ -0,0 +1,5 @@ +{ + "search": { + "milestone_kind": "Cerca fites..." + } +} diff --git a/options/locale_next/locale_cs-CZ.json b/options/locale_next/locale_cs-CZ.json new file mode 100644 index 0000000000..373b9dc31e --- /dev/null +++ b/options/locale_next/locale_cs-CZ.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "sloučil %[1]d commit z %[2]s do %[3]s %[4]s", + "other": "sloučil %[1]d commity z větve %[2]s do větve %[3]s před %[4]s" + }, + "title_desc": { + "one": "žádá o sloučení %[1]d commitu z %[2]s do %[3]s", + "other": "chce sloučit %[1]d commity z větve %[2]s do %[3]s" + } + } + }, + "search": { + "milestone_kind": "Hledat milníky..." + } +} diff --git a/options/locale_next/locale_da.json b/options/locale_next/locale_da.json new file mode 100644 index 0000000000..834f66024f --- /dev/null +++ b/options/locale_next/locale_da.json @@ -0,0 +1,5 @@ +{ + "search": { + "milestone_kind": "Søg milepæle..." + } +} diff --git a/options/locale_next/locale_de-DE.json b/options/locale_next/locale_de-DE.json new file mode 100644 index 0000000000..82e4ea54d4 --- /dev/null +++ b/options/locale_next/locale_de-DE.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "hat %[1]d Commit von %[2]s nach %[3]s %[4]s zusammengeführt", + "other": "hat %[1]d Commits von %[2]s nach %[3]s %[4]s zusammengeführt" + }, + "title_desc": { + "one": "möchte %[1]d Commit von %[2]s nach %[3]s zusammenführen", + "other": "möchte %[1]d Commits von %[2]s nach %[3]s zusammenführen" + } + } + }, + "search": { + "milestone_kind": "Meilensteine suchen …" + } +} diff --git a/options/locale_next/locale_el-GR.json b/options/locale_next/locale_el-GR.json new file mode 100644 index 0000000000..9fa112cf0f --- /dev/null +++ b/options/locale_next/locale_el-GR.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "συγχώνευσε %[1]d υποβολή από τον κλάδο %[2]s στον κλάδο %[3]s %[4]s", + "other": "συγχώνευσε %[1]d υποβολές από %[2]s σε %[3]s %[4]s" + }, + "title_desc": { + "one": ": θα ήθελε να συγχωνεύσει %[1]d υποβολή από τον κλάδο %[2]s στον κλάδο %[3]s", + "other": "θέλει να συγχωνεύσει %[1]d υποβολές από %[2]s σε %[3]s" + } + } + }, + "search": { + "milestone_kind": "Αναζήτηση ορόσημων..." + } +} diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json new file mode 100644 index 0000000000..64e3e50abc --- /dev/null +++ b/options/locale_next/locale_en-US.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "merged %[1]d commit from %[2]s into %[3]s %[4]s", + "other": "merged %[1]d commits from %[2]s into %[3]s %[4]s" + }, + "title_desc": { + "one": "wants to merge %[1]d commit from %[2]s into %[3]s", + "other": "wants to merge %[1]d commits from %[2]s into %[3]s" + } + } + }, + "search": { + "milestone_kind": "Search milestones..." + } +} diff --git a/options/locale_next/locale_eo.json b/options/locale_next/locale_eo.json new file mode 100644 index 0000000000..c57b462d5f --- /dev/null +++ b/options/locale_next/locale_eo.json @@ -0,0 +1,5 @@ +{ + "search": { + "milestone_kind": "Serĉi celojn..." + } +} diff --git a/options/locale_next/locale_es-ES.json b/options/locale_next/locale_es-ES.json new file mode 100644 index 0000000000..7313cdc0a7 --- /dev/null +++ b/options/locale_next/locale_es-ES.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "fusionó %[1]d commit de %[2]s en %[3]s %[4]s", + "other": "fusionó %[1]d commits de %[2]s en %[3]s %[4]s" + }, + "title_desc": { + "one": "quiere fusionar %[1]d commit de %[2]s en %[3]s", + "other": "quiere fusionar %[1]d commits de %[2]s en %[3]s" + } + } + }, + "search": { + "milestone_kind": "Buscar hitos…" + } +} diff --git a/options/locale_next/locale_et.json b/options/locale_next/locale_et.json new file mode 100644 index 0000000000..ac009856db --- /dev/null +++ b/options/locale_next/locale_et.json @@ -0,0 +1,5 @@ +{ + "search": { + "milestone_kind": "Otsi verstapostid..." + } +} diff --git a/options/locale_next/locale_fa-IR.json b/options/locale_next/locale_fa-IR.json new file mode 100644 index 0000000000..6964db4934 --- /dev/null +++ b/options/locale_next/locale_fa-IR.json @@ -0,0 +1,12 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "%[1]d کامیت ادغام شده از %[2]s به %[3]s %[4]s" + }, + "title_desc": { + "other": "قصد ادغام %[1]d تغییر را از %[2]s به %[3]s دارد" + } + } + } +} diff --git a/options/locale_next/locale_fi-FI.json b/options/locale_next/locale_fi-FI.json new file mode 100644 index 0000000000..88a2110bcb --- /dev/null +++ b/options/locale_next/locale_fi-FI.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "yhdistetty %[1]d committia lähteestä %[2]s kohteeseen %[3]s %[4]s" + }, + "title_desc": { + "other": "haluaa yhdistää %[1]d committia lähteestä %[2]s kohteeseen %[3]s" + } + } + }, + "search": { + "milestone_kind": "Etsi merkkipaaluja..." + } +} diff --git a/options/locale_next/locale_fil.json b/options/locale_next/locale_fil.json new file mode 100644 index 0000000000..6be27dbb8c --- /dev/null +++ b/options/locale_next/locale_fil.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "isinali ang %[1]d commit mula%[2]s patungong %[3]s %[4]s", + "other": "isinali ang %[1]d mga commit mula sa %[2]s patungong %[3]s %[4]s" + }, + "title_desc": { + "one": "hinihiling na isama ang %[1]d commit mula %[2]s patungong %[3]s", + "other": "hiniling na isama ang %[1]d mga commit mula sa %[2]s patungong %[3]s" + } + } + }, + "search": { + "milestone_kind": "Maghanap ng mga milestone…" + } +} diff --git a/options/locale_next/locale_fr-FR.json b/options/locale_next/locale_fr-FR.json new file mode 100644 index 0000000000..995a2be8bb --- /dev/null +++ b/options/locale_next/locale_fr-FR.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "fusionné %[1]d commit depuis %[2]s vers %[3]s %[4]s", + "other": "a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s" + }, + "title_desc": { + "one": "veut fusionner %[1]d commit depuis %[2]s vers %[3]s", + "other": "souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s" + } + } + }, + "search": { + "milestone_kind": "Recherche dans les jalons..." + } +} diff --git a/options/locale_next/locale_gl.json b/options/locale_next/locale_gl.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_gl.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_hi.json b/options/locale_next/locale_hi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_hi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_hu-HU.json b/options/locale_next/locale_hu-HU.json new file mode 100644 index 0000000000..2a21d68095 --- /dev/null +++ b/options/locale_next/locale_hu-HU.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "egyesítve %[1]d változás(ok) a %[2]s-ból %[3]s-ba %[4]s" + }, + "title_desc": { + "other": "egyesíteni szeretné %[1]d változás(oka)t a(z) %[2]s-ból %[3]s-ba" + } + } + }, + "search": { + "milestone_kind": "Mérföldkövek keresése..." + } +} diff --git a/options/locale_next/locale_id-ID.json b/options/locale_next/locale_id-ID.json new file mode 100644 index 0000000000..13580f3cfe --- /dev/null +++ b/options/locale_next/locale_id-ID.json @@ -0,0 +1,12 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "commit %[1]d telah digabungkan dari %[2]s menjadi %[3]s %[4]s" + }, + "title_desc": { + "other": "ingin menggabungkan komit %[1]d dari %[2]s menuju %[3]s" + } + } + } +} diff --git a/options/locale_next/locale_is-IS.json b/options/locale_next/locale_is-IS.json new file mode 100644 index 0000000000..40d5a7e7aa --- /dev/null +++ b/options/locale_next/locale_is-IS.json @@ -0,0 +1,9 @@ +{ + "repo": { + "pulls": { + "title_desc": { + "other": "vill sameina %[1]d framlög frá %[2]s í %[3]s" + } + } + } +} diff --git a/options/locale_next/locale_it-IT.json b/options/locale_next/locale_it-IT.json new file mode 100644 index 0000000000..61cb012433 --- /dev/null +++ b/options/locale_next/locale_it-IT.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "ha fuso %[1]d commit da %[2]s in %[3]s %[4]s", + "other": "ha unito %[1]d commit da %[2]s a %[3]s %[4]s" + }, + "title_desc": { + "one": "vuole fondere %[1]d commit da %[2]s in %[3]s", + "other": "vuole unire %[1]d commit da %[2]s a %[3]s" + } + } + }, + "search": { + "milestone_kind": "Ricerca tappe..." + } +} diff --git a/options/locale_next/locale_ja-JP.json b/options/locale_next/locale_ja-JP.json new file mode 100644 index 0000000000..447ee8ae22 --- /dev/null +++ b/options/locale_next/locale_ja-JP.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "が %[1]d 個のコミットを %[2]s から %[3]s へマージ %[4]s" + }, + "title_desc": { + "other": "が %[2]s から %[3]s への %[1]d コミットのマージを希望しています" + } + } + }, + "search": { + "milestone_kind": "マイルストーンを検索..." + } +} diff --git a/options/locale_next/locale_ko-KR.json b/options/locale_next/locale_ko-KR.json new file mode 100644 index 0000000000..1beaec4627 --- /dev/null +++ b/options/locale_next/locale_ko-KR.json @@ -0,0 +1,12 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "님이 %[2]s 에서 %[3]s 로 %[1]d 커밋을 %[4]s 병합함" + }, + "title_desc": { + "other": "%[2]s 에서 %[3]s 로 %[1]d개의 커밋들을 병합하려함" + } + } + } +} diff --git a/options/locale_next/locale_lt.json b/options/locale_next/locale_lt.json new file mode 100644 index 0000000000..cce5782546 --- /dev/null +++ b/options/locale_next/locale_lt.json @@ -0,0 +1,5 @@ +{ + "search": { + "milestone_kind": "Ieškoti gairių..." + } +} diff --git a/options/locale_next/locale_lv-LV.json b/options/locale_next/locale_lv-LV.json new file mode 100644 index 0000000000..e338556aab --- /dev/null +++ b/options/locale_next/locale_lv-LV.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "iekļāva %[1]d iesūtījumu no %[2]s %[3]s %[4]s", + "other": "Iekļāva %[1]d iesūtījumus no %[2]s zarā %[3]s %[4]s" + }, + "title_desc": { + "one": "vēlas iekļaut %[1]d iesūtījumu no %[2]s %[3]s", + "other": "vēlas iekļaut %[1]d iesūtījumus no %[2]s zarā %[3]s" + } + } + }, + "search": { + "milestone_kind": "Meklēt atskaites punktus..." + } +} diff --git a/options/locale_next/locale_ml-IN.json b/options/locale_next/locale_ml-IN.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_ml-IN.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_nb_NO.json b/options/locale_next/locale_nb_NO.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_nb_NO.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_nds.json b/options/locale_next/locale_nds.json new file mode 100644 index 0000000000..a141362816 --- /dev/null +++ b/options/locale_next/locale_nds.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "hett %[1]d Kommitteren vun %[2]s na %[3]s %[4]s tosamenföhrt", + "other": "hett %[1]d Kommitterens vun %[2]s na %[3]s %[4]s tosamenföhrt" + }, + "title_desc": { + "one": "will %[1]d Kommitteren vun %[2]s na %[3]s tosamenföhren", + "other": "will %[1]d Kommitterens vun %[2]s na %[3]s tosamenföhren" + } + } + }, + "search": { + "milestone_kind": "In Markstenen söken …" + } +} diff --git a/options/locale_next/locale_nl-NL.json b/options/locale_next/locale_nl-NL.json new file mode 100644 index 0000000000..9bbfc0fecd --- /dev/null +++ b/options/locale_next/locale_nl-NL.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "heeft %[1]d commit van %[2]s samengevoegd in %[3]s %[4]s", + "other": "heeft %[1]d commits samengevoegd van %[2]s naar %[3]s %[4]s" + }, + "title_desc": { + "one": "wilt %[1]d commit van %[2]s samenvoegen in %[3]s", + "other": "wilt %[1]d commits van %[2]s samenvoegen met %[3]s" + } + } + }, + "search": { + "milestone_kind": "Zoek mijlpalen..." + } +} diff --git a/options/locale_next/locale_pl-PL.json b/options/locale_next/locale_pl-PL.json new file mode 100644 index 0000000000..1f2def3ea0 --- /dev/null +++ b/options/locale_next/locale_pl-PL.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "many": "scala %[1]d commity/ów z %[2]s do %[3]s %[4]s" + }, + "title_desc": { + "many": "chce scalić %[1]d commity/ów z %[2]s do %[3]s" + } + } + }, + "search": { + "milestone_kind": "Wyszukaj kamienie milowe..." + } +} diff --git a/options/locale_next/locale_pt-BR.json b/options/locale_next/locale_pt-BR.json new file mode 100644 index 0000000000..e7758ef1bf --- /dev/null +++ b/options/locale_next/locale_pt-BR.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "mesclou %[1]d commit de %[2]s em %[3]s %[4]s", + "other": "mesclou %[1]d commits de %[2]s em %[3]s %[4]s" + }, + "title_desc": { + "one": "quer mesclar %[1]d commit de %[2]s em %[3]s", + "other": "quer mesclar %[1]d commits de %[2]s em %[3]s" + } + } + }, + "search": { + "milestone_kind": "Pesquisar marcos..." + } +} diff --git a/options/locale_next/locale_pt-PT.json b/options/locale_next/locale_pt-PT.json new file mode 100644 index 0000000000..475023d461 --- /dev/null +++ b/options/locale_next/locale_pt-PT.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "integrou %[1]d cometimento do ramo %[2]s no ramo %[3]s %[4]s", + "other": "integrou %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s %[4]s" + }, + "title_desc": { + "one": "quer integrar %[1]d cometimento do ramo %[2]s no ramo %[3]s", + "other": "quer integrar %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s" + } + } + }, + "search": { + "milestone_kind": "Procurar etapas..." + } +} diff --git a/options/locale_next/locale_ru-RU.json b/options/locale_next/locale_ru-RU.json new file mode 100644 index 0000000000..de310505a3 --- /dev/null +++ b/options/locale_next/locale_ru-RU.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "слит %[1]d коммит из %[2]s в %[3]s %[4]s", + "many": "слито %[1]d коммит(ов) из %[2]s в %[3]s %[4]s" + }, + "title_desc": { + "one": "хочет влить %[1]d коммит из %[2]s в %[3]s", + "many": "хочет влить %[1]d коммит(ов) из %[2]s в %[3]s" + } + } + }, + "search": { + "milestone_kind": "Найти этапы..." + } +} diff --git a/options/locale_next/locale_si-LK.json b/options/locale_next/locale_si-LK.json new file mode 100644 index 0000000000..25e149f9b8 --- /dev/null +++ b/options/locale_next/locale_si-LK.json @@ -0,0 +1,12 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "මර්ජ්%[1]d සිට %[2]s දක්වා %[3]s %[4]s" + }, + "title_desc": { + "other": "%[1]d සිට %[2]s දක්වා %[3]s" + } + } + } +} diff --git a/options/locale_next/locale_sk-SK.json b/options/locale_next/locale_sk-SK.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sk-SK.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sl.json b/options/locale_next/locale_sl.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sl.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sr-SP.json b/options/locale_next/locale_sr-SP.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sr-SP.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sv-SE.json b/options/locale_next/locale_sv-SE.json new file mode 100644 index 0000000000..fe68f161c2 --- /dev/null +++ b/options/locale_next/locale_sv-SE.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "sammanfogade %[1]d incheckningar från %[2]s in i %[3]s %[4]s" + }, + "title_desc": { + "other": "vill sammanfoga %[1]d incheckningar från s[2]s in i %[3]s" + } + } + }, + "search": { + "milestone_kind": "Sök milstolpar..." + } +} diff --git a/options/locale_next/locale_tr-TR.json b/options/locale_next/locale_tr-TR.json new file mode 100644 index 0000000000..ef2cdb6584 --- /dev/null +++ b/options/locale_next/locale_tr-TR.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "%[4]s %[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirdi" + }, + "title_desc": { + "other": "%[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirmek istiyor" + } + } + }, + "search": { + "milestone_kind": "Kilometre taşlarını ara..." + } +} diff --git a/options/locale_next/locale_uk-UA.json b/options/locale_next/locale_uk-UA.json new file mode 100644 index 0000000000..aded9786b4 --- /dev/null +++ b/options/locale_next/locale_uk-UA.json @@ -0,0 +1,17 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "one": "об'єднав %[1]d коміт з %[2]s в %[3]s %[4]s", + "many": "об'єднав %[1]d комітів з %[2]s в %[3]s %[4]s" + }, + "title_desc": { + "one": "хоче об'єднати %[1]d коміт з %[2]s в %[3]s", + "many": "хоче об'єднати %[1]d комітів з %[2]s в %[3]s" + } + } + }, + "search": { + "milestone_kind": "Шукати віхи..." + } +} diff --git a/options/locale_next/locale_vi.json b/options/locale_next/locale_vi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_vi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_yi.json b/options/locale_next/locale_yi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_yi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_zh-CN.json b/options/locale_next/locale_zh-CN.json new file mode 100644 index 0000000000..091b3fe609 --- /dev/null +++ b/options/locale_next/locale_zh-CN.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "于 %[4]s 将 %[1]d 次代码提交从 %[2]s合并至 %[3]s" + }, + "title_desc": { + "other": "请求将 %[1]d 次代码提交从 %[2]s 合并至 %[3]s" + } + } + }, + "search": { + "milestone_kind": "搜索里程碑…" + } +} diff --git a/options/locale_next/locale_zh-HK.json b/options/locale_next/locale_zh-HK.json new file mode 100644 index 0000000000..dd7b954559 --- /dev/null +++ b/options/locale_next/locale_zh-HK.json @@ -0,0 +1,9 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "於 %[4]s 將 %[1]d 次代碼提交從 %[2]s合併至 %[3]s" + } + } + } +} diff --git a/options/locale_next/locale_zh-TW.json b/options/locale_next/locale_zh-TW.json new file mode 100644 index 0000000000..4d31a713c0 --- /dev/null +++ b/options/locale_next/locale_zh-TW.json @@ -0,0 +1,15 @@ +{ + "repo": { + "pulls": { + "merged_title_desc": { + "other": "將 %[1]d 次提交從 %[2]s 合併至 %[3]s %[4]s" + }, + "title_desc": { + "other": "請求將 %[1]d 次程式碼提交從 %[2]s 合併至 %[3]s" + } + } + }, + "search": { + "milestone_kind": "搜尋里程碑..." + } +} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 5e30cf3684..936df9d3d2 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -63,10 +63,10 @@ {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} {{if .Issue.OriginalAuthor}} {{.Issue.OriginalAuthor}} - {{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} {{else}} {{.Issue.PullRequest.Merger.GetDisplayName}} - {{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} {{end}} {{if .MadeUsingAGit}} {{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}} @@ -79,11 +79,11 @@ {{end}} {{else}} {{if .Issue.OriginalAuthor}} - {{.Issue.OriginalAuthor}} {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}} + {{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} {{else}} {{.Issue.Poster.GetDisplayName}} - {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} {{end}} {{if .MadeUsingAGit}} diff --git a/tools/migrate_locales.sh b/tools/migrate_locales.sh new file mode 100755 index 0000000000..f02fe702cc --- /dev/null +++ b/tools/migrate_locales.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# Copyright 2024 The Forgejo Authors. All rights reserved. +# SPDX-License-Identifier: MIT + +if [ -z "$1" ] || [ -z "$2" ] +then + echo "USAGE: $0 section key [key1 [keyN]]" + exit 1 +fi + +if ! [ -d ../options/locale_next ] +then + echo 'Call this script from the `tools` directory.' + exit 1 +fi + +destsection="$1" +keyJSON="$destsection.$2" +key1="" +keyN="" +if [ -n "$3" ] +then + key1="$3" +else + key1="$2" +fi +if [ -n "$4" ] +then + keyN="$4" +fi + +cd ../options/locale + +# Migrate the string in one file. +function process() { + file="$1" + exec 3<$file + + val1="" + valN="" + cursection="" + line1=0 + lineN=0 + lineNumber=0 + + # Parse the file + while read -u 3 line + do + ((++lineNumber)) + if [[ $line =~ ^\[[-._a-zA-Z0-9]+\]$ ]] + then + cursection="${line#[}" + cursection="${cursection%]}" + elif [ "$cursection" = "$destsection" ] + then + key="${line%%=*}" + value="${line#*=}" + key="$(echo $key)" # Trim leading/trailing whitespace + value="$(echo $value)" + + if [ "$key" = "$key1" ] + then + val1="$value" + line1=$lineNumber + fi + if [ -n "$keyN" ] && [ "$key" = "$keyN" ] + then + valN="$value" + lineN=$lineNumber + fi + + if [ -n "$val1" ] && ( [ -n "$valN" ] || [ -z "$keyN" ] ) + then + # Found all desired strings + break + fi + fi + done + + if [ -n "$val1" ] || [ -n "$valN" ] + then + localename="${file#locale_}" + localename="${localename%.ini}" + localename="${localename%-*}" + + if [ "$file" = "locale_en-US.ini" ] + then + # Delete migrated string from source file + if [ $line1 -gt 0 ] && [ $lineN -gt 0 ] && [ $lineN -ne $line1 ] + then + sed -i "${line1}d;${lineN}d" "$file" + elif [ $line1 -gt 0 ] + then + sed -i "${line1}d" "$file" + elif [ $lineN -gt 0 ] + then + sed -i "${lineN}d" "$file" + fi + fi + + # Write JSON + jsonfile="../locale_next/${file/.ini/.json}" + + pluralform="other" + oneform="one" + case $localename in + "be" | "bs" | "cnr" | "csb" | "hr" | "lt" | "pl" | "ru" | "sr" | "szl" | "uk" | "wen") + # These languages have no "other" form and use "many" instead. + pluralform="many" + ;; + "ace" | "ay" | "bm" | "bo" | "cdo" | "cpx" | "crh" | "dz" | "gan" | "hak" | "hnj" | "hsn" | "id" | "ig" | "ii" | "ja" | "jbo" | "jv" | "kde" | "kea" | "km" | "ko" | "kos" | "lkt" | "lo" | "lzh" | "ms" | "my" | "nan" | "nqo" | "osa" | "sah" | "ses" | "sg" | "son" | "su" | "th" | "tlh" | "to" | "tok" | "tpi" | "tt" | "vi" | "wo" | "wuu" | "yo" | "yue" | "zh") + # These languages have no singular form. + oneform="" + ;; + *) + ;; + esac + + content="" + if [ -z "$keyN" ] + then + content="$(jq --arg val "$val1" ".$keyJSON = \$val" < "$jsonfile")" + else + object='{}' + if [ -n "$val1" ] && [ -n "$oneform" ] + then + object=$(jq --arg val "$val1" ".$oneform = \$val" <<< "$object") + fi + if [ -n "$valN" ] + then + object=$(jq --arg val "$valN" ".$pluralform = \$val" <<< "$object") + fi + content="$(jq --argjson val "$object" ".$keyJSON = \$val" < "$jsonfile")" + fi + jq . <<< "$content" > "$jsonfile" + fi +} + +for file in *.ini +do + process "$file" & +done +wait +