loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial support for localization and pluralization with go-i18n-JSON-v2 format

+1317 -51
+1
.deadcode-out
··· 246 246 MockLocale.TrString 247 247 MockLocale.Tr 248 248 MockLocale.TrN 249 + MockLocale.TrPluralString 249 250 MockLocale.TrSize 250 251 MockLocale.PrettyNumber 251 252
+6 -14
modules/translation/i18n/dummy.go
··· 22 22 23 23 // TrHTML implements Locale. 24 24 func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML { 25 - args := slices.Clone(trArgs) 26 - for i, v := range args { 27 - switch v := v.(type) { 28 - case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: 29 - // for most basic types (including template.HTML which is safe), just do nothing and use it 30 - case string: 31 - args[i] = template.HTMLEscapeString(v) 32 - case fmt.Stringer: 33 - args[i] = template.HTMLEscapeString(v.String()) 34 - default: 35 - args[i] = template.HTMLEscapeString(fmt.Sprint(v)) 36 - } 37 - } 38 - return template.HTML(k.TrString(trKey, args...)) 25 + return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) 39 26 } 40 27 41 28 // TrString implements Locale. 42 29 func (k *KeyLocale) TrString(trKey string, trArgs ...any) string { 43 30 return FormatDummy(trKey, trArgs...) 31 + } 32 + 33 + // TrPluralString implements Locale. 34 + func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { 35 + return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...)) 44 36 } 45 37 46 38 func FormatDummy(trKey string, args ...any) string {
+4 -2
modules/translation/i18n/errors.go
··· 8 8 ) 9 9 10 10 var ( 11 - ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} 12 - ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} 11 + ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} 12 + ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist} 13 + ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist} 14 + ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} 13 15 )
+21 -2
modules/translation/i18n/i18n.go
··· 8 8 "io" 9 9 ) 10 10 11 + type ( 12 + PluralFormIndex uint8 13 + PluralFormRule func(int64) PluralFormIndex 14 + ) 15 + 16 + const ( 17 + PluralFormZero PluralFormIndex = iota 18 + PluralFormOne 19 + PluralFormTwo 20 + PluralFormFew 21 + PluralFormMany 22 + PluralFormOther 23 + ) 24 + 11 25 var DefaultLocales = NewLocaleStore() 12 26 13 27 type Locale interface { 14 28 // TrString translates a given key and arguments for a language 15 29 TrString(trKey string, trArgs ...any) string 30 + // TrPluralString translates a given pluralized key and arguments for a language. 31 + // This function returns an error if new-style support for the given key is not available. 32 + TrPluralString(count any, trKey string, trArgs ...any) template.HTML 16 33 // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML 17 34 TrHTML(trKey string, trArgs ...any) template.HTML 18 35 // HasKey reports if a locale has a translation for a given key ··· 31 48 Locale(langName string) (Locale, bool) 32 49 // HasLang returns whether a given language is present in the store 33 50 HasLang(langName string) bool 34 - // AddLocaleByIni adds a new language to the store 35 - AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error 51 + // AddLocaleByIni adds a new old-style language to the store 52 + AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error 53 + // AddLocaleByJSON adds new-style content to an existing language to the store 54 + AddToLocaleFromJSON(langName string, source []byte) error 36 55 } 37 56 38 57 // ResetDefaultLocales resets the current default locales
+101 -5
modules/translation/i18n/i18n_test.go
··· 12 12 "github.com/stretchr/testify/require" 13 13 ) 14 14 15 + var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex { 16 + if n == 0 { 17 + return PluralFormZero 18 + } 19 + if n == 1 { 20 + return PluralFormOne 21 + } 22 + if n >= 2 && n <= 4 { 23 + return PluralFormFew 24 + } 25 + return PluralFormOther 26 + } 27 + 28 + var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex { 29 + if n == 1 { 30 + return PluralFormOne 31 + } 32 + return PluralFormOther 33 + } 34 + 15 35 func TestLocaleStore(t *testing.T) { 16 36 testData1 := []byte(` 17 37 .dot.name = Dot Name ··· 27 47 28 48 [section] 29 49 sub = Changed Sub String 50 + commits = fallback value for commits 51 + `) 52 + 53 + testDataJSON2 := []byte(` 54 + { 55 + "section.json": "the JSON is %s", 56 + "section.commits": { 57 + "one": "one %d commit", 58 + "few": "some %d commits", 59 + "other": "lots of %d commits" 60 + }, 61 + "section.incomplete": { 62 + "few": "some %d objects (translated)" 63 + }, 64 + "nested": { 65 + "outer": { 66 + "inner": { 67 + "json": "Hello World", 68 + "issue": { 69 + "one": "one %d issue", 70 + "few": "some %d issues", 71 + "other": "lots of %d issues" 72 + } 73 + } 74 + } 75 + } 76 + } 77 + `) 78 + testDataJSON1 := []byte(` 79 + { 80 + "section.incomplete": { 81 + "one": "[untranslated] some %d object", 82 + "other": "[untranslated] some %d objects" 83 + } 84 + } 30 85 `) 31 86 32 87 ls := NewLocaleStore() 33 - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil)) 34 - require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) 88 + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil)) 89 + require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil)) 90 + require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1)) 91 + require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2)) 35 92 ls.SetDefaultLang("lang1") 36 93 37 94 lang1, _ := ls.Locale("lang1") ··· 56 113 result2 := lang2.TrHTML("section.mixed", "a&b") 57 114 assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2) 58 115 116 + result = lang2.TrString("section.json", "valid") 117 + assert.Equal(t, "the JSON is valid", result) 118 + 119 + result = lang2.TrString("nested.outer.inner.json") 120 + assert.Equal(t, "Hello World", result) 121 + 122 + result = lang2.TrString("section.commits") 123 + assert.Equal(t, "lots of %d commits", result) 124 + 125 + result2 = lang2.TrPluralString(1, "section.commits", 1) 126 + assert.EqualValues(t, "one 1 commit", result2) 127 + 128 + result2 = lang2.TrPluralString(3, "section.commits", 3) 129 + assert.EqualValues(t, "some 3 commits", result2) 130 + 131 + result2 = lang2.TrPluralString(8, "section.commits", 8) 132 + assert.EqualValues(t, "lots of 8 commits", result2) 133 + 134 + result2 = lang2.TrPluralString(0, "section.commits") 135 + assert.EqualValues(t, "section.commits", result2) 136 + 137 + result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1) 138 + assert.EqualValues(t, "one 1 issue", result2) 139 + 140 + result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3) 141 + assert.EqualValues(t, "some 3 issues", result2) 142 + 143 + result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9) 144 + assert.EqualValues(t, "lots of 9 issues", result2) 145 + 146 + result2 = lang2.TrPluralString(3, "section.incomplete", 3) 147 + assert.EqualValues(t, "some 3 objects (translated)", result2) 148 + 149 + result2 = lang2.TrPluralString(1, "section.incomplete", 1) 150 + assert.EqualValues(t, "[untranslated] some 1 object", result2) 151 + 152 + result2 = lang2.TrPluralString(7, "section.incomplete", 7) 153 + assert.EqualValues(t, "[untranslated] some 7 objects", result2) 154 + 59 155 langs, descs := ls.ListLangNameDesc() 60 156 assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) 61 157 assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) ··· 77 173 `) 78 174 79 175 ls := NewLocaleStore() 80 - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) 176 + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2)) 81 177 lang1, _ := ls.Locale("lang1") 82 178 assert.Equal(t, "11", lang1.TrString("a")) 83 179 assert.Equal(t, "21", lang1.TrString("b")) ··· 118 214 119 215 func TestLocaleWithTemplate(t *testing.T) { 120 216 ls := NewLocaleStore() 121 - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil)) 217 + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), nil)) 122 218 lang1, _ := ls.Locale("lang1") 123 219 124 220 tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) ··· 181 277 182 278 for _, testData := range testDataList { 183 279 ls := NewLocaleStore() 184 - err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) 280 + err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil) 185 281 lang1, _ := ls.Locale("lang1") 186 282 require.NoError(t, err, testData.hint) 187 283 assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
+163 -17
modules/translation/i18n/localestore.go
··· 8 8 "html/template" 9 9 "slices" 10 10 11 + "code.gitea.io/gitea/modules/json" 11 12 "code.gitea.io/gitea/modules/log" 12 13 "code.gitea.io/gitea/modules/setting" 14 + "code.gitea.io/gitea/modules/util" 13 15 ) 14 16 15 17 // This file implements the static LocaleStore that will not watch for changes ··· 18 20 store *localeStore 19 21 langName string 20 22 idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap 23 + 24 + newStyleMessages map[string]string 25 + pluralRule PluralFormRule 21 26 } 22 27 23 28 var _ Locale = (*locale)(nil) ··· 38 43 return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)} 39 44 } 40 45 46 + const ( 47 + PluralFormSeparator string = "\036" 48 + ) 49 + 50 + // A note about pluralization rules. 51 + // go-i18n supports plural rules in theory. 52 + // In practice, it relies on another library that hardcodes a list of common languages 53 + // and their plural rules, and does not support languages not hardcoded there. 54 + // So we pretend that all languages are English and use our own function to extract 55 + // the correct plural form for a given count and language. 56 + 41 57 // AddLocaleByIni adds locale by ini into the store 42 - func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error { 58 + func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error { 43 59 if _, ok := store.localeMap[langName]; ok { 44 60 return ErrLocaleAlreadyExist 45 61 } ··· 47 63 store.langNames = append(store.langNames, langName) 48 64 store.langDescs = append(store.langDescs, langDesc) 49 65 50 - l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)} 66 + l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)} 51 67 store.localeMap[l.langName] = l 52 68 53 69 iniFile, err := setting.NewConfigProviderForLocale(source, moreSource) ··· 78 94 return nil 79 95 } 80 96 97 + func RecursivelyAddTranslationsFromJSON(locale *locale, object map[string]any, prefix string) error { 98 + for key, value := range object { 99 + var fullkey string 100 + if prefix != "" { 101 + fullkey = prefix + "." + key 102 + } else { 103 + fullkey = key 104 + } 105 + 106 + switch v := value.(type) { 107 + case string: 108 + // Check whether we are adding a plural form to the parent object, or a new nested JSON object. 109 + 110 + if key == "zero" || key == "one" || key == "two" || key == "few" || key == "many" { 111 + locale.newStyleMessages[prefix+PluralFormSeparator+key] = v 112 + } else if key == "other" { 113 + locale.newStyleMessages[prefix] = v 114 + } else { 115 + locale.newStyleMessages[fullkey] = v 116 + } 117 + 118 + case map[string]any: 119 + err := RecursivelyAddTranslationsFromJSON(locale, v, fullkey) 120 + if err != nil { 121 + return err 122 + } 123 + 124 + case nil: 125 + default: 126 + return fmt.Errorf("Unrecognized JSON value '%s'", value) 127 + } 128 + } 129 + 130 + return nil 131 + } 132 + 133 + func (store *localeStore) AddToLocaleFromJSON(langName string, source []byte) error { 134 + locale, ok := store.localeMap[langName] 135 + if !ok { 136 + return ErrLocaleDoesNotExist 137 + } 138 + 139 + var result map[string]any 140 + if err := json.Unmarshal(source, &result); err != nil { 141 + return err 142 + } 143 + 144 + return RecursivelyAddTranslationsFromJSON(locale, result, "") 145 + } 146 + 147 + func (l *locale) LookupNewStyleMessage(trKey string) string { 148 + if msg, ok := l.newStyleMessages[trKey]; ok { 149 + return msg 150 + } 151 + return "" 152 + } 153 + 154 + func (l *locale) LookupPlural(trKey string, count any) string { 155 + n, err := util.ToInt64(count) 156 + if err != nil { 157 + log.Error("Invalid plural count '%s'", count) 158 + return "" 159 + } 160 + 161 + pluralForm := l.pluralRule(n) 162 + suffix := "" 163 + switch pluralForm { 164 + case PluralFormZero: 165 + suffix = PluralFormSeparator + "zero" 166 + case PluralFormOne: 167 + suffix = PluralFormSeparator + "one" 168 + case PluralFormTwo: 169 + suffix = PluralFormSeparator + "two" 170 + case PluralFormFew: 171 + suffix = PluralFormSeparator + "few" 172 + case PluralFormMany: 173 + suffix = PluralFormSeparator + "many" 174 + case PluralFormOther: 175 + // No suffix for the "other" string. 176 + default: 177 + log.Error("Invalid plural form index %d for count %d", pluralForm, count) 178 + return "" 179 + } 180 + 181 + if result, ok := l.newStyleMessages[trKey+suffix]; ok { 182 + return result 183 + } 184 + 185 + log.Error("Missing translation for plural form index %d for count %d", pluralForm, count) 186 + return "" 187 + } 188 + 81 189 func (store *localeStore) HasLang(langName string) bool { 82 190 _, ok := store.localeMap[langName] 83 191 return ok ··· 113 221 func (l *locale) TrString(trKey string, trArgs ...any) string { 114 222 format := trKey 115 223 116 - idx, ok := l.store.trKeyToIdxMap[trKey] 117 - found := false 118 - if ok { 119 - if msg, ok := l.idxToMsgMap[idx]; ok { 120 - format = msg // use the found translation 121 - found = true 122 - } else if def, ok := l.store.localeMap[l.store.defaultLang]; ok { 123 - // try to use default locale's translation 124 - if msg, ok := def.idxToMsgMap[idx]; ok { 125 - format = msg 224 + if msg := l.LookupNewStyleMessage(trKey); msg != "" { 225 + format = msg 226 + } else { 227 + // First fallback: old-style translation 228 + idx, ok := l.store.trKeyToIdxMap[trKey] 229 + found := false 230 + if ok { 231 + if msg, ok := l.idxToMsgMap[idx]; ok { 232 + format = msg // use the found translation 126 233 found = true 127 234 } 128 235 } 129 - } 130 - if !found { 131 - log.Error("Missing translation %q", trKey) 236 + 237 + if !found { 238 + // Second fallback: new-style default language 239 + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { 240 + if msg := defaultLang.LookupNewStyleMessage(trKey); msg != "" { 241 + format = msg 242 + } else { 243 + // Third fallback: old-style default language 244 + if msg, ok := defaultLang.idxToMsgMap[idx]; ok { 245 + format = msg 246 + found = true 247 + } 248 + } 249 + } 250 + 251 + if !found { 252 + log.Error("Missing translation %q", trKey) 253 + } 254 + } 132 255 } 133 256 134 257 msg, err := Format(format, trArgs...) ··· 138 261 return msg 139 262 } 140 263 141 - func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { 264 + func PrepareArgsForHTML(trArgs ...any) []any { 142 265 args := slices.Clone(trArgs) 143 266 for i, v := range args { 144 267 switch v := v.(type) { ··· 152 275 args[i] = template.HTMLEscapeString(fmt.Sprint(v)) 153 276 } 154 277 } 155 - return template.HTML(l.TrString(trKey, args...)) 278 + return args 279 + } 280 + 281 + func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { 282 + return template.HTML(l.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) 283 + } 284 + 285 + func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { 286 + message := l.LookupPlural(trKey, count) 287 + 288 + if message == "" { 289 + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { 290 + message = defaultLang.LookupPlural(trKey, count) 291 + } 292 + if message == "" { 293 + message = trKey 294 + } 295 + } 296 + 297 + message, err := Format(message, PrepareArgsForHTML(trArgs...)...) 298 + if err != nil { 299 + log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err) 300 + } 301 + return template.HTML(message) 156 302 } 157 303 158 304 // HasKey returns whether a key is present in this locale or not
+4
modules/translation/mock.go
··· 31 31 return template.HTML(key1) 32 32 } 33 33 34 + func (l MockLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { 35 + return template.HTML(trKey) 36 + } 37 + 34 38 func (l MockLocale) TrSize(s int64) ReadableSize { 35 39 return ReadableSize{fmt.Sprint(s), ""} 36 40 }
+253
modules/translation/plural_rules.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + // Some useful links: 5 + // https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html 6 + // https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information 7 + // https://github.com/WeblateOrg/language-data/blob/main/languages.csv 8 + // Note that in some cases there is ambiguity about the correct form for a given language. In this case, ask the locale's translators. 9 + 10 + package translation 11 + 12 + import ( 13 + "strings" 14 + 15 + "code.gitea.io/gitea/modules/log" 16 + "code.gitea.io/gitea/modules/translation/i18n" 17 + ) 18 + 19 + // The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync! 20 + const ( 21 + PluralRuleDefault = 0 22 + PluralRuleBengali = 1 23 + PluralRuleIcelandic = 2 24 + PluralRuleFilipino = 3 25 + PluralRuleOneForm = 4 26 + PluralRuleCzech = 5 27 + PluralRuleRussian = 6 28 + PluralRulePolish = 7 29 + PluralRuleLatvian = 8 30 + PluralRuleLithuanian = 9 31 + PluralRuleFrench = 10 32 + PluralRuleCatalan = 11 33 + PluralRuleSlovenian = 12 34 + PluralRuleArabic = 13 35 + ) 36 + 37 + func GetPluralRuleImpl(langName string) int { 38 + // First, check for languages with country-specific plural rules. 39 + switch langName { 40 + case "pt-BR": 41 + return PluralRuleFrench 42 + 43 + case "pt-PT": 44 + return PluralRuleCatalan 45 + 46 + default: 47 + break 48 + } 49 + 50 + // Remove the country portion of the locale name. 51 + langName = strings.Split(strings.Split(langName, "_")[0], "-")[0] 52 + 53 + // When adding a new language not in the list, add its plural rule definition here. 54 + switch langName { 55 + 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": 56 + return PluralRuleDefault 57 + 58 + 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": 59 + return PluralRuleBengali 60 + 61 + case "is": 62 + return PluralRuleIcelandic 63 + 64 + case "fil": 65 + return PluralRuleFilipino 66 + 67 + 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": 68 + return PluralRuleOneForm 69 + 70 + case "cpp", "cs", "sk": 71 + return PluralRuleCzech 72 + 73 + case "be", "bs", "cnr", "hr", "ru", "sr", "uk", "wen": 74 + return PluralRuleRussian 75 + 76 + case "csb", "pl", "szl": 77 + return PluralRulePolish 78 + 79 + case "lv", "prg": 80 + return PluralRuleLatvian 81 + 82 + case "lt": 83 + return PluralRuleLithuanian 84 + 85 + case "fr": 86 + return PluralRuleFrench 87 + 88 + case "ca", "es", "it": 89 + return PluralRuleCatalan 90 + 91 + case "sl": 92 + return PluralRuleSlovenian 93 + 94 + case "ar": 95 + return PluralRuleArabic 96 + 97 + default: 98 + break 99 + } 100 + 101 + log.Error("No plural rule defined for language %s", langName) 102 + return PluralRuleDefault 103 + } 104 + 105 + var PluralRules = []i18n.PluralFormRule{ 106 + // [ 0] Common 2-form, e.g. English, German 107 + func(n int64) i18n.PluralFormIndex { 108 + if n != 1 { 109 + return i18n.PluralFormOther 110 + } 111 + return i18n.PluralFormOne 112 + }, 113 + 114 + // [ 1] Bengali 115 + func(n int64) i18n.PluralFormIndex { 116 + if n > 1 { 117 + return i18n.PluralFormOther 118 + } 119 + return i18n.PluralFormOne 120 + }, 121 + 122 + // [ 2] Icelandic 123 + func(n int64) i18n.PluralFormIndex { 124 + if n%10 != 1 || n%100 == 11 { 125 + return i18n.PluralFormOther 126 + } 127 + return i18n.PluralFormOne 128 + }, 129 + 130 + // [ 3] Filipino 131 + func(n int64) i18n.PluralFormIndex { 132 + if n != 1 && n != 2 && n != 3 && (n%10 == 4 || n%10 == 6 || n%10 == 9) { 133 + return i18n.PluralFormOther 134 + } 135 + return i18n.PluralFormOne 136 + }, 137 + 138 + // [ 4] OneForm 139 + func(n int64) i18n.PluralFormIndex { 140 + return i18n.PluralFormOther 141 + }, 142 + 143 + // [ 5] Czech 144 + func(n int64) i18n.PluralFormIndex { 145 + if n == 1 { 146 + return i18n.PluralFormOne 147 + } 148 + if n >= 2 && n <= 4 { 149 + return i18n.PluralFormFew 150 + } 151 + return i18n.PluralFormOther 152 + }, 153 + 154 + // [ 6] Russian 155 + func(n int64) i18n.PluralFormIndex { 156 + if n%10 == 1 && n%100 != 11 { 157 + return i18n.PluralFormOne 158 + } 159 + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { 160 + return i18n.PluralFormFew 161 + } 162 + return i18n.PluralFormMany 163 + }, 164 + 165 + // [ 7] Polish 166 + func(n int64) i18n.PluralFormIndex { 167 + if n == 1 { 168 + return i18n.PluralFormOne 169 + } 170 + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { 171 + return i18n.PluralFormFew 172 + } 173 + return i18n.PluralFormMany 174 + }, 175 + 176 + // [ 8] Latvian 177 + func(n int64) i18n.PluralFormIndex { 178 + if n%10 == 0 || n%100 >= 11 && n%100 <= 19 { 179 + return i18n.PluralFormZero 180 + } 181 + if n%10 == 1 && n%100 != 11 { 182 + return i18n.PluralFormOne 183 + } 184 + return i18n.PluralFormOther 185 + }, 186 + 187 + // [ 9] Lithuanian 188 + func(n int64) i18n.PluralFormIndex { 189 + if n%10 == 1 && (n%100 < 11 || n%100 > 19) { 190 + return i18n.PluralFormOne 191 + } 192 + if n%10 >= 2 && n%10 <= 9 && (n%100 < 11 || n%100 > 19) { 193 + return i18n.PluralFormFew 194 + } 195 + return i18n.PluralFormMany 196 + }, 197 + 198 + // [10] French 199 + func(n int64) i18n.PluralFormIndex { 200 + if n == 0 || n == 1 { 201 + return i18n.PluralFormOne 202 + } 203 + if n != 0 && n%1000000 == 0 { 204 + return i18n.PluralFormMany 205 + } 206 + return i18n.PluralFormOther 207 + }, 208 + 209 + // [11] Catalan 210 + func(n int64) i18n.PluralFormIndex { 211 + if n == 1 { 212 + return i18n.PluralFormOne 213 + } 214 + if n != 0 && n%1000000 == 0 { 215 + return i18n.PluralFormMany 216 + } 217 + return i18n.PluralFormOther 218 + }, 219 + 220 + // [12] Slovenian 221 + func(n int64) i18n.PluralFormIndex { 222 + if n%100 == 1 { 223 + return i18n.PluralFormOne 224 + } 225 + if n%100 == 2 { 226 + return i18n.PluralFormTwo 227 + } 228 + if n%100 == 3 || n%100 == 4 { 229 + return i18n.PluralFormFew 230 + } 231 + return i18n.PluralFormOther 232 + }, 233 + 234 + // [13] Arabic 235 + func(n int64) i18n.PluralFormIndex { 236 + if n == 0 { 237 + return i18n.PluralFormZero 238 + } 239 + if n == 1 { 240 + return i18n.PluralFormOne 241 + } 242 + if n == 2 { 243 + return i18n.PluralFormTwo 244 + } 245 + if n%100 >= 3 && n%100 <= 10 { 246 + return i18n.PluralFormFew 247 + } 248 + if n%100 >= 11 { 249 + return i18n.PluralFormMany 250 + } 251 + return i18n.PluralFormOther 252 + }, 253 + }
+14 -2
modules/translation/translation.go
··· 32 32 TrString(string, ...any) string 33 33 34 34 Tr(key string, args ...any) template.HTML 35 + // New-style pluralized strings 36 + TrPluralString(count any, trKey string, trArgs ...any) template.HTML 37 + // Old-style pseudo-pluralized strings, deprecated 35 38 TrN(cnt any, key1, keyN string, args ...any) template.HTML 36 39 37 40 TrSize(size int64) ReadableSize ··· 100 103 } 101 104 102 105 key := "locale_" + setting.Langs[i] + ".ini" 103 - if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil { 104 - log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) 106 + if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil { 107 + log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err) 108 + } 109 + 110 + key = "locale_next/locale_" + setting.Langs[i] + ".json" 111 + if bytes, err := options.AssetFS().ReadFile(key); err == nil { 112 + if err = i18n.DefaultLocales.AddToLocaleFromJSON(setting.Langs[i], bytes); err != nil { 113 + log.Error("Failed to add new-style messages to %s: %v", setting.Langs[i], err) 114 + } 115 + } else { 116 + log.Error("Failed to open new-style messages for %s: %v", setting.Langs[i], err) 105 117 } 106 118 } 107 119 if len(setting.Langs) != 0 {
+108
modules/translation/translation_test.go
··· 48 48 assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000)) 49 49 assert.EqualValues(t, "1,000,000.1", l.PrettyNumber(1000000.1)) 50 50 } 51 + 52 + func TestGetPluralRule(t *testing.T) { 53 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en")) 54 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US")) 55 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK")) 56 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds")) 57 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE")) 58 + 59 + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh")) 60 + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja")) 61 + 62 + assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn")) 63 + 64 + assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is")) 65 + 66 + assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil")) 67 + 68 + assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs")) 69 + 70 + assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru")) 71 + 72 + assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl")) 73 + 74 + assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv")) 75 + 76 + assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt")) 77 + 78 + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr")) 79 + 80 + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca")) 81 + 82 + assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl")) 83 + 84 + assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar")) 85 + 86 + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT")) 87 + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR")) 88 + 89 + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid")) 90 + } 91 + 92 + func TestApplyPluralRule(t *testing.T) { 93 + testCases := []struct { 94 + expect i18n.PluralFormIndex 95 + pluralRule int 96 + values []int64 97 + }{ 98 + {i18n.PluralFormOne, PluralRuleDefault, []int64{1}}, 99 + {i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}}, 100 + 101 + {i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}}, 102 + 103 + {i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}}, 104 + {i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}}, 105 + 106 + {i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}}, 107 + {i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}}, 108 + 109 + {i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}}, 110 + {i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}}, 111 + 112 + {i18n.PluralFormOne, PluralRuleCzech, []int64{1}}, 113 + {i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}}, 114 + {i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}}, 115 + 116 + {i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}}, 117 + {i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}}, 118 + {i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}}, 119 + 120 + {i18n.PluralFormOne, PluralRulePolish, []int64{1}}, 121 + {i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}}, 122 + {i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}}, 123 + 124 + {i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}}, 125 + {i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}}, 126 + {i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}}, 127 + 128 + {i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}}, 129 + {i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}}, 130 + {i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}}, 131 + 132 + {i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}}, 133 + {i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}}, 134 + {i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}}, 135 + 136 + {i18n.PluralFormOne, PluralRuleCatalan, []int64{1}}, 137 + {i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}}, 138 + {i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}}, 139 + 140 + {i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}}, 141 + {i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}}, 142 + {i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}}, 143 + {i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}}, 144 + 145 + {i18n.PluralFormZero, PluralRuleArabic, []int64{0}}, 146 + {i18n.PluralFormOne, PluralRuleArabic, []int64{1}}, 147 + {i18n.PluralFormTwo, PluralRuleArabic, []int64{2}}, 148 + {i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}}, 149 + {i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}}, 150 + {i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}}, 151 + } 152 + 153 + for _, tc := range testCases { 154 + for _, n := range tc.values { 155 + assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n) 156 + } 157 + } 158 + }
-5
options/locale/locale_en-US.ini
··· 190 190 runner_kind = Search runners... 191 191 no_results = No matching results found. 192 192 issue_kind = Search issues... 193 - milestone_kind = Search milestones... 194 193 pull_kind = Search pulls... 195 194 keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator. 196 195 ··· 1887 1886 pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. 1888 1887 pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s">%[2]s#%[3]d</a>` 1889 1888 pulls.create = Create pull request 1890 - pulls.title_desc_one = wants to merge %[1]d commit from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code> 1891 - pulls.title_desc_few = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code> 1892 - pulls.merged_title_desc_one = merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s 1893 - pulls.merged_title_desc_few = merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s 1894 1889 pulls.change_target_branch_at = `changed target branch from <b>%s</b> to <b>%s</b> %s` 1895 1890 pulls.tab_conversation = Conversation 1896 1891 pulls.tab_commits = Commits
+1
options/locale_next/locale_ar.json
··· 1 + {}
+1
options/locale_next/locale_be.json
··· 1 + {}
+14
options/locale_next/locale_bg.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "сля %[1]d подаване от <code>%[2]s</code> в <code>%[3]s</code> %[4]s", 6 + "other": "сля %[1]d подавания от <code>%[2]s</code> в <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "иска да слее %[1]d подаване от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "иска да слее %[1]d подавания от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + } 14 + }
+1
options/locale_next/locale_bn.json
··· 1 + {}
+1
options/locale_next/locale_bs.json
··· 1 + {}
+5
options/locale_next/locale_ca.json
··· 1 + { 2 + "search": { 3 + "milestone_kind": "Cerca fites..." 4 + } 5 + }
+17
options/locale_next/locale_cs-CZ.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "sloučil %[1]d commit z <code>%[2]s</code> do <code>%[3]s</code> %[4]s", 6 + "other": "sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "žádá o sloučení %[1]d commitu z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Hledat milníky..." 16 + } 17 + }
+5
options/locale_next/locale_da.json
··· 1 + { 2 + "search": { 3 + "milestone_kind": "Søg milepæle..." 4 + } 5 + }
+17
options/locale_next/locale_de-DE.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "hat %[1]d Commit von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt", 6 + "other": "hat %[1]d Commits von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt" 7 + }, 8 + "title_desc": { 9 + "one": "möchte %[1]d Commit von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen", 10 + "other": "möchte %[1]d Commits von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Meilensteine suchen …" 16 + } 17 + }
+17
options/locale_next/locale_el-GR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "συγχώνευσε %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code>%[3]s</code> %[4]s", 6 + "other": "συγχώνευσε %[1]d υποβολές από <code>%[2]s</code> σε <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": ": θα ήθελε να συγχωνεύσει %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "θέλει να συγχωνεύσει %[1]d υποβολές από <code>%[2]s</code> σε <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Αναζήτηση ορόσημων..." 16 + } 17 + }
+17
options/locale_next/locale_en-US.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s", 6 + "other": "merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Search milestones..." 16 + } 17 + }
+5
options/locale_next/locale_eo.json
··· 1 + { 2 + "search": { 3 + "milestone_kind": "Serĉi celojn..." 4 + } 5 + }
+17
options/locale_next/locale_es-ES.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "fusionó %[1]d commit de <code>%[2]s</code> en <code>%[3]s</code> %[4]s", 6 + "other": "fusionó %[1]d commits de <code>%[2]s</code> en <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "quiere fusionar %[1]d commit de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "quiere fusionar %[1]d commits de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Buscar hitos…" 16 + } 17 + }
+5
options/locale_next/locale_et.json
··· 1 + { 2 + "search": { 3 + "milestone_kind": "Otsi verstapostid..." 4 + } 5 + }
+12
options/locale_next/locale_fa-IR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "%[1]d کامیت ادغام شده از <code>%[2]s</code> به <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "قصد ادغام %[1]d تغییر را از <code>%[2]s</code> به <code id=\"%[4]s\">%[3]s</code> دارد" 9 + } 10 + } 11 + } 12 + }
+15
options/locale_next/locale_fi-FI.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "yhdistetty %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "haluaa yhdistää %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "Etsi merkkipaaluja..." 14 + } 15 + }
+17
options/locale_next/locale_fil.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "isinali ang %[1]d commit mula<code>%[2]s</code> patungong <code>%[3]s</code> %[4]s", 6 + "other": "isinali ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "hinihiling na isama ang %[1]d commit mula <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "hiniling na isama ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Maghanap ng mga milestone…" 16 + } 17 + }
+17
options/locale_next/locale_fr-FR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "fusionné %[1]d commit depuis <code>%[2]s</code> vers <code>%[3]s</code> %[4]s", 6 + "other": "a fusionné %[1]d révision(s) à partir de <code>%[2]s</code> vers <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "veut fusionner %[1]d commit depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "souhaite fusionner %[1]d révision(s) depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Recherche dans les jalons..." 16 + } 17 + }
+1
options/locale_next/locale_gl.json
··· 1 + {}
+1
options/locale_next/locale_hi.json
··· 1 + {}
+15
options/locale_next/locale_hu-HU.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "egyesítve %[1]d változás(ok) a <code>%[2]s</code>-ból <code>%[3]s</code>-ba %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "egyesíteni szeretné %[1]d változás(oka)t a(z) <code>%[2]s</code>-ból <code id=\"%[4]s\">%[3]s</code>-ba" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "Mérföldkövek keresése..." 14 + } 15 + }
+12
options/locale_next/locale_id-ID.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + } 12 + }
+9
options/locale_next/locale_is-IS.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "title_desc": { 5 + "other": "vill sameina %[1]d framlög frá <code>%[2]s</code> í <code id=\"%[4]s\">%[3]s</code>" 6 + } 7 + } 8 + } 9 + }
+17
options/locale_next/locale_it-IT.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "ha fuso %[1]d commit da <code>%[2]s</code> in <code>%[3]s</code> %[4]s", 6 + "other": "ha unito %[1]d commit da <code>%[2]s</code> a <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "vuole fondere %[1]d commit da <code>%[2]s</code> in <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "vuole unire %[1]d commit da <code>%[2]s</code> a <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Ricerca tappe..." 16 + } 17 + }
+15
options/locale_next/locale_ja-JP.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "が %[1]d 個のコミットを <code>%[2]s</code> から <code>%[3]s</code> へマージ %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "が <code>%[2]s</code> から <code id=\"%[4]s\">%[3]s</code> への %[1]d コミットのマージを希望しています" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "マイルストーンを検索..." 14 + } 15 + }
+12
options/locale_next/locale_ko-KR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "님이 <code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[1]d 커밋을 %[4]s 병합함" 6 + }, 7 + "title_desc": { 8 + "other": "<code>%[2]s</code> 에서 <code id=\"%[4]s\">%[3]s</code> 로 %[1]d개의 커밋들을 병합하려함" 9 + } 10 + } 11 + } 12 + }
+5
options/locale_next/locale_lt.json
··· 1 + { 2 + "search": { 3 + "milestone_kind": "Ieškoti gairių..." 4 + } 5 + }
+17
options/locale_next/locale_lv-LV.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "iekļāva %[1]d iesūtījumu no <code>%[2]s</code> <code>%[3]s</code> %[4]s", 6 + "other": "Iekļāva %[1]d iesūtījumus no <code>%[2]s</code> zarā <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "vēlas iekļaut %[1]d iesūtījumu no <code>%[2]s</code> <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "vēlas iekļaut %[1]d iesūtījumus no <code>%[2]s</code> zarā <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Meklēt atskaites punktus..." 16 + } 17 + }
+1
options/locale_next/locale_ml-IN.json
··· 1 + {}
+1
options/locale_next/locale_nb_NO.json
··· 1 + {}
+17
options/locale_next/locale_nds.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "hett %[1]d Kommitteren vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt", 6 + "other": "hett %[1]d Kommitterens vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt" 7 + }, 8 + "title_desc": { 9 + "one": "will %[1]d Kommitteren vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren", 10 + "other": "will %[1]d Kommitterens vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "In Markstenen söken …" 16 + } 17 + }
+17
options/locale_next/locale_nl-NL.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "heeft %[1]d commit van <code>%[2]s</code> samengevoegd in <code>%[3]s</code> %[4]s", 6 + "other": "heeft %[1]d commits samengevoegd van <code>%[2]s</code> naar <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "wilt %[1]d commit van <code>%[2]s</code> samenvoegen in <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "wilt %[1]d commits van <code>%[2]s</code> samenvoegen met <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Zoek mijlpalen..." 16 + } 17 + }
+15
options/locale_next/locale_pl-PL.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "many": "scala %[1]d commity/ów z <code>%[2]s</code> do <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "many": "chce scalić %[1]d commity/ów z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "Wyszukaj kamienie milowe..." 14 + } 15 + }
+17
options/locale_next/locale_pt-BR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "mesclou %[1]d commit de <code>%[2]s</code> em <code>%[3]s</code> %[4]s", 6 + "other": "mesclou %[1]d commits de <code>%[2]s</code> em <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "quer mesclar %[1]d commit de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "quer mesclar %[1]d commits de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Pesquisar marcos..." 16 + } 17 + }
+17
options/locale_next/locale_pt-PT.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "integrou %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s", 6 + "other": "integrou %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "quer integrar %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>", 10 + "other": "quer integrar %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Procurar etapas..." 16 + } 17 + }
+17
options/locale_next/locale_ru-RU.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "слит %[1]d коммит из <code>%[2]s</code> в <code>%[3]s</code> %[4]s", 6 + "many": "слито %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "хочет влить %[1]d коммит из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>", 10 + "many": "хочет влить %[1]d коммит(ов) из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Найти этапы..." 16 + } 17 + }
+12
options/locale_next/locale_si-LK.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "මර්ජ්%[1]d සිට <code>%[2]s</code> දක්වා <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "%[1]d සිට <code>%[2]s</code> දක්වා <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + } 12 + }
+1
options/locale_next/locale_sk-SK.json
··· 1 + {}
+1
options/locale_next/locale_sl.json
··· 1 + {}
+1
options/locale_next/locale_sr-SP.json
··· 1 + {}
+15
options/locale_next/locale_sv-SE.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "sammanfogade %[1]d incheckningar från <code>%[2]s</code> in i <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "vill sammanfoga %[1]d incheckningar från <code>s[2]s</code> in i <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "Sök milstolpar..." 14 + } 15 + }
+15
options/locale_next/locale_tr-TR.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "%[4]s <code>%[2]s</code> içindeki %[1]d işlemeyi <code>%[3]s</code> ile birleştirdi" 6 + }, 7 + "title_desc": { 8 + "other": "<code>%[2]s</code> içindeki %[1]d işlemeyi <code id=\"%[4]s\">%[3]s</code> ile birleştirmek istiyor" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "Kilometre taşlarını ara..." 14 + } 15 + }
+17
options/locale_next/locale_uk-UA.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "one": "об'єднав %[1]d коміт з <code>%[2]s</code> в <code>%[3]s</code> %[4]s", 6 + "many": "об'єднав %[1]d комітів з <code>%[2]s</code> в <code>%[3]s</code> %[4]s" 7 + }, 8 + "title_desc": { 9 + "one": "хоче об'єднати %[1]d коміт з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>", 10 + "many": "хоче об'єднати %[1]d комітів з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>" 11 + } 12 + } 13 + }, 14 + "search": { 15 + "milestone_kind": "Шукати віхи..." 16 + } 17 + }
+1
options/locale_next/locale_vi.json
··· 1 + {}
+1
options/locale_next/locale_yi.json
··· 1 + {}
+15
options/locale_next/locale_zh-CN.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "于 %[4]s 将 %[1]d 次代码提交从 <code>%[2]s</code>合并至 <code>%[3]s</code>" 6 + }, 7 + "title_desc": { 8 + "other": "请求将 %[1]d 次代码提交从 <code>%[2]s</code> 合并至 <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "搜索里程碑…" 14 + } 15 + }
+9
options/locale_next/locale_zh-HK.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "於 %[4]s 將 %[1]d 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code>" 6 + } 7 + } 8 + } 9 + }
+15
options/locale_next/locale_zh-TW.json
··· 1 + { 2 + "repo": { 3 + "pulls": { 4 + "merged_title_desc": { 5 + "other": "將 %[1]d 次提交從 <code>%[2]s</code> 合併至 <code>%[3]s</code> %[4]s" 6 + }, 7 + "title_desc": { 8 + "other": "請求將 %[1]d 次程式碼提交從 <code>%[2]s</code> 合併至 <code id=\"%[4]s\">%[3]s</code>" 9 + } 10 + } 11 + }, 12 + "search": { 13 + "milestone_kind": "搜尋里程碑..." 14 + } 15 + }
+4 -4
templates/repo/issue/view_title.tmpl
··· 63 63 {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} 64 64 {{if .Issue.OriginalAuthor}} 65 65 {{.Issue.OriginalAuthor}} 66 - <span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span> 66 + <span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span> 67 67 {{else}} 68 68 <a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a> 69 - <span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span> 69 + <span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span> 70 70 {{end}} 71 71 {{if .MadeUsingAGit}} 72 72 {{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}} ··· 79 79 {{end}} 80 80 {{else}} 81 81 {{if .Issue.OriginalAuthor}} 82 - <span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}}</span> 82 + <span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}</span> 83 83 {{else}} 84 84 <span id="pull-desc-display" class="pull-desc"> 85 85 <a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> 86 - {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}} 86 + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} 87 87 </span> 88 88 {{end}} 89 89 {{if .MadeUsingAGit}}
+145
tools/migrate_locales.sh
··· 1 + #!/bin/bash 2 + 3 + # Copyright 2024 The Forgejo Authors. All rights reserved. 4 + # SPDX-License-Identifier: MIT 5 + 6 + if [ -z "$1" ] || [ -z "$2" ] 7 + then 8 + echo "USAGE: $0 section key [key1 [keyN]]" 9 + exit 1 10 + fi 11 + 12 + if ! [ -d ../options/locale_next ] 13 + then 14 + echo 'Call this script from the `tools` directory.' 15 + exit 1 16 + fi 17 + 18 + destsection="$1" 19 + keyJSON="$destsection.$2" 20 + key1="" 21 + keyN="" 22 + if [ -n "$3" ] 23 + then 24 + key1="$3" 25 + else 26 + key1="$2" 27 + fi 28 + if [ -n "$4" ] 29 + then 30 + keyN="$4" 31 + fi 32 + 33 + cd ../options/locale 34 + 35 + # Migrate the string in one file. 36 + function process() { 37 + file="$1" 38 + exec 3<$file 39 + 40 + val1="" 41 + valN="" 42 + cursection="" 43 + line1=0 44 + lineN=0 45 + lineNumber=0 46 + 47 + # Parse the file 48 + while read -u 3 line 49 + do 50 + ((++lineNumber)) 51 + if [[ $line =~ ^\[[-._a-zA-Z0-9]+\]$ ]] 52 + then 53 + cursection="${line#[}" 54 + cursection="${cursection%]}" 55 + elif [ "$cursection" = "$destsection" ] 56 + then 57 + key="${line%%=*}" 58 + value="${line#*=}" 59 + key="$(echo $key)" # Trim leading/trailing whitespace 60 + value="$(echo $value)" 61 + 62 + if [ "$key" = "$key1" ] 63 + then 64 + val1="$value" 65 + line1=$lineNumber 66 + fi 67 + if [ -n "$keyN" ] && [ "$key" = "$keyN" ] 68 + then 69 + valN="$value" 70 + lineN=$lineNumber 71 + fi 72 + 73 + if [ -n "$val1" ] && ( [ -n "$valN" ] || [ -z "$keyN" ] ) 74 + then 75 + # Found all desired strings 76 + break 77 + fi 78 + fi 79 + done 80 + 81 + if [ -n "$val1" ] || [ -n "$valN" ] 82 + then 83 + localename="${file#locale_}" 84 + localename="${localename%.ini}" 85 + localename="${localename%-*}" 86 + 87 + if [ "$file" = "locale_en-US.ini" ] 88 + then 89 + # Delete migrated string from source file 90 + if [ $line1 -gt 0 ] && [ $lineN -gt 0 ] && [ $lineN -ne $line1 ] 91 + then 92 + sed -i "${line1}d;${lineN}d" "$file" 93 + elif [ $line1 -gt 0 ] 94 + then 95 + sed -i "${line1}d" "$file" 96 + elif [ $lineN -gt 0 ] 97 + then 98 + sed -i "${lineN}d" "$file" 99 + fi 100 + fi 101 + 102 + # Write JSON 103 + jsonfile="../locale_next/${file/.ini/.json}" 104 + 105 + pluralform="other" 106 + oneform="one" 107 + case $localename in 108 + "be" | "bs" | "cnr" | "csb" | "hr" | "lt" | "pl" | "ru" | "sr" | "szl" | "uk" | "wen") 109 + # These languages have no "other" form and use "many" instead. 110 + pluralform="many" 111 + ;; 112 + "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") 113 + # These languages have no singular form. 114 + oneform="" 115 + ;; 116 + *) 117 + ;; 118 + esac 119 + 120 + content="" 121 + if [ -z "$keyN" ] 122 + then 123 + content="$(jq --arg val "$val1" ".$keyJSON = \$val" < "$jsonfile")" 124 + else 125 + object='{}' 126 + if [ -n "$val1" ] && [ -n "$oneform" ] 127 + then 128 + object=$(jq --arg val "$val1" ".$oneform = \$val" <<< "$object") 129 + fi 130 + if [ -n "$valN" ] 131 + then 132 + object=$(jq --arg val "$valN" ".$pluralform = \$val" <<< "$object") 133 + fi 134 + content="$(jq --argjson val "$object" ".$keyJSON = \$val" < "$jsonfile")" 135 + fi 136 + jq . <<< "$content" > "$jsonfile" 137 + fi 138 + } 139 + 140 + for file in *.ini 141 + do 142 + process "$file" & 143 + done 144 + wait 145 +