diff --git a/docs/content/doc/features/localization.en-us.md b/docs/content/doc/features/localization.en-us.md
index e57233a62255c..7786fd3a29113 100644
--- a/docs/content/doc/features/localization.en-us.md
+++ b/docs/content/doc/features/localization.en-us.md
@@ -31,3 +31,74 @@ After a translation has been accepted, it will be reflected in the main reposito
At the time of writing, this means that a changed translation may not appear until the following Gitea release.
If you use a bleeding edge build, it should appear as soon as you update after the change is synced.
+
+## Plurals
+
+Prior to version 1.19, Gitea handled plurals using the .TrN function which has some
+built in rules for managing plurals but is unable to properly all languages.
+
+From 1.19 we will migrate to use the CLDR formulation.
+
+Translation keys which handle plurals should be marked with a `_plural` suffix. This
+will allow autogeneration of the various forms using go templates, e.g.
+
+```ini
+form.reach_limit_of_creation_plural = You have already reached your limit of %d {{if .One}}repository{{else}}repositories{{end}}.
+```
+
+Only the `form` is provided to this template - not the operand value itself. This is to allow autogeneration of the forms at time of loading.
+
+These will be compiled to various different keys based on the available formats in the
+language. e.g. assuming the above key is in the English locale it becomes:
+
+```ini
+form.reach_limit_of_creation_plural_one = You have already reached your limit of %d repository.
+form.reach_limit_of_creation_plural_other = You have already reached your limit of %d repositories.
+
+```
+
+If the template format is too cumbersome forms can be directly created and they will
+be used in preference to the template generated variants.
+
+These keys should be used with the `.TrPlural` function. (Ordinals can be handled with `.TrOrdinal`.)
+
+Each language has different numbers of plural forms, in English (and a number of other
+languages) there are two plural forms:
+
+* `One`: This matches the singular form.
+* `Other`: This matches the plural form.
+
+Other languages, e.g. Mandarin, have no plural forms, and others many more.
+
+The possible forms are:
+
+* `Other` - the most common form and will often match to standard plural form.
+* `Zero` - matches a zeroth form, which in Latvian would match the form used for 10-20, 30 and so on.
+* `One` - matches the singular form in English, but in Latvian matches the form used for 1, 21, 31 and so on.
+* `Two` - matches the dual form used in for example Arabic for 2 items, but also more complexly in Celtic languages.
+* `Few` - matches the form used in Arabic for 3-10, 103-110, and the ternary form in Celtic languages. In Russian and Ukranian for 2-4, 22-24.
+* `Many` - matches the form used for large numbers in romance lanaguages like French, e.g. 1 000 000 *de* chat*s*, but in Russian and Ukranian it handles 0, 5~19, 100, 1000 and so on.
+
+Some plural forms are only relevant if the object being counted is of a certain
+grammatical gender or in certain tenses. Write your translation template appropriately to take account of this using `not` or `and` as appropriately.
+
+Translators may want to review the CLDR information for their language or look at
+`modules/translation/i18n/plurals/generate/plurals.xml`.
+
+Ordinal forms, e.g. 1st, 2nd, 3rd and so on can be handled with `.TrOrdinal`. These
+have the same forms as the plural forms, and we will use `_ordinal` as a base suffix
+in future.
+
+### Technical details
+
+The following is technical and is provided to aid understanding in cases of problems only. Only use `.TrPlural` (and `.TrOrdinal`) with translation keys that have the suffix `_plural` (or `_ordinal`.) If you do not the specific per plural forms must be provided explicitly in the locale file. In this case keys for plural forms will be searched for in the following hierarchy:
+
+1. `${key}_${form}` in the locale.
+2. `${key}_other` in the locale.
+3. `${key}` in the locale.
+4. `${key}_${form}` in the default locale.
+5. `${key}_other` in the default locale.
+6. `${key}` in the default locale.
+7. Use the string `${key}_${form}` directly as the format.
+
+You do not have to worry about this if the key has the `_plural` (or `_ordinal`) suffix as the correct keys will be created automatically.
diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index a7232a4658ab2..20edc7cccde4d 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -138,6 +138,9 @@ type nullLocale struct{}
func (nullLocale) Language() string { return "" }
func (nullLocale) Tr(key string, _ ...interface{}) string { return key }
func (nullLocale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { return "" }
+func (nullLocale) HasKey(_ string) bool { return false }
+func (nullLocale) TrPlural(cnt interface{}, key string, args ...interface{}) string { return key }
+func (nullLocale) TrOrdinal(cnt interface{}, key string, args ...interface{}) string { return key }
var _ (translation.Locale) = nullLocale{}
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index 5a8e13c811f7e..c37e51337c3c8 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -561,6 +561,14 @@ func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface
return key1
}
+func (l mockLocale) TrPlural(_cnt interface{}, key string, _args ...interface{}) string {
+ return key
+}
+
+func (l mockLocale) TrOrdinal(_cnt interface{}, key string, _args ...interface{}) string {
+ return key
+}
+
func TestFormatError(t *testing.T) {
cases := []struct {
err error
diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go
index 963f79c3c6b14..0722c44ddb531 100644
--- a/modules/test/context_tests.go
+++ b/modules/test/context_tests.go
@@ -99,6 +99,10 @@ func (l mockLocale) Language() string {
return "en"
}
+func (l mockLocale) HasKey(_ string) bool {
+ return false
+}
+
func (l mockLocale) Tr(s string, _ ...interface{}) string {
return s
}
@@ -107,6 +111,14 @@ func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface
return key1
}
+func (l mockLocale) TrPlural(_cnt interface{}, key string, _args ...interface{}) string {
+ return key
+}
+
+func (l mockLocale) TrOrdinal(_cnt interface{}, key string, _args ...interface{}) string {
+ return key
+}
+
type mockResponseWriter struct {
httptest.ResponseRecorder
size int
diff --git a/modules/translation/i18n/common/dtd/ldmlSupplemental.dtd b/modules/translation/i18n/common/dtd/ldmlSupplemental.dtd
new file mode 100644
index 0000000000000..72afe4b5c4cac
--- /dev/null
+++ b/modules/translation/i18n/common/dtd/ldmlSupplemental.dtd
@@ -0,0 +1,1307 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/translation/i18n/format.go b/modules/translation/i18n/format.go
index 3fb9e6d6d05fa..33624aee78bbb 100644
--- a/modules/translation/i18n/format.go
+++ b/modules/translation/i18n/format.go
@@ -10,7 +10,7 @@ import (
)
// Format formats provided arguments for a given translated message
-func Format(format string, args ...interface{}) (msg string, err error) {
+func Format(l Locale, format string, args ...interface{}) (msg string, err error) {
if len(args) == 0 {
return format, nil
}
@@ -38,5 +38,14 @@ func Format(format string, args ...interface{}) (msg string, err error) {
fmtArgs = append(fmtArgs, arg)
}
}
+
+ for i, arg := range fmtArgs {
+ switch val := arg.(type) {
+ case TranslatableFormatted:
+ fmtArgs[i] = formatWrapper{l: l, t: val}
+ case TranslatableStringer:
+ fmtArgs[i] = stringWrapper{l: l, t: val}
+ }
+ }
return fmt.Sprintf(format, fmtArgs...), err
}
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go
index d8ed43a1cd2f6..83473c7ed265a 100644
--- a/modules/translation/i18n/i18n.go
+++ b/modules/translation/i18n/i18n.go
@@ -10,13 +10,6 @@ import (
var DefaultLocales = NewLocaleStore()
-type Locale interface {
- // Tr translates a given key and arguments for a language
- Tr(trKey string, trArgs ...interface{}) string
- // Has reports if a locale has a translation for a given key
- Has(trKey string) bool
-}
-
// LocaleStore provides the functions common to all locale stores
type LocaleStore interface {
io.Closer
@@ -46,6 +39,11 @@ func ResetDefaultLocales() {
DefaultLocales = NewLocaleStore()
}
+// Tr use default locales to translate content to target language.
+func Tr(lang, trKey string, trArgs ...interface{}) string {
+ return DefaultLocales.Tr(lang, trKey, trArgs...)
+}
+
// GetLocales returns the locale from the default locales
func GetLocale(lang string) (Locale, bool) {
return DefaultLocales.Locale(lang)
diff --git a/modules/translation/i18n/locale.go b/modules/translation/i18n/locale.go
new file mode 100644
index 0000000000000..0254ff78d62a3
--- /dev/null
+++ b/modules/translation/i18n/locale.go
@@ -0,0 +1,141 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package i18n
+
+import (
+ "text/template"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/translation/i18n/plurals"
+)
+
+type locale struct {
+ store *localeStore
+ langName string
+ idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
+
+ tmpl *template.Template
+}
+
+func newLocale(store *localeStore, langName string) *locale {
+ return &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), tmpl: &template.Template{}}
+}
+
+// Tr translates content to locale language. fall back to default language.
+func (l *locale) Tr(trKey string, trArgs ...interface{}) string {
+ format := trKey
+
+ idx, ok := l.store.trKeyToIdxMap[trKey]
+ if ok {
+ if msg, ok := l.idxToMsgMap[idx]; ok {
+ format = msg // use the found translation
+ } 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
+ }
+ }
+ }
+ return l.format(trKey, format, trArgs...)
+}
+
+func (l *locale) format(trKey, format string, trArgs ...interface{}) string {
+ msg, err := Format(l, format, trArgs...)
+ if err != nil {
+ log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
+ }
+ return msg
+}
+
+// Has returns whether a key is present in this locale or not
+func (l *locale) Has(trKey string) bool {
+ idx, ok := l.store.trKeyToIdxMap[trKey]
+ if !ok {
+ return false
+ }
+ _, ok = l.idxToMsgMap[idx]
+ return ok
+}
+
+func (l *locale) TrOrdinal(cnt interface{}, trKey string, args ...interface{}) string {
+ return l.trPlurals(cnt, plurals.DefaultRules.Ordinal(l.langName), trKey, args...)
+}
+
+func (l *locale) TrPlural(cnt interface{}, trKey string, args ...interface{}) string {
+ return l.trPlurals(cnt, plurals.DefaultRules.Rule(l.langName), trKey, args...)
+}
+
+func (l *locale) TrPlurals(cnt interface{}, ruleType, trKey string, args ...interface{}) string {
+ return l.trPlurals(cnt, plurals.DefaultRules.RuleByType(plurals.RuleType(ruleType), l.langName), trKey, args...)
+}
+
+func (l *locale) trPlurals(cnt interface{}, rule *plurals.Rule, trKey string, args ...interface{}) string {
+ if rule == nil {
+ // if we fail to parse fall back to the standard
+ return l.Tr(trKey, args...)
+ }
+
+ operands, err := plurals.NewOperands(cnt)
+ if err != nil {
+ // if we fail to parse fall back to the standard
+ return l.Tr(trKey, args...)
+ }
+
+ form := rule.PluralFormFunc(operands)
+
+ // Now generate the pluralised key
+ formKey := trKey + "_" + string(form)
+ formIdx, formOk := l.store.trKeyToIdxMap[formKey]
+ if formOk { // there are at least some locales that have a format for this key...
+ msg, found := l.idxToMsgMap[formIdx]
+ if found { // and our locale has this key
+ return l.format(formKey, msg, args...)
+ }
+ }
+
+ // Try falling back to the other form
+ otherKey := trKey + "_" + string(plurals.Other)
+ otherIdx, otherOk := l.store.trKeyToIdxMap[otherKey]
+ if otherOk { // there are at least some locales that have a format for this key...
+ msg, found := l.idxToMsgMap[otherIdx]
+ if found { // and our locale has this key
+ return l.format(formKey, msg, args...)
+ }
+ }
+
+ // Try falling back to the trkey
+ trIdx, trOk := l.store.trKeyToIdxMap[trKey]
+ if trOk { // there are at least some locales that have a format for this key...
+ msg, found := l.idxToMsgMap[trIdx]
+ if found { // and our locale has this key
+ return l.format(formKey, msg, args...)
+ }
+ }
+
+ // Try falling back to the default language
+ if def, ok := l.store.localeMap[l.store.defaultLang]; ok && def != l {
+ if formOk {
+ msg, found := def.idxToMsgMap[formIdx]
+ if found { // the default locale has this key
+ return def.format(formKey, msg, args...)
+ }
+ }
+ if otherOk {
+ msg, found := def.idxToMsgMap[otherIdx]
+ if found { // the default locale has this key
+ return def.format(formKey, msg, args...)
+ }
+ }
+ if trOk {
+ msg, found := def.idxToMsgMap[trIdx]
+ if found { // the default locale has this key
+ return def.format(formKey, msg, args...)
+ }
+ }
+ }
+
+ // OK we've tried really hard to handle this and we've got nowhere - just use the formKey string as the format
+ return l.format(trKey, formKey, args...)
+}
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index 853519e22416b..17348d36204f0 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -6,20 +6,15 @@ package i18n
import (
"fmt"
+ "strings"
+ "text/template"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/translation/i18n/plurals"
"gopkg.in/ini.v1"
)
-// This file implements the static LocaleStore that will not watch for changes
-
-type locale struct {
- store *localeStore
- langName string
- idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
-}
-
type localeStore struct {
// After initializing has finished, these fields are read-only.
langNames []string
@@ -45,7 +40,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 := newLocale(store, langName)
store.localeMap[l.langName] = l
iniFile, err := ini.LoadSources(ini.LoadOptions{
@@ -59,18 +54,69 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
for _, section := range iniFile.Sections() {
for _, key := range section.Keys() {
+
+ // Create a translation key for this section/key pair
var trKey string
if section.Name() == "" || section.Name() == "DEFAULT" {
trKey = key.Name()
} else {
trKey = section.Name() + "." + key.Name()
}
+
+ // Look-up an idx for the key in the "global" key to idx map
idx, ok := store.trKeyToIdxMap[trKey]
if !ok {
idx = len(store.trKeyToIdxMap)
store.trKeyToIdxMap[trKey] = idx
}
+
+ // Store this value
l.idxToMsgMap[idx] = key.Value()
+
+ // Now handle plurals & ordinals
+ ruletypes := []plurals.RuleType{plurals.Cardinal, plurals.Ordinal}
+ for i, suffix := range []string{"_plural", "_ordinal"} {
+ if !strings.HasSuffix(trKey, suffix) {
+ continue
+ }
+
+ tmpl := template.New("")
+ tmpl, err := tmpl.Parse(key.Value())
+ if err != nil {
+ log.Error("Misformatted key %s in %s: %v", trKey, l.langName, err)
+ continue
+ }
+
+ pluralRules := plurals.DefaultRules.RuleByType(ruletypes[i], l.langName)
+
+ for form := range pluralRules.PluralForms {
+ formKey := trKey + "_" + string(form)
+
+ // Get an idx for this new key
+ idx, ok := store.trKeyToIdxMap[formKey]
+ if !ok {
+ idx = len(store.trKeyToIdxMap)
+ store.trKeyToIdxMap[formKey] = idx
+ }
+
+ // Allow for already added explicit non-templated variants
+ // (Later keys may just override our generated keys and that's fine)
+ if _, ok := l.idxToMsgMap[idx]; ok {
+ continue
+ }
+
+ // Otherwise generate from the template with the form
+ sb := &strings.Builder{}
+ err = tmpl.Execute(sb, form)
+ if err != nil {
+ log.Error("Misformatted key %s in %s: %v", trKey, l.langName, err)
+ continue
+ }
+
+ l.idxToMsgMap[idx] = sb.String()
+ }
+ }
+
}
}
iniFile = nil
@@ -114,7 +160,7 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
l, ok = store.localeMap[store.defaultLang]
if !ok {
// no default - return an empty locale
- l = &locale{store: store, idxToMsgMap: make(map[int]string)}
+ l = newLocale(store, "")
}
}
return l, found
@@ -124,36 +170,3 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
func (store *localeStore) Close() error {
return nil
}
-
-// Tr translates content to locale language. fall back to default language.
-func (l *locale) Tr(trKey string, trArgs ...interface{}) string {
- format := trKey
-
- idx, ok := l.store.trKeyToIdxMap[trKey]
- if ok {
- if msg, ok := l.idxToMsgMap[idx]; ok {
- format = msg // use the found translation
- } 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
- }
- }
- }
-
- msg, err := Format(format, trArgs...)
- if err != nil {
- log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
- }
- return msg
-}
-
-// Has returns whether a key is present in this locale or not
-func (l *locale) Has(trKey string) bool {
- idx, ok := l.store.trKeyToIdxMap[trKey]
- if !ok {
- return false
- }
- _, ok = l.idxToMsgMap[idx]
- return ok
-}
diff --git a/modules/translation/i18n/plurals/form.go b/modules/translation/i18n/plurals/form.go
new file mode 100644
index 0000000000000..5181ebcea9d17
--- /dev/null
+++ b/modules/translation/i18n/plurals/form.go
@@ -0,0 +1,51 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package plurals
+
+// Form represents a language pluralization form as defined here:
+// http://cldr.unicode.org/index/cldr-spec/plural-rules
+type Form string
+
+const (
+ Invalid Form = ""
+ Zero Form = "zero"
+ One Form = "one"
+ Two Form = "two"
+ Few Form = "few"
+ Many Form = "many"
+ Other Form = "other"
+)
+
+// Zero returns if the form is Zero
+func (f Form) Zero() bool {
+ return f == Zero
+}
+
+// One returns if the form is One
+func (f Form) One() bool {
+ return f == One
+}
+
+// Two returns if the form is Two
+func (f Form) Two() bool {
+ return f == Two
+}
+
+// Few returns if the form is Few
+func (f Form) Few() bool {
+ return f == Few
+}
+
+// Many returns if the form is Many
+func (f Form) Many() bool {
+ return f == Many
+}
+
+// Other returns if the form is Other
+func (f Form) Other() bool {
+ return f == Other
+}
diff --git a/modules/translation/i18n/plurals/generate/condition_parser.go b/modules/translation/i18n/plurals/generate/condition_parser.go
new file mode 100644
index 0000000000000..679483a69c5de
--- /dev/null
+++ b/modules/translation/i18n/plurals/generate/condition_parser.go
@@ -0,0 +1,148 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package generate
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+// As noted below relation is a lot simpler than the original full rules imply:
+//
+// relation = expr ('=' | '!=') range_list
+// expr = operand ('%' value)?
+// operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' | 'e'
+var relationRegexp = regexp.MustCompile(`([nieftvw])(?:\s*%\s*([0-9]+))?\s*(!=|=)(.*)`)
+
+// ConditionToGoString converts a CLDR plural rules to Go code.
+// See http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax
+func ConditionToGoString(condition string) string {
+ // the BNF does not allow for ors to be inside ands
+ // so a simple recursion will work
+
+ // condition = and_condition ('or' and_condition)*
+ var parsedOrConditions []string
+ for _, andCondition := range strings.Split(condition, "or") {
+ andCondition = strings.TrimSpace(andCondition)
+ if andCondition == "" {
+ continue
+ }
+ var parsedAndConditions []string
+
+ // again the BNF does not allow for ands to be inside relations
+ // so a simple recursion will work
+
+ // and_condition = relation ('and' relation)*
+ for _, relation := range strings.Split(andCondition, "and") {
+
+ // Now although the full BNF allows for a more a complex set of relations
+ // the files we are interested in are much simpler and their restricted BNF is
+ // as below
+
+ // relation = expr ('=' | '!=') range_list
+ // expr = operand ('%' modValue)?
+ // operand = 'n' | 'i' | 'e' | 'f' | 't' | 'v' | 'w'
+
+ // An operand here relates to how the input number N is after exponentiation is applied
+ //
+ // n the absolute value of N.
+ // i the integer digits of N.
+ // e the exponent value of N.
+ // v the number of visible fraction digits in N, with trailing zeros.
+ // w the number of visible fraction digits in N, without trailing zeros.
+ // f the visible fraction digits in N, with trailing zeros, expressed as an integer.
+ // t the visible fraction digits in N, without trailing zeros, expressed as an integer.
+ //
+ // This implies that at least in some languages 1.3 and 1.30 could have different plural forms.
+ parts := relationRegexp.FindStringSubmatch(relation)
+ if parts == nil {
+ continue
+ }
+
+ operand, modValue, relationType, ranges := strings.ToUpper(parts[1]), parts[2], parts[3], strings.TrimSpace(parts[4])
+
+ // Now we want to convert the condition string to something which will evaluate
+
+ // Now convert the operand to a field in the structure
+ operand = "ops." + operand
+
+ // ranges = (range | value) (',' range_list)*
+ // range = from'..'to (value..value)
+ // value = digit+
+ // digit = [0-9]
+ var parsedExprRanges []string
+ var values []string
+ for _, rangeValue := range strings.Split(ranges, ",") {
+ // check if contains ..
+ // range = value'..'value
+ if parts := strings.Split(rangeValue, ".."); len(parts) == 2 {
+ from, to := parts[0], parts[1]
+
+ // Now if we are testing the N operand because it could be a decimal number we need to use a different function
+ if operand == "ops.N" {
+ if modValue != "" {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("ops.NModInRange(%s, %s, %s)", modValue, from, to))
+ continue
+ }
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("ops.NInRange(%s, %s)", from, to))
+ continue
+ }
+
+ // Otherwise we can simply mod the operand value directly
+ if modValue != "" {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("intInRange(%s %% %s, %s, %s)", operand, modValue, from, to))
+ } else {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("intInRange(%s, %s, %s)", operand, from, to))
+ }
+ continue
+ }
+
+ // We have a plain value - collect them and test them together
+ values = append(values, rangeValue)
+ }
+
+ if len(values) > 0 {
+ valuesArgs := strings.Join(values, ",")
+
+ // Now if we are testing the N operand because it could be a decimal number we need to use a different function
+ if operand == "ops.N" {
+ if modValue != "" {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("ops.NModEqualsAny(%s, %s)", modValue, valuesArgs))
+ } else {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("ops.NEqualsAny(%s)", valuesArgs))
+ }
+ } else if modValue != "" {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("intEqualsAny(%s %% %s, %s)", operand, modValue, valuesArgs))
+ } else {
+ parsedExprRanges = append(parsedExprRanges, fmt.Sprintf("intEqualsAny(%s, %s)", operand, valuesArgs))
+ }
+ }
+
+ // join all the parsed Ranges together as Ors
+ parsedRelations := strings.Join(parsedExprRanges, " || ")
+
+ // Group them
+ if len(parsedExprRanges) > 1 {
+ parsedRelations = "(" + parsedRelations + ")"
+ }
+
+ // Handle not
+ if relationType == "!=" {
+ parsedRelations = "!" + parsedRelations
+ }
+
+ parsedAndConditions = append(parsedAndConditions, parsedRelations)
+ }
+ parsedAndCondition := strings.TrimSpace(strings.Join(parsedAndConditions, " && "))
+ if parsedAndCondition == "" {
+ continue
+ }
+ parsedOrConditions = append(parsedOrConditions, parsedAndCondition)
+ }
+ return strings.Join(parsedOrConditions, " ||\n")
+}
diff --git a/modules/translation/i18n/plurals/generate/main/generate.go b/modules/translation/i18n/plurals/generate/main/generate.go
new file mode 100644
index 0000000000000..1e469f1161385
--- /dev/null
+++ b/modules/translation/i18n/plurals/generate/main/generate.go
@@ -0,0 +1,184 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package main
+
+import (
+ "bytes"
+ "encoding/xml"
+ "flag"
+ "fmt"
+ "go/format"
+ "os"
+ "text/template"
+
+ "code.gitea.io/gitea/modules/translation/i18n/plurals/generate"
+)
+
+var usage = `%[1]s parses and generates go code for CLDR plural.xml and ordinal.xml.xml
+
+Usage: %[1]s [-v] [-c code.go] [-t test.go] plurals.xml ordinals.xml
+
+`
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, usage, os.Args[0])
+ flag.PrintDefaults()
+ }
+
+ var codeOut, testOut string
+ flag.BoolVar(&verbose, "v", false, "verbose output")
+ flag.StringVar(&codeOut, "c", "", "file to output generated code")
+ flag.StringVar(&testOut, "t", "", "file to output generated tests")
+ flag.Parse()
+
+ args := flag.Args()
+
+ if len(args) == 0 {
+ args = []string{"plurals.xml"}
+ }
+
+ data := &generate.SupplementalData{}
+
+ for _, in := range args {
+ buf, err := os.ReadFile(in)
+ if err != nil {
+ fatalf("failed to read file: %s", err)
+ }
+
+ var individual generate.SupplementalData
+ if err := xml.Unmarshal(buf, &individual); err != nil {
+ fatalf("failed to unmarshal xml: %s", err)
+ }
+ data.Plurals = append(data.Plurals, individual.Plurals...)
+ }
+
+ if verbose {
+ count := 0
+ groups := 0
+ for _, plurals := range data.Plurals {
+ for _, localeGroups := range plurals.LocaleGroups {
+ count += len(localeGroups.SplitLocales())
+ groups++
+ }
+ }
+ verbosef("parsed %d locales in %d groups", count, groups)
+ }
+
+ if codeOut != "" {
+ if err := runTemplate(codeTemplate, codeOut, data); err != nil {
+ fatalf("failed to generate code: %v", err)
+ } else {
+ verbosef("generated %s", codeOut)
+ }
+ } else {
+ logf("not generating code file (use -c)")
+ }
+
+ if testOut != "" {
+ if err := runTemplate(testTemplate, testOut, data); err != nil {
+ fatalf("failed to generate test code: %v", err)
+ } else {
+ verbosef("generated %s", testOut)
+ }
+ } else {
+ logf("not generating test file (use -t)")
+ }
+}
+
+func runTemplate(t *template.Template, filename string, data *generate.SupplementalData) error {
+ buf := bytes.NewBuffer(nil)
+ if err := t.Execute(buf, data); err != nil {
+ return fmt.Errorf("unable to execute template: %w", err)
+ }
+ bs, err := format.Source(buf.Bytes())
+ if err != nil {
+ verbosef("Bad source:\n%s", buf.String())
+ return fmt.Errorf("unable to format source: %w", err)
+ }
+ file, err := os.Create(filename)
+ if err != nil {
+ return fmt.Errorf("failed to create file %s because %w", filename, err)
+ }
+ defer file.Close()
+ _, err = file.Write(bs)
+ if err != nil {
+ return fmt.Errorf("unable to write generated source: %w", err)
+ }
+ return nil
+}
+
+var codeTemplate = template.Must(template.New("codeTemplate").Parse(`// This file is generated by modules/translation/i18n/plurals/generate/main/generate.go DO NOT EDIT
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package plurals
+
+// DefaultRules returns a map of Rules generated from CLDR language data.
+var DefaultRules *Rules
+
+func init() {
+ DefaultRules = &Rules{}
+{{range $p, $plurals := .Plurals}}
+{{range .LocaleGroups}}
+ addPluralRules(DefaultRules, {{printf "%q" $plurals.Type}}, {{printf "%#v" .SplitLocales}}, &Rule{
+ PluralForms: newPluralFormSet({{range $i, $e := .Rules}}{{if $i}}, {{end}}{{$e.CountTitle}}{{end}}),
+ PluralFormFunc: func(ops *Operands) Form { {{range .Rules}}{{if .GoCondition}}
+ // {{.Condition}}
+ if {{.GoCondition}} {
+ return {{.CountTitle}}
+ }{{end}}{{end}}
+ return Other
+ },
+ }){{end}}{{end}}
+}
+`))
+
+var testTemplate = template.Must(template.New("testTemplate").Parse(`// This file is generated by modules/translation/i18n/plurals/generate/main/generate.go DO NOT EDIT
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package plurals
+
+import "testing"
+
+{{range $p, $plurals := .Plurals}}
+{{range $i, $localeGroup := .LocaleGroups}}
+func Test{{$i}}{{$plurals.Type}}(t *testing.T) {
+ var tests []pluralFormTest
+ {{range $localeGroup.Rules}}
+ {{if .IntegerSamples}}tests = appendIntegerTests(tests, {{printf "%q" $plurals.Type}}, {{.CountTitle}}, {{printf "%#v" .IntegerSamples}}){{end}}
+ {{if .DecimalSamples}}tests = appendDecimalTests(tests, {{printf "%q" $plurals.Type}}, {{.CountTitle}}, {{printf "%#v" .DecimalSamples}}){{end}}
+ {{end}}
+ locales := {{printf "%#v" $localeGroup.SplitLocales}}
+ for _, locale := range locales {
+ runTests(t, locale, {{printf "%q" $plurals.Type}}, tests)
+ }
+}
+
+{{end}}
+{{end}}
+`))
+
+func logf(format string, args ...interface{}) {
+ fmt.Fprintf(os.Stderr, format+"\n", args...)
+}
+
+var verbose bool
+
+func verbosef(format string, args ...interface{}) {
+ if verbose {
+ logf(format, args...)
+ }
+}
+
+func fatalf(format string, args ...interface{}) {
+ logf("fatal: "+format+"\n", args...)
+ os.Exit(1)
+}
diff --git a/modules/translation/i18n/plurals/generate/ordinals.xml b/modules/translation/i18n/plurals/generate/ordinals.xml
new file mode 100644
index 0000000000000..048069a62eb4d
--- /dev/null
+++ b/modules/translation/i18n/plurals/generate/ordinals.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+ @integer 0~15, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n % 10 = 1,2 and n % 100 != 11,12 @integer 1, 2, 21, 22, 31, 32, 41, 42, 51, 52, 61, 62, 71, 72, 81, 82, 101, 1001, …
+ @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1 @integer 1
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1,5 @integer 1, 5
+ @integer 0, 2~4, 6~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1..4 @integer 1~4
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n % 10 = 2,3 and n % 100 != 12,13 @integer 2, 3, 22, 23, 32, 33, 42, 43, 52, 53, 62, 63, 72, 73, 82, 83, 102, 1002, …
+ @integer 0, 1, 4~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, …
+ @integer 0~2, 4~16, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n % 10 = 6,9 or n = 10 @integer 6, 9, 10, 16, 19, 26, 29, 36, 39, 106, 1006, …
+ @integer 0~5, 7, 8, 11~15, 17, 18, 20, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n % 10 = 6 or n % 10 = 9 or n % 10 = 0 and n != 0 @integer 6, 9, 10, 16, 19, 20, 26, 29, 30, 36, 39, 40, 100, 1000, 10000, 100000, 1000000, …
+ @integer 0~5, 7, 8, 11~15, 17, 18, 21, 101, 1001, …
+
+
+ n = 11,8,80,800 @integer 8, 11, 80, 800
+ @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 11,8,80..89,800..899 @integer 8, 11, 80~89, 800~803
+ @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ i = 1 @integer 1
+ i = 0 or i % 100 = 2..20,40,60,80 @integer 0, 2~16, 102, 1002, …
+ @integer 21~36, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1 @integer 1
+ n % 10 = 4 and n % 100 != 14 @integer 4, 24, 34, 44, 54, 64, 74, 84, 104, 1004, …
+ @integer 0, 2, 3, 5~17, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1..4 or n % 100 = 1..4,21..24,41..44,61..64,81..84 @integer 1~4, 21~24, 41~44, 61~64, 101, 1001, …
+ n = 5 or n % 100 = 5 @integer 5, 105, 205, 305, 405, 505, 605, 705, 1005, …
+ @integer 0, 6~20, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …
+ n % 10 = 2 and n % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, …
+ n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, …
+ @integer 0, 4~18, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1 @integer 1
+ n = 2,3 @integer 2, 3
+ n = 4 @integer 4
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1,11 @integer 1, 11
+ n = 2,12 @integer 2, 12
+ n = 3,13 @integer 3, 13
+ @integer 0, 4~10, 14~21, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1,3 @integer 1, 3
+ n = 2 @integer 2
+ n = 4 @integer 4
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …
+ i % 10 = 2 and i % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, …
+ i % 10 = 7,8 and i % 100 != 17,18 @integer 7, 8, 27, 28, 37, 38, 47, 48, 57, 58, 67, 68, 77, 78, 87, 88, 107, 1007, …
+ @integer 0, 3~6, 9~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ i % 10 = 1,2,5,7,8 or i % 100 = 20,50,70,80 @integer 1, 2, 5, 7, 8, 11, 12, 15, 17, 18, 20~22, 25, 101, 1001, …
+ i % 10 = 3,4 or i % 1000 = 100,200,300,400,500,600,700,800,900 @integer 3, 4, 13, 14, 23, 24, 33, 34, 43, 44, 53, 54, 63, 64, 73, 74, 100, 1003, …
+ i = 0 or i % 10 = 6 or i % 100 = 40,60,90 @integer 0, 6, 16, 26, 36, 40, 46, 56, 106, 1006, …
+ @integer 9, 10, 19, 29, 30, 39, 49, 59, 69, 79, 109, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n = 1 @integer 1
+ n = 2,3 @integer 2, 3
+ n = 4 @integer 4
+ n = 6 @integer 6
+ @integer 0, 5, 7~20, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1,5,7,8,9,10 @integer 1, 5, 7~10
+ n = 2,3 @integer 2, 3
+ n = 4 @integer 4
+ n = 6 @integer 6
+ @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, …
+
+
+ n = 1,5,7..9 @integer 1, 5, 7~9
+ n = 2,3 @integer 2, 3
+ n = 4 @integer 4
+ n = 6 @integer 6
+ @integer 0, 10~24, 100, 1000, 10000, 100000, 1000000, …
+
+
+
+
+
+ n = 0,7,8,9 @integer 0, 7~9
+ n = 1 @integer 1
+ n = 2 @integer 2
+ n = 3,4 @integer 3, 4
+ n = 5,6 @integer 5, 6
+ @integer 10~25, 100, 1000, 10000, 100000, 1000000, …
+
+
+
diff --git a/modules/translation/i18n/plurals/generate/plurals.xml b/modules/translation/i18n/plurals/generate/plurals.xml
new file mode 100644
index 0000000000000..eaac2882532bd
--- /dev/null
+++ b/modules/translation/i18n/plurals/generate/plurals.xml
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+ @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ i = 0,1 @integer 0, 1 @decimal 0.0~1.5
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ i = 1 and v = 0 @integer 1
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0
+ @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, …
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …
+
+
+
+
+
+ n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+ @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …
+
+
+ n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+ i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+ @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
+ n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00
+ @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ i = 1 and v = 0 @integer 1
+ v != 0 or n = 0 or n % 100 = 2..19 @integer 0, 2~16, 102, 1002, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @integer 20~35, 100, 1000, 10000, 100000, 1000000, …
+
+
+ v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+ v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ i = 0,1 @integer 0, 1 @decimal 0.0~1.5
+ e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
+ @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
+
+
+ i = 0..1 @integer 0, 1 @decimal 0.0~1.5
+ e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
+ @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
+
+
+ i = 1 and v = 0 @integer 1
+ e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
+
+
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …
+ @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
+
+
+
+
+
+ n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000
+ n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000
+ n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00
+ @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …
+ v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …
+ v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+ v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+ v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …
+ v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ i = 1 and v = 0 @integer 1
+ i = 2 and v = 0 @integer 2
+ v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …
+ @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ i = 1 and v = 0 @integer 1
+ i = 2..4 and v = 0 @integer 2~4
+ v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+
+
+ i = 1 and v = 0 @integer 1
+ v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
+ v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+ @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
+ n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …
+ n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
+
+
+ n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
+ n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …
+ f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
+ @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, …
+ n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
+ @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …
+ v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
+ v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+ @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
+
+
+ n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …
+ n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …
+ n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …
+ n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, …
+ @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …
+
+
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+ n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000
+ n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000
+ @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …
+ v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …
+ v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …
+ v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+ @integer 3~10, 13~19, 23, 103, 1003, …
+
+
+
+
+
+ n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, …
+ n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, …
+ n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, …
+ @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, …
+
+
+ n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+ n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …
+ n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
+ @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+ n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+ n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+ n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+ n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000
+ n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000
+ @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+
+
+
diff --git a/modules/translation/i18n/plurals/generate/xml.go b/modules/translation/i18n/plurals/generate/xml.go
new file mode 100644
index 0000000000000..fa61f3bba9840
--- /dev/null
+++ b/modules/translation/i18n/plurals/generate/xml.go
@@ -0,0 +1,138 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+//go:generate go run main/generate.go -c ../rules_gen.go -t ../rules_gen_test.go plurals.xml ordinals.xml
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package generate
+
+import (
+ "encoding/xml"
+ "strings"
+)
+
+// SupplementalData is the root of plural.xml
+type SupplementalData struct {
+ XMLName xml.Name `xml:"supplementalData"`
+ Plurals []Plurals `xml:"plurals"`
+}
+
+// SupplementalData is the root of plural.xml
+type Plurals struct {
+ Type string `xml:"type,attr"`
+ LocaleGroups []LocaleGroup `xml:"pluralRules"`
+}
+
+// LocaleGroup is a group of locales with the same plural rules.
+type LocaleGroup struct {
+ Locales string `xml:"locales,attr"`
+ Rules []Rule `xml:"pluralRule"`
+}
+
+// SplitLocales returns all the locales in the PluralGroup as a slice.
+func (lg *LocaleGroup) SplitLocales() []string {
+ return strings.Split(lg.Locales, " ")
+}
+
+// Rule is a rule for a single plural form.
+type Rule struct {
+ // Count is one of: `zero` | `one` | `two` | `few` | `many` | `other`
+ Count string `xml:"count,attr"`
+ // Rule looks like:
+ // # (@integer (…)?)? (@decimal (…)?)?
+ // # ">n % 10 = 1 and n % 100 != 11..19"
+ // # "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"
+ // # "@decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …"
+ Rule string `xml:",innerxml"`
+}
+
+// CountTitle returns the title case of the pluralRule's count.
+func (r *Rule) CountTitle() string {
+ return strings.ToUpper(r.Count[0:1]) + r.Count[1:]
+}
+
+// Condition returns the condition where the pluralRule applies.
+// These look like: "", ">n % 10 = 1 and n % 100 != 11..19" etc.
+//
+// The conditions themselves have the following syntax.
+//
+// # condition = and_condition ('or' and_condition)*
+// # and_condition = relation ('and' relation)*
+//
+// Now the next bit needs some adjustment:
+//
+// # relation = is_relation | in_relation | within_relation
+// # is_relation = expr 'is' ('not')? value
+// # ^------------ This is not present in plurals.xml/ordinals.xml
+// # in_relation = expr (('not')? 'in' | '=' | '!=') range_list
+// # ^^^^^^^^^^^^^^^^^^^^^ not in plurals.xml/ordinals.xml
+// # within_relation = expr ('not')? 'within' range_list
+// # ^------------- This is not present in plurals.xml/ordinals.xml
+//
+// So relation is really:
+//
+// # relation = expr ('=' | '!=') range_list
+// # expr = operand (('mod' | '%') value)?
+// # ^^^^^ not in plurals.xml/ordinals.xml
+// # operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w' | 'c' | 'e'
+// # not in plurals.xml/ordinals.xml ^^^^^^^^^^^
+// # range_list = (range | value) (',' range_list)*
+// # range = value'..'value
+// # value = digit+
+// # digit = [0-9]
+func (r *Rule) Condition() string {
+ i := strings.Index(r.Rule, "@")
+ if i >= 0 {
+ return r.Rule[:i]
+ }
+ return r.Rule
+}
+
+// Samples returns the integer and decimal samples for the pluralRule
+//
+// # samples = ('@integer' sampleList)?
+// # ('@decimal' sampleList)?
+// # sampleList = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
+// # sampleRange = sampleValue ('~' sampleValue)?
+// # sampleValue = value ('.' digit+)? ([ce] digitPos digit+)?
+// # value = digit+
+// # digit = [0-9]
+// # 1 = [1-9]
+func (r *Rule) Samples() (integer, decimal []string) {
+ // First of all remove the ellipses as they're not helpful
+ rule := strings.ReplaceAll(r.Rule, ", …", "")
+
+ // Now the we know that @decimal is always after the @integer section
+ ruleSplit := strings.SplitN(rule, " @decimal ", 2)
+ rule = ruleSplit[0]
+
+ if len(ruleSplit) > 1 {
+ decimal = strings.Split(ruleSplit[1], ", ")
+ }
+
+ ruleSplit = strings.SplitN(rule, " @integer ", 2)
+ if len(ruleSplit) > 1 {
+ integer = strings.Split(ruleSplit[1], ", ")
+ }
+
+ return integer, decimal
+}
+
+// IntegerSamples returns the integer exmaples for the PLuralRule.
+func (r *Rule) IntegerSamples() []string {
+ integer, _ := r.Samples()
+ return integer
+}
+
+// DecimalSamples returns the decimal exmaples for the PLuralRule.
+func (r *Rule) DecimalSamples() []string {
+ _, decimal := r.Samples()
+ return decimal
+}
+
+// GoCondition returns the converted CLDR plural rules to Go code
+func (r *Rule) GoCondition() string {
+ return ConditionToGoString(r.Condition())
+}
diff --git a/modules/translation/i18n/plurals/operands.go b/modules/translation/i18n/plurals/operands.go
new file mode 100644
index 0000000000000..6b6ba4dae9f3d
--- /dev/null
+++ b/modules/translation/i18n/plurals/operands.go
@@ -0,0 +1,192 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package plurals
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+func intInRange(i, from, to int64) bool {
+ return from <= i && i <= to
+}
+
+func intEqualsAny(i int64, any ...int64) bool {
+ for _, a := range any {
+ if i == a {
+ return true
+ }
+ }
+ return false
+}
+
+// Operands is a representation of http://unicode.org/reports/tr35/tr35-numbers.html#Operands
+type Operands struct {
+ N float64 // absolute value of the source number (integer and decimals)
+ I int64 // integer digits of n
+ E int64 // exponent
+ V int64 // number of visible fraction digits in n, with trailing zeros
+ W int64 // number of visible fraction digits in n, without trailing zeros
+ F int64 // visible fractional digits in n, with trailing zeros
+ T int64 // visible fractional digits in n, without trailing zeros
+}
+
+// NEqualsAny returns true if o represents an integer equal to any of the arguments.
+func (o *Operands) NEqualsAny(any ...int64) bool {
+ if o.T != 0 {
+ return false
+ }
+
+ return intEqualsAny(o.I, any...)
+}
+
+// NModEqualsAny returns true if o represents an integer equal to any of the arguments modulo mod.
+func (o *Operands) NModEqualsAny(mod int64, any ...int64) bool {
+ if o.T != 0 {
+ return false
+ }
+
+ modI := o.I % mod
+ return intEqualsAny(modI, any...)
+}
+
+// NInRange returns true if o represents an integer in the closed interval [from, to].
+func (o *Operands) NInRange(from, to int64) bool {
+ return o.T == 0 && intInRange(o.I, from, to)
+}
+
+// NModInRange returns true if o represents an integer in the closed interval [from, to] modulo mod.
+func (o *Operands) NModInRange(mod, from, to int64) bool {
+ modI := o.I % mod
+ return o.T == 0 && intInRange(modI, from, to)
+}
+
+// NewOperands returns the operands for number.
+func NewOperands(number interface{}) (*Operands, error) {
+ switch number := number.(type) {
+ case int:
+ return operandsFromInt64(int64(number)), nil
+ case int8:
+ return operandsFromInt64(int64(number)), nil
+ case int16:
+ return operandsFromInt64(int64(number)), nil
+ case int32:
+ return operandsFromInt64(int64(number)), nil
+ case int64:
+ return operandsFromInt64(number), nil
+ case string:
+ return operandsFromString(number)
+ case float32, float64:
+ return nil, fmt.Errorf("floats must be formatted as a string")
+ default:
+ return nil, fmt.Errorf("invalid type %T; expected integer or string", number)
+ }
+}
+
+func operandsFromInt64(i int64) *Operands {
+ if i < 0 {
+ i = -i
+ }
+ return &Operands{float64(i), i, 0, 0, 0, 0, 0}
+}
+
+func operandsFromString(s string) (*Operands, error) {
+ s = strings.TrimSpace(s)
+
+ // strip the sign
+ if s[0] == '-' {
+ s = s[1:]
+ }
+
+ ops := &Operands{}
+
+ // Now the problem is s could be in [1-9](.[0-9]+)?e[1-9][0-9]*
+ // We need to determine how many numbers after the decimal place remain.
+ s = strings.Replace(s, "e", "c", 1)
+ if parts := strings.SplitN(s, "c", 2); len(parts) == 2 {
+ if idx := strings.Index(parts[0], "."); idx >= 0 {
+ numberOfDecimalsPreExp := len(parts[0]) - idx - 1
+ exp, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return nil, err
+ }
+ ops.E = int64(exp)
+ if exp >= numberOfDecimalsPreExp {
+ s = parts[0][:idx] + parts[0][idx+1:]
+ exp -= numberOfDecimalsPreExp
+ s += strings.Repeat("0", exp)
+ } else {
+ s = parts[0][:idx] + parts[0][idx+1:len(parts[0])+exp-numberOfDecimalsPreExp] + "." + parts[0][len(parts[0])+exp-numberOfDecimalsPreExp:]
+ }
+ } else {
+ exp, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return nil, err
+ }
+ ops.E = int64(exp)
+
+ s = parts[0] + strings.Repeat("0", exp)
+ }
+ }
+
+ // attempt to parse as a float
+ n, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ // ops.N is the value of the number
+ ops.N = n
+
+ // Now split at the "."
+ parts := strings.SplitN(s, ".", 2)
+
+ // ops.I is the integer floor of the number
+ ops.I, err = strconv.ParseInt(parts[0], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ // if there is no decimal part the rest of the parts of the operand is 0
+ if len(parts) == 1 {
+ return ops, nil
+ }
+
+ // parts[1] is the decimal part
+ fraction := parts[1]
+
+ // V is the number of visible fraction digits in n, with trailing zeros
+ ops.V = int64(len(fraction))
+ for i := ops.V - 1; i >= 0; i-- {
+ if fraction[i] != '0' {
+ // W is the number of visible fraction digits in n, without trailing zeros
+ ops.W = i + 1
+ break
+ }
+ }
+
+ if ops.V > 0 {
+ // F is the visible fractional digits in n, with trailing zeros
+ // we get this from the V
+ f, err := strconv.ParseInt(fraction, 10, 0)
+ if err != nil {
+ return nil, err
+ }
+ ops.F = f
+ }
+ if ops.W > 0 {
+ // T is visible fractional digits in n, without trailing zeros
+ // we get this from the W
+ t, err := strconv.ParseInt(fraction[:ops.W], 10, 0)
+ if err != nil {
+ return nil, err
+ }
+ ops.T = t
+ }
+ return ops, nil
+}
diff --git a/modules/translation/i18n/plurals/rule_test.go b/modules/translation/i18n/plurals/rule_test.go
new file mode 100644
index 0000000000000..70611f7ec5146
--- /dev/null
+++ b/modules/translation/i18n/plurals/rule_test.go
@@ -0,0 +1,127 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package plurals
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+)
+
+type pluralFormTest struct {
+ num interface{}
+ typ string
+ form Form
+}
+
+func runTests(t *testing.T, pluralRuleID, typ string, tests []pluralFormTest) {
+ if pluralRuleID == "root" {
+ return
+ }
+ pluralRules := DefaultRules
+ if rule := pluralRules.RuleByType(RuleType(typ), pluralRuleID); rule != nil {
+ for _, test := range tests {
+ ops, err := NewOperands(test.num)
+ if err != nil {
+ t.Errorf("%s: NewOperands(%v) errored with %s", pluralRuleID, test.num, err)
+ break
+ }
+ if pluralForm := rule.PluralFormFunc(ops); pluralForm != test.form {
+ t.Errorf("%s:%v: PluralFormFunc(%#v) returned %q, %v; expected %q", pluralRuleID, test.num, ops, pluralForm, err, test.form)
+ }
+ }
+ } else {
+ t.Errorf("could not find plural rule for locale %s", pluralRuleID)
+ }
+}
+
+func appendIntegerTests(tests []pluralFormTest, typ string, form Form, examples []string) []pluralFormTest {
+ for _, ex := range expandExamples(examples) {
+ var i int64
+ if strings.Count(ex, "c") == 1 || strings.Count(ex, "e") == 1 {
+ ex = strings.Replace(ex, "e", "c", 1)
+ // Now the problem is s could be in [1-9](.[0-9]+)?e[1-9][0-9]*
+ // We need to determine how many numbers after the decimal place remain.
+ if parts := strings.SplitN(ex, "c", 2); len(parts) == 2 {
+ if idx := strings.Index(parts[0], "."); idx >= 0 {
+ numberOfDecimalsPreExp := len(parts[0]) - idx - 1
+ exp, err := strconv.Atoi(parts[1])
+ if err != nil {
+ panic(err)
+ }
+ if exp >= numberOfDecimalsPreExp {
+ ex = parts[0][:idx] + parts[0][idx+1:]
+ exp -= numberOfDecimalsPreExp
+ ex += strings.Repeat("0", exp)
+ } else {
+ ex = parts[0][:idx] + parts[0][idx+1:len(parts[0])+exp-numberOfDecimalsPreExp] + "." + parts[0][len(parts[0])+exp-numberOfDecimalsPreExp:]
+ }
+ } else {
+ exp, err := strconv.Atoi(parts[1])
+ if err != nil {
+ panic(err)
+ }
+ ex = parts[0] + strings.Repeat("0", exp)
+ }
+ }
+ }
+
+ var err error
+ i, err = strconv.ParseInt(ex, 10, 64)
+ if err != nil {
+ panic(err)
+ }
+ tests = append(tests, pluralFormTest{ex, typ, form}, pluralFormTest{i, typ, form})
+ }
+ return tests
+}
+
+func appendDecimalTests(tests []pluralFormTest, typ string, form Form, examples []string) []pluralFormTest {
+ for _, ex := range expandExamples(examples) {
+ ex = strings.Replace(ex, "c", "e", 1)
+ tests = append(tests, pluralFormTest{ex, typ, form})
+ }
+ return tests
+}
+
+func expandExamples(examples []string) []string {
+ var expanded []string
+ for _, ex := range examples {
+ ex = strings.Replace(ex, "c", "e", 1)
+ if parts := strings.Split(ex, "~"); len(parts) == 2 {
+ for ex := parts[0]; ; ex = increment(ex) {
+ expanded = append(expanded, ex)
+ if ex == parts[1] {
+ break
+ }
+ }
+ } else {
+ expanded = append(expanded, ex)
+ }
+ }
+ return expanded
+}
+
+func increment(dec string) string {
+ runes := []rune(dec)
+ carry := true
+ for i := len(runes) - 1; carry && i >= 0; i-- {
+ switch runes[i] {
+ case '.':
+ continue
+ case '9':
+ runes[i] = '0'
+ default:
+ runes[i]++
+ carry = false
+ }
+ }
+ if carry {
+ runes = append([]rune{'1'}, runes...)
+ }
+ return string(runes)
+}
diff --git a/modules/translation/i18n/plurals/rules.go b/modules/translation/i18n/plurals/rules.go
new file mode 100644
index 0000000000000..03411cb0234d2
--- /dev/null
+++ b/modules/translation/i18n/plurals/rules.go
@@ -0,0 +1,119 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package plurals
+
+import (
+ "strings"
+)
+
+type RuleType string
+
+const (
+ Cardinal RuleType = "cardinal"
+ Ordinal RuleType = "ordinal"
+)
+
+// Rule defines the CLDR plural rules for a language.
+// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
+// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
+type Rule struct {
+ PluralForms map[Form]struct{}
+ PluralFormFunc func(*Operands) Form
+}
+
+func addPluralRules(rules *Rules, typ RuleType, ids []string, ps *Rule) {
+ for _, id := range ids {
+ if id == "root" {
+ continue
+ }
+ switch typ {
+ case Cardinal:
+ if rules.CardinalMap == nil {
+ rules.CardinalMap = map[string]*Rule{}
+ }
+ rules.CardinalMap[id] = ps
+ case Ordinal:
+ if rules.OrdinalMap == nil {
+ rules.OrdinalMap = map[string]*Rule{}
+ }
+ rules.OrdinalMap[id] = ps
+ default:
+ if rules.Others == nil {
+ rules.Others = map[RuleType]map[string]*Rule{}
+ }
+ if rules.Others[typ] == nil {
+ rules.Others[typ] = map[string]*Rule{}
+ }
+ rules.Others[typ][id] = ps
+ }
+ }
+}
+
+func newPluralFormSet(pluralForms ...Form) map[Form]struct{} {
+ set := make(map[Form]struct{}, len(pluralForms))
+ for _, plural := range pluralForms {
+ set[plural] = struct{}{}
+ }
+ return set
+}
+
+type Rules struct {
+ CardinalMap map[string]*Rule
+ OrdinalMap map[string]*Rule
+ Others map[RuleType]map[string]*Rule
+}
+
+// Rule returns the closest matching plural rule for the language tag
+// or nil if no rule could be found.
+func (r Rules) Rule(locale string) *Rule {
+ for {
+ if rule, ok := r.CardinalMap[locale]; ok {
+ return rule
+ }
+ idx := strings.LastIndex(locale, "-")
+ if idx < 0 {
+ return r.CardinalMap["en"]
+ }
+ locale = locale[:idx]
+ }
+}
+
+// Rule returns the closest matching plural rule for the language tag
+// or nil if no rule could be found.
+func (r Rules) Ordinal(locale string) *Rule {
+ for {
+ if rule, ok := r.OrdinalMap[locale]; ok {
+ return rule
+ }
+ idx := strings.LastIndex(locale, "-")
+ if idx < 0 {
+ return r.OrdinalMap["en"]
+ }
+ locale = locale[:idx]
+ }
+}
+
+// Rule returns the closest matching plural rule for the language tag
+// or nil if no rule could be found.
+func (r Rules) RuleByType(typ RuleType, locale string) *Rule {
+ switch typ {
+ case Cardinal:
+ return r.Rule(locale)
+ case Ordinal:
+ return r.Ordinal(locale)
+ }
+ for {
+ if rule, ok := r.Others[typ][locale]; ok {
+ return rule
+ }
+ idx := strings.LastIndex(locale, "-")
+ if idx < 0 {
+ return r.Others[typ]["en"]
+ }
+ locale = locale[:idx]
+ }
+}
diff --git a/modules/translation/i18n/plurals/rules_gen.go b/modules/translation/i18n/plurals/rules_gen.go
new file mode 100644
index 0000000000000..807f2459243e3
--- /dev/null
+++ b/modules/translation/i18n/plurals/rules_gen.go
@@ -0,0 +1,1001 @@
+// This file is generated by modules/translation/i18n/plurals/generate/main/generate.go DO NOT EDIT
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package plurals
+
+// DefaultRules returns a map of Rules generated from CLDR language data.
+var DefaultRules *Rules
+
+func init() {
+ DefaultRules = &Rules{}
+
+ addPluralRules(DefaultRules, "cardinal", []string{"bm", "bo", "dz", "hnj", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "tpi", "vi", "wo", "yo", "yue", "zh"}, &Rule{
+ PluralForms: newPluralFormSet(Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0 or n = 1
+ if intEqualsAny(ops.I, 0) ||
+ ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ff", "hy", "kab"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0,1
+ if intEqualsAny(ops.I, 0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "ji", "lij", "nl", "sc", "scn", "sv", "sw", "ur", "yi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"si"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0,1 or i = 0 and f = 1
+ if ops.NEqualsAny(0, 1) ||
+ intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ak", "bho", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0..1
+ if ops.NInRange(0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"tzm"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0..1 or n = 11..99
+ if ops.NInRange(0, 1) ||
+ ops.NInRange(11, 99) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"af", "an", "asa", "az", "bal", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"da"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1 or t != 0 and i = 0,1
+ if ops.NEqualsAny(1) ||
+ !intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"is"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0
+ if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ !intEqualsAny(ops.T, 0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"mk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ceb", "fil", "tl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) ||
+ intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) ||
+ !intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"lv", "prg"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19
+ if ops.NModEqualsAny(10, 0) ||
+ ops.NModInRange(100, 11, 19) ||
+ intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) {
+ return Zero
+ }
+ // n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) ||
+ intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) ||
+ !intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"lag"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // i = 0,1 and n != 0
+ if intEqualsAny(ops.I, 0, 1) && !ops.NEqualsAny(0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ksh"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"shi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0 or n = 1
+ if intEqualsAny(ops.I, 0) ||
+ ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2..10
+ if ops.NInRange(2, 10) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"mo", "ro"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // v != 0 or n = 0 or n % 100 = 2..19
+ if !intEqualsAny(ops.V, 0) ||
+ ops.NEqualsAny(0) ||
+ ops.NModInRange(100, 2, 19) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"bs", "hr", "sh", "sr"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) ||
+ intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"fr"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0,1
+ if intEqualsAny(ops.I, 0, 1) {
+ return One
+ }
+ // e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
+ if intEqualsAny(ops.E, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
+ !intInRange(ops.E, 0, 5) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"pt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0..1
+ if intInRange(ops.I, 0, 1) {
+ return One
+ }
+ // e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
+ if intEqualsAny(ops.E, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
+ !intInRange(ops.E, 0, 5) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"it", "pt_PT"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
+ if intEqualsAny(ops.E, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
+ !intInRange(ops.E, 0, 5) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"es"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
+ if intEqualsAny(ops.E, 0) && !intEqualsAny(ops.I, 0) && intEqualsAny(ops.I%1000000, 0) && intEqualsAny(ops.V, 0) ||
+ !intInRange(ops.E, 0, 5) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"gd"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,11
+ if ops.NEqualsAny(1, 11) {
+ return One
+ }
+ // n = 2,12
+ if ops.NEqualsAny(2, 12) {
+ return Two
+ }
+ // n = 3..10,13..19
+ if ops.NInRange(3, 10) || ops.NInRange(13, 19) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"sl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 100 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) {
+ return One
+ }
+ // v = 0 and i % 100 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 3..4 or v != 0
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
+ !intEqualsAny(ops.V, 0) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"dsb", "hsb"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 100 = 1 or f % 100 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) ||
+ intEqualsAny(ops.F%100, 1) {
+ return One
+ }
+ // v = 0 and i % 100 = 2 or f % 100 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) ||
+ intEqualsAny(ops.F%100, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 3..4 or f % 100 = 3..4
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
+ intInRange(ops.F%100, 3, 4) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"he", "iw"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // i = 2 and v = 0
+ if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) {
+ return Two
+ }
+ // v = 0 and n != 0..10 and n % 10 = 0
+ if intEqualsAny(ops.V, 0) && !ops.NInRange(0, 10) && ops.NModEqualsAny(10, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"cs", "sk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // i = 2..4 and v = 0
+ if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) {
+ return Few
+ }
+ // v != 0
+ if !intEqualsAny(ops.V, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"pl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
+ return Few
+ }
+ // v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14
+ if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"be"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) {
+ return One
+ }
+ // n % 10 = 2..4 and n % 100 != 12..14
+ if ops.NModInRange(10, 2, 4) && !ops.NModInRange(100, 12, 14) {
+ return Few
+ }
+ // n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14
+ if ops.NModEqualsAny(10, 0) ||
+ ops.NModInRange(10, 5, 9) ||
+ ops.NModInRange(100, 11, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"lt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11..19
+ if ops.NModEqualsAny(10, 1) && !ops.NModInRange(100, 11, 19) {
+ return One
+ }
+ // n % 10 = 2..9 and n % 100 != 11..19
+ if ops.NModInRange(10, 2, 9) && !ops.NModInRange(100, 11, 19) {
+ return Few
+ }
+ // f != 0
+ if !intEqualsAny(ops.F, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"mt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 0 or n % 100 = 2..10
+ if ops.NEqualsAny(0) ||
+ ops.NModInRange(100, 2, 10) {
+ return Few
+ }
+ // n % 100 = 11..19
+ if ops.NModInRange(100, 11, 19) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ru", "uk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
+ return Few
+ }
+ // v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"br"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11,71,91
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11, 71, 91) {
+ return One
+ }
+ // n % 10 = 2 and n % 100 != 12,72,92
+ if ops.NModEqualsAny(10, 2) && !ops.NModEqualsAny(100, 12, 72, 92) {
+ return Two
+ }
+ // n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
+ if (ops.NModInRange(10, 3, 4) || ops.NModEqualsAny(10, 9)) && !(ops.NModInRange(100, 10, 19) || ops.NModInRange(100, 70, 79) || ops.NModInRange(100, 90, 99)) {
+ return Few
+ }
+ // n != 0 and n % 1000000 = 0
+ if !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ga"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 3..6
+ if ops.NInRange(3, 6) {
+ return Few
+ }
+ // n = 7..10
+ if ops.NInRange(7, 10) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"gv"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) {
+ return One
+ }
+ // v = 0 and i % 10 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 0,20,40,60,80
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) {
+ return Few
+ }
+ // v != 0
+ if !intEqualsAny(ops.V, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"kw"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000
+ if ops.NModEqualsAny(100, 2, 22, 42, 62, 82) ||
+ ops.NModEqualsAny(1000, 0) && (ops.NModInRange(100000, 1000, 20000) || ops.NModEqualsAny(100000, 40000, 60000, 80000)) ||
+ !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 100000) {
+ return Two
+ }
+ // n % 100 = 3,23,43,63,83
+ if ops.NModEqualsAny(100, 3, 23, 43, 63, 83) {
+ return Few
+ }
+ // n != 1 and n % 100 = 1,21,41,61,81
+ if !ops.NEqualsAny(1) && ops.NModEqualsAny(100, 1, 21, 41, 61, 81) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"ar", "ars"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n % 100 = 3..10
+ if ops.NModInRange(100, 3, 10) {
+ return Few
+ }
+ // n % 100 = 11..99
+ if ops.NModInRange(100, 11, 99) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "cardinal", []string{"cy"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 3
+ if ops.NEqualsAny(3) {
+ return Few
+ }
+ // n = 6
+ if ops.NEqualsAny(6) {
+ return Many
+ }
+ return Other
+ },
+ })
+
+ addPluralRules(DefaultRules, "ordinal", []string{"af", "am", "an", "ar", "bg", "bs", "ce", "cs", "da", "de", "dsb", "el", "es", "et", "eu", "fa", "fi", "fy", "gl", "gsw", "he", "hr", "hsb", "ia", "id", "in", "is", "iw", "ja", "km", "kn", "ko", "ky", "lt", "lv", "ml", "mn", "my", "nb", "nl", "no", "pa", "pl", "prg", "ps", "pt", "root", "ru", "sd", "sh", "si", "sk", "sl", "sr", "sw", "ta", "te", "th", "tpi", "tr", "ur", "uz", "yue", "zh", "zu"}, &Rule{
+ PluralForms: newPluralFormSet(Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"sv"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1,2 and n % 100 != 11,12
+ if ops.NModEqualsAny(10, 1, 2) && !ops.NModEqualsAny(100, 11, 12) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"bal", "fil", "fr", "ga", "hy", "lo", "mo", "ms", "ro", "tl", "vi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"hu"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,5
+ if ops.NEqualsAny(1, 5) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"ne"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1..4
+ if ops.NInRange(1, 4) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"be"}, &Rule{
+ PluralForms: newPluralFormSet(Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 2,3 and n % 100 != 12,13
+ if ops.NModEqualsAny(10, 2, 3) && !ops.NModEqualsAny(100, 12, 13) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"uk"}, &Rule{
+ PluralForms: newPluralFormSet(Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 3 and n % 100 != 13
+ if ops.NModEqualsAny(10, 3) && !ops.NModEqualsAny(100, 13) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"tk"}, &Rule{
+ PluralForms: newPluralFormSet(Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 6,9 or n = 10
+ if ops.NModEqualsAny(10, 6, 9) ||
+ ops.NEqualsAny(10) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"kk"}, &Rule{
+ PluralForms: newPluralFormSet(Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 6 or n % 10 = 9 or n % 10 = 0 and n != 0
+ if ops.NModEqualsAny(10, 6) ||
+ ops.NModEqualsAny(10, 9) ||
+ ops.NModEqualsAny(10, 0) && !ops.NEqualsAny(0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"it", "sc", "scn"}, &Rule{
+ PluralForms: newPluralFormSet(Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 11,8,80,800
+ if ops.NEqualsAny(11, 8, 80, 800) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"lij"}, &Rule{
+ PluralForms: newPluralFormSet(Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 11,8,80..89,800..899
+ if ops.NInRange(80, 89) || ops.NInRange(800, 899) || ops.NEqualsAny(11, 8) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"ka"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1
+ if intEqualsAny(ops.I, 1) {
+ return One
+ }
+ // i = 0 or i % 100 = 2..20,40,60,80
+ if intEqualsAny(ops.I, 0) ||
+ (intInRange(ops.I%100, 2, 20) || intEqualsAny(ops.I%100, 40, 60, 80)) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"sq"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n % 10 = 4 and n % 100 != 14
+ if ops.NModEqualsAny(10, 4) && !ops.NModEqualsAny(100, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"kw"}, &Rule{
+ PluralForms: newPluralFormSet(One, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1..4 or n % 100 = 1..4,21..24,41..44,61..64,81..84
+ if ops.NInRange(1, 4) ||
+ (ops.NModInRange(100, 1, 4) || ops.NModInRange(100, 21, 24) || ops.NModInRange(100, 41, 44) || ops.NModInRange(100, 61, 64) || ops.NModInRange(100, 81, 84)) {
+ return One
+ }
+ // n = 5 or n % 100 = 5
+ if ops.NEqualsAny(5) ||
+ ops.NModEqualsAny(100, 5) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"en"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) {
+ return One
+ }
+ // n % 10 = 2 and n % 100 != 12
+ if ops.NModEqualsAny(10, 2) && !ops.NModEqualsAny(100, 12) {
+ return Two
+ }
+ // n % 10 = 3 and n % 100 != 13
+ if ops.NModEqualsAny(10, 3) && !ops.NModEqualsAny(100, 13) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"mr"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2,3
+ if ops.NEqualsAny(2, 3) {
+ return Two
+ }
+ // n = 4
+ if ops.NEqualsAny(4) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"gd"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,11
+ if ops.NEqualsAny(1, 11) {
+ return One
+ }
+ // n = 2,12
+ if ops.NEqualsAny(2, 12) {
+ return Two
+ }
+ // n = 3,13
+ if ops.NEqualsAny(3, 13) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"ca"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,3
+ if ops.NEqualsAny(1, 3) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 4
+ if ops.NEqualsAny(4) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"mk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i % 10 = 1 and i % 100 != 11
+ if intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
+ return One
+ }
+ // i % 10 = 2 and i % 100 != 12
+ if intEqualsAny(ops.I%10, 2) && !intEqualsAny(ops.I%100, 12) {
+ return Two
+ }
+ // i % 10 = 7,8 and i % 100 != 17,18
+ if intEqualsAny(ops.I%10, 7, 8) && !intEqualsAny(ops.I%100, 17, 18) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"az"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i % 10 = 1,2,5,7,8 or i % 100 = 20,50,70,80
+ if intEqualsAny(ops.I%10, 1, 2, 5, 7, 8) ||
+ intEqualsAny(ops.I%100, 20, 50, 70, 80) {
+ return One
+ }
+ // i % 10 = 3,4 or i % 1000 = 100,200,300,400,500,600,700,800,900
+ if intEqualsAny(ops.I%10, 3, 4) ||
+ intEqualsAny(ops.I%1000, 100, 200, 300, 400, 500, 600, 700, 800, 900) {
+ return Few
+ }
+ // i = 0 or i % 10 = 6 or i % 100 = 40,60,90
+ if intEqualsAny(ops.I, 0) ||
+ intEqualsAny(ops.I%10, 6) ||
+ intEqualsAny(ops.I%100, 40, 60, 90) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"gu", "hi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2,3
+ if ops.NEqualsAny(2, 3) {
+ return Two
+ }
+ // n = 4
+ if ops.NEqualsAny(4) {
+ return Few
+ }
+ // n = 6
+ if ops.NEqualsAny(6) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"as", "bn"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,5,7,8,9,10
+ if ops.NEqualsAny(1, 5, 7, 8, 9, 10) {
+ return One
+ }
+ // n = 2,3
+ if ops.NEqualsAny(2, 3) {
+ return Two
+ }
+ // n = 4
+ if ops.NEqualsAny(4) {
+ return Few
+ }
+ // n = 6
+ if ops.NEqualsAny(6) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"or"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,5,7..9
+ if ops.NInRange(7, 9) || ops.NEqualsAny(1, 5) {
+ return One
+ }
+ // n = 2,3
+ if ops.NEqualsAny(2, 3) {
+ return Two
+ }
+ // n = 4
+ if ops.NEqualsAny(4) {
+ return Few
+ }
+ // n = 6
+ if ops.NEqualsAny(6) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(DefaultRules, "ordinal", []string{"cy"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0,7,8,9
+ if ops.NEqualsAny(0, 7, 8, 9) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 3,4
+ if ops.NEqualsAny(3, 4) {
+ return Few
+ }
+ // n = 5,6
+ if ops.NEqualsAny(5, 6) {
+ return Many
+ }
+ return Other
+ },
+ })
+}
diff --git a/modules/translation/i18n/plurals/rules_gen_test.go b/modules/translation/i18n/plurals/rules_gen_test.go
new file mode 100644
index 0000000000000..b69f1bc1f3c22
--- /dev/null
+++ b/modules/translation/i18n/plurals/rules_gen_test.go
@@ -0,0 +1,1083 @@
+// This file is generated by modules/translation/i18n/plurals/generate/main/generate.go DO NOT EDIT
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package plurals
+
+import "testing"
+
+func Test0cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0~15", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"bm", "bo", "dz", "hnj", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "tpi", "vi", "wo", "yo", "yue", "zh"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test1cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~1.0", "0.00~0.04"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"1.1~2.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test2cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~1.5"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ff", "hy", "kab"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test3cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "ji", "lij", "nl", "sc", "scn", "sv", "sw", "ur", "yi"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test4cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0", "0.1", "1.0", "0.00", "0.01", "1.00", "0.000", "0.001", "1.000", "0.0000", "0.0001", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.2~0.9", "1.1~1.8", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"si"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test5cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0", "1.0", "0.00", "1.00", "0.000", "1.000", "0.0000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ak", "bho", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test6cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1", "11~24"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0", "1.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "20.0", "21.0", "22.0", "23.0", "24.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~10", "100~106", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"tzm"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test7cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"af", "an", "asa", "az", "bal", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test8cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1~1.6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "2.0~3.4", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"da"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test9cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1~1.6", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"is"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test10cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "0.2~1.0", "1.2~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"mk"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test11cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0~3", "5", "7", "8", "10~13", "15", "17", "18", "20", "21", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~0.3", "0.5", "0.7", "0.8", "1.0~1.3", "1.5", "1.7", "1.8", "2.0", "2.1", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"4", "6", "9", "14", "16", "19", "24", "26", "104", "1004"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.4", "0.6", "0.9", "1.4", "1.6", "1.9", "2.4", "2.6", "10.4", "100.4", "1000.4"})
+
+ locales := []string{"ceb", "fil", "tl"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test12cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1", "1.0", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~9", "22~29", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.2~0.9", "1.2~1.9", "10.2", "100.2", "1000.2"})
+
+ locales := []string{"lv", "prg"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test13cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1~1.6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"lag"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test14cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ksh"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test15cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "2.00", "2.000", "2.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test16cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~1.0", "0.00~0.04"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~10"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "2.00", "3.00", "4.00", "5.00", "6.00", "7.00", "8.00"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"11~26", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"1.1~1.9", "2.1~2.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"shi"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test17cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"0", "2~16", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"mo", "ro"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test18cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"0.2~0.4", "1.2~1.4", "2.2~2.4", "3.2~3.4", "4.2~4.4", "5.2", "10.2", "100.2", "1000.2"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"bs", "hr", "sh", "sr"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test19cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~1.5"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"1000000", "1c6", "2c6", "3c6", "4c6", "5c6", "6c6"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"1.0000001c6", "1.1c6", "2.0000001c6", "2.1c6", "3.0000001c6", "3.1c6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1c3", "2c3", "3c3", "4c3", "5c3", "6c3"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001c3", "1.1c3", "2.0001c3", "2.1c3", "3.0001c3", "3.1c3"})
+
+ locales := []string{"fr"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test20cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"0", "1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.0~1.5"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"1000000", "1c6", "2c6", "3c6", "4c6", "5c6", "6c6"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"1.0000001c6", "1.1c6", "2.0000001c6", "2.1c6", "3.0000001c6", "3.1c6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"2~17", "100", "1000", "10000", "100000", "1c3", "2c3", "3c3", "4c3", "5c3", "6c3"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001c3", "1.1c3", "2.0001c3", "2.1c3", "3.0001c3", "3.1c3"})
+
+ locales := []string{"pt"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test21cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"1000000", "1c6", "2c6", "3c6", "4c6", "5c6", "6c6"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"1.0000001c6", "1.1c6", "2.0000001c6", "2.1c6", "3.0000001c6", "3.1c6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1c3", "2c3", "3c3", "4c3", "5c3", "6c3"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001c3", "1.1c3", "2.0001c3", "2.1c3", "3.0001c3", "3.1c3"})
+
+ locales := []string{"it", "pt_PT"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test22cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"1000000", "1c6", "2c6", "3c6", "4c6", "5c6", "6c6"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"1.0000001c6", "1.1c6", "2.0000001c6", "2.1c6", "3.0000001c6", "3.1c6"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1c3", "2c3", "3c3", "4c3", "5c3", "6c3"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0", "1.0001c3", "1.1c3", "2.0001c3", "2.1c3", "3.0001c3", "3.1c3"})
+
+ locales := []string{"es"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test23cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "11"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "11.0", "1.00", "11.00", "1.000", "11.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "12"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "12.0", "2.00", "12.00", "2.000", "12.000", "2.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3~10", "13~19"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "3.00"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "20~34", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"gd"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test24cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"sl"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test25cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"0.2", "1.2", "2.2", "3.2", "4.2", "5.2", "6.2", "7.2", "10.2", "100.2", "1000.2"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"0.3", "0.4", "1.3", "1.4", "2.3", "2.4", "3.3", "3.4", "4.3", "4.4", "5.3", "5.4", "6.3", "6.4", "7.3", "7.4", "10.3", "100.3", "1000.3"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"dsb", "hsb"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test26cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"20", "30", "40", "50", "60", "70", "80", "90", "100", "1000", "10000", "100000", "1000000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "3~17", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"he", "iw"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test27cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~4"})
+
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"cs", "sk"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test28cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"pl"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test29cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"2.0", "3.0", "4.0", "22.0", "23.0", "24.0", "32.0", "33.0", "102.0", "1002.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"0.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "11.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
+
+ locales := []string{"be"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test30cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~9", "22~29", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "22.0", "102.0", "1002.0"})
+
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"lt"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test31cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"0", "2~10", "102~107", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "102.0", "1002.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"11~19", "111~117", "1011"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"mt"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test32cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ru", "uk"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test33cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "21", "31", "41", "51", "61", "81", "101", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "81.0", "101.0", "1001.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "22", "32", "42", "52", "62", "82", "102", "1002"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "22.0", "32.0", "42.0", "52.0", "62.0", "82.0", "102.0", "1002.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3", "4", "9", "23", "24", "29", "33", "34", "39", "43", "44", "49", "103", "1003"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "4.0", "9.0", "23.0", "24.0", "29.0", "33.0", "34.0", "103.0", "1003.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"1000000.0", "1000000.00", "1000000.000", "1000000.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "5~8", "10~20", "100", "1000", "10000", "100000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0"})
+
+ locales := []string{"br"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test34cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "2.00", "2.000", "2.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3~6"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "4.0", "5.0", "6.0", "3.00", "4.00", "5.00", "6.00", "3.000", "4.000", "5.000", "6.000", "3.0000", "4.0000", "5.0000", "6.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"7~10"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"7.0", "8.0", "9.0", "10.0", "7.00", "8.00", "9.00", "10.00", "7.000", "8.000", "9.000", "10.000", "7.0000", "8.0000", "9.0000", "10.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"0", "11~25", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ga"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test35cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "12", "22", "32", "42", "52", "62", "72", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"0", "20", "40", "60", "80", "100", "120", "140", "1000", "10000", "100000", "1000000"})
+
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"3~10", "13~19", "23", "103", "1003"})
+
+ locales := []string{"gv"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test36cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2", "22", "42", "62", "82", "102", "122", "142", "1000", "10000", "100000"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "22.0", "42.0", "62.0", "82.0", "102.0", "122.0", "142.0", "1000.0", "10000.0", "100000.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3", "23", "43", "63", "83", "103", "123", "143", "1003"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "23.0", "43.0", "63.0", "83.0", "103.0", "123.0", "143.0", "1003.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"21", "41", "61", "81", "101", "121", "141", "161", "1001"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"21.0", "41.0", "61.0", "81.0", "101.0", "121.0", "141.0", "161.0", "1001.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"4~19", "100", "1004", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.1", "1000000.0"})
+
+ locales := []string{"kw"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test37cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "2.00", "2.000", "2.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3~10", "103~110", "1003"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "103.0", "1003.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"11~26", "111", "1011"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"100~102", "200~202", "300~302", "400~402", "500~502", "600", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"ar", "ars"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test38cardinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "cardinal", Zero, []string{"0"})
+ tests = appendDecimalTests(tests, "cardinal", Zero, []string{"0.0", "0.00", "0.000", "0.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", One, []string{"1"})
+ tests = appendDecimalTests(tests, "cardinal", One, []string{"1.0", "1.00", "1.000", "1.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Two, []string{"2"})
+ tests = appendDecimalTests(tests, "cardinal", Two, []string{"2.0", "2.00", "2.000", "2.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Few, []string{"3"})
+ tests = appendDecimalTests(tests, "cardinal", Few, []string{"3.0", "3.00", "3.000", "3.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Many, []string{"6"})
+ tests = appendDecimalTests(tests, "cardinal", Many, []string{"6.0", "6.00", "6.000", "6.0000"})
+
+ tests = appendIntegerTests(tests, "cardinal", Other, []string{"4", "5", "7~20", "100", "1000", "10000", "100000", "1000000"})
+ tests = appendDecimalTests(tests, "cardinal", Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"})
+
+ locales := []string{"cy"}
+ for _, locale := range locales {
+ runTests(t, locale, "cardinal", tests)
+ }
+}
+
+func Test0ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~15", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"af", "am", "an", "ar", "bg", "bs", "ce", "cs", "da", "de", "dsb", "el", "es", "et", "eu", "fa", "fi", "fy", "gl", "gsw", "he", "hr", "hsb", "ia", "id", "in", "is", "iw", "ja", "km", "kn", "ko", "ky", "lt", "lv", "ml", "mn", "my", "nb", "nl", "no", "pa", "pl", "prg", "ps", "pt", "root", "ru", "sd", "sh", "si", "sk", "sl", "sr", "sw", "ta", "te", "th", "tpi", "tr", "ur", "uz", "yue", "zh", "zu"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test1ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "2", "21", "22", "31", "32", "41", "42", "51", "52", "61", "62", "71", "72", "81", "82", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"sv"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test2ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"bal", "fil", "fr", "ga", "hy", "lo", "mo", "ms", "ro", "tl", "vi"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test3ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "5"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "2~4", "6~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"hu"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test4ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1~4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"ne"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test5ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"2", "3", "22", "23", "32", "33", "42", "43", "52", "53", "62", "63", "72", "73", "82", "83", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "1", "4~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"be"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test6ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"3", "23", "33", "43", "53", "63", "73", "83", "103", "1003"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~2", "4~16", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"uk"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test7ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"6", "9", "10", "16", "19", "26", "29", "36", "39", "106", "1006"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~5", "7", "8", "11~15", "17", "18", "20", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"tk"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test8ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"6", "9", "10", "16", "19", "20", "26", "29", "30", "36", "39", "40", "100", "1000", "10000", "100000", "1000000"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~5", "7", "8", "11~15", "17", "18", "21", "101", "1001"})
+
+ locales := []string{"kk"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test9ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"8", "11", "80", "800"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~7", "9", "10", "12~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"it", "sc", "scn"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test10ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"8", "11", "80~89", "800~803"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0~7", "9", "10", "12~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"lij"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test11ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"0", "2~16", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"21~36", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"ka"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test12ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"4", "24", "34", "44", "54", "64", "74", "84", "104", "1004"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "2", "3", "5~17", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"sq"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test13ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1~4", "21~24", "41~44", "61~64", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"5", "105", "205", "305", "405", "505", "605", "705", "1005"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "6~20", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"kw"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test14ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "22", "32", "42", "52", "62", "72", "82", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"3", "23", "33", "43", "53", "63", "73", "83", "103", "1003"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "4~18", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"en"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test15ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "3"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"mr"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test16ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "11"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "12"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"3", "13"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "4~10", "14~21", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"gd"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test17ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "3"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"ca"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test18ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "22", "32", "42", "52", "62", "72", "82", "102", "1002"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"7", "8", "27", "28", "37", "38", "47", "48", "57", "58", "67", "68", "77", "78", "87", "88", "107", "1007"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "3~6", "9~19", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"mk"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test19ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "2", "5", "7", "8", "11", "12", "15", "17", "18", "20~22", "25", "101", "1001"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"3", "4", "13", "14", "23", "24", "33", "34", "43", "44", "53", "54", "63", "64", "73", "74", "100", "1003"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"0", "6", "16", "26", "36", "40", "46", "56", "106", "1006"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"9", "10", "19", "29", "30", "39", "49", "59", "69", "79", "109", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"az"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test20ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "3"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"6"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "5", "7~20", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"gu", "hi"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test21ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "5", "7~10"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "3"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"6"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "11~25", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"as", "bn"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test22ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1", "5", "7~9"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2", "3"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"6"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"0", "10~24", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"or"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
+
+func Test23ordinal(t *testing.T) {
+ var tests []pluralFormTest
+
+ tests = appendIntegerTests(tests, "ordinal", Zero, []string{"0", "7~9"})
+
+ tests = appendIntegerTests(tests, "ordinal", One, []string{"1"})
+
+ tests = appendIntegerTests(tests, "ordinal", Two, []string{"2"})
+
+ tests = appendIntegerTests(tests, "ordinal", Few, []string{"3", "4"})
+
+ tests = appendIntegerTests(tests, "ordinal", Many, []string{"5", "6"})
+
+ tests = appendIntegerTests(tests, "ordinal", Other, []string{"10~25", "100", "1000", "10000", "100000", "1000000"})
+
+ locales := []string{"cy"}
+ for _, locale := range locales {
+ runTests(t, locale, "ordinal", tests)
+ }
+}
diff --git a/modules/translation/i18n/plurals/rules_test.go b/modules/translation/i18n/plurals/rules_test.go
new file mode 100644
index 0000000000000..7af8757d824a8
--- /dev/null
+++ b/modules/translation/i18n/plurals/rules_test.go
@@ -0,0 +1,81 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This file is heavily inspired by https://github.com/nicksnyder/go-i18n/tree/main/v2/internal/plural
+
+package plurals
+
+import (
+ "testing"
+)
+
+func TestRules(t *testing.T) {
+ expectedRule := &Rule{}
+
+ testCases := []struct {
+ name string
+ rules Rules
+ locale string
+ rule *Rule
+ }{
+ {
+ name: "exact match",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "en": expectedRule,
+ "es": {},
+ }},
+ locale: "en",
+ rule: expectedRule,
+ },
+ {
+ name: "inexact match",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "en": expectedRule,
+ }},
+ locale: "en-US",
+ rule: expectedRule,
+ },
+ {
+ name: "portuguese doesn't match european portuguese",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "pt-PT": {},
+ }},
+ locale: "pt",
+ rule: nil,
+ },
+ {
+ name: "european portuguese preferred",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "pt": {},
+ "pt-PT": expectedRule,
+ }},
+ locale: "pt-PT",
+ rule: expectedRule,
+ },
+ {
+ name: "zh-Hans",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "zh": expectedRule,
+ }},
+ locale: "zh-Hans",
+ rule: expectedRule,
+ },
+ {
+ name: "zh-Hant",
+ rules: Rules{CardinalMap: map[string]*Rule{
+ "zh": expectedRule,
+ }},
+ locale: "zh-Hant",
+ rule: expectedRule,
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ if rule := testCase.rules.Rule(testCase.locale); rule != testCase.rule {
+ panic(rule)
+ }
+ })
+ }
+}
diff --git a/modules/translation/i18n/translatable.go b/modules/translation/i18n/translatable.go
new file mode 100644
index 0000000000000..3ae5c18cae6f7
--- /dev/null
+++ b/modules/translation/i18n/translatable.go
@@ -0,0 +1,50 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package i18n
+
+import "fmt"
+
+// Locale represents an interface to translation
+type Locale interface {
+ // Has reports if a locale has a translation for a given key
+ Has(trKey string) bool
+
+ // Tr translates a given key and arguments for a language
+ Tr(key string, args ...interface{}) string
+
+ // TrPlural translates a given key and arguments for a language
+ TrPlural(cnt interface{}, key string, args ...interface{}) string
+
+ // TrOrdinal translates a given key and arguments for a language
+ TrOrdinal(cnt interface{}, key string, args ...interface{}) string
+}
+
+// TranslatableFormatted structs provide their own translated string when formatted in translation
+type TranslatableFormatted interface {
+ TranslatedFormat(l Locale, s fmt.State, c rune)
+}
+
+// TranslatableStringer structs provide their own translated string when formatted as a string in translation
+type TranslatableStringer interface {
+ TranslatedString(l Locale) string
+}
+
+type formatWrapper struct {
+ l Locale
+ t TranslatableFormatted
+}
+
+func (f formatWrapper) Format(s fmt.State, c rune) {
+ f.t.TranslatedFormat(f.l, s, c)
+}
+
+type stringWrapper struct {
+ l Locale
+ t TranslatableStringer
+}
+
+func (s stringWrapper) String() string {
+ return s.t.TranslatedString(s.l)
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index fc311aa61b4ca..2290eb8ff4146 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -24,6 +24,8 @@ type Locale interface {
Language() string
Tr(string, ...interface{}) string
TrN(cnt interface{}, key1, keyN string, args ...interface{}) string
+ TrPlural(cnt interface{}, key string, args ...interface{}) string
+ TrOrdinal(cnt interface{}, key string, args ...interface{}) string
}
// LangType represents a lang type
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1566dfc97d422..360f9bd77c6c0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -387,8 +387,7 @@ issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s.
issue.x_mentioned_you = @%s mentioned you:
issue.action.force_push = %[1]s force-pushed the %[2]s from %[3]s to %[4]s.
-issue.action.push_1 = @%[1]s pushed %[3]d commit to %[2]s
-issue.action.push_n = @%[1]s pushed %[3]d commits to %[2]s
+issue.action.push_plural = @%[1]s pushed %[3]d {{if .One}}commit{{else}}commits{{end}} to %[2]s
issue.action.close = @%[1]s closed #%[2]d.
issue.action.reopen = @%[1]s reopened #%[2]d.
issue.action.merge = @%[1]s merged #%[2]d into %[3]s.
@@ -940,8 +939,7 @@ archive.title = This repo is archived. You can view files and clone it, but cann
archive.issue.nocomment = This repo is archived. You cannot comment on issues.
archive.pull.nocomment = This repo is archived. You cannot comment on pull requests.
-form.reach_limit_of_creation_1 = You have already reached your limit of %d repository.
-form.reach_limit_of_creation_n = You have already reached your limit of %d repositories.
+form.reach_limit_of_creation_plural = You have already reached your limit of %d {{if .One}}repository{{else}}repositories{{end}}.
form.name_reserved = The repository name '%s' is reserved.
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name.
@@ -1021,6 +1019,7 @@ broken_message = The Git data underlying this repository cannot be read. Contact
code = Code
code.desc = Access source code, files, commits and branches.
branch = Branch
+branch_plural = {{if .One}}Branch{{else}}Branches{{end}}
tree = Tree
clear_ref = `Clear current reference`
filter_branch_and_tag = Filter branch or tag
@@ -1038,9 +1037,11 @@ org_labels_desc_manage = manage
milestones = Milestones
commits = Commits
commit = Commit
+commit_plural = {{if .One}}Commit{{else}}Commits{{end}}
release = Release
releases = Releases
tag = Tag
+tag_plural = {{if .One}}Tag{{else}}Tags{{end}}
released_this = released this
file.title = %s at %s
file_raw = Raw
@@ -1076,6 +1077,7 @@ download_file = Download file
normal_view = Normal View
line = line
lines = lines
+line_plural = {{if .One}}line{{else}}lines{{end}}
editor.add_file = Add File
editor.new_file = New File
@@ -1258,8 +1260,10 @@ issues.label_templates.use = Use Label Set
issues.label_templates.fail_to_load_file = Failed to load label template file '%s': %v
issues.add_label = added the %s label %s
issues.add_labels = added the %s labels %s
+issues.add_label_plural = added the %s {{if .One}}label{{else}}labels{{end}} %s
issues.remove_label = removed the %s label %s
issues.remove_labels = removed the %s labels %s
+issues.remove_label_plural = removed the %s {{if .One}}label{{else}}labels{{end}} %s
issues.add_remove_labels = added %s and removed %s labels %s
issues.add_milestone_at = `added this to the %s milestone %s`
issues.add_project_at = `added this to the %s project %s`
@@ -1431,8 +1435,7 @@ issues.due_date = Due Date
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
issues.error_modifying_due_date = "Failed to modify the due date."
issues.error_removing_due_date = "Failed to remove the due date."
-issues.push_commit_1 = "added %d commit %s"
-issues.push_commits_n = "added %d commits %s"
+issues.push_commit_plural = "added %d {{if .One}}commit{{else}}commits{{end}} %s"
issues.force_push_codes = `force-pushed %[1]s from %[2]s
to %[4]s
%[6]s`
issues.due_date_form = "yyyy-mm-dd"
issues.due_date_form_add = "Add due date"
@@ -1562,19 +1565,14 @@ pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals ye
pulls.blocked_by_rejection = "This Pull Request has changes requested by an official reviewer."
pulls.blocked_by_official_review_requests = "This Pull Request has official review requests."
pulls.blocked_by_outdated_branch = "This Pull Request is blocked because it's outdated."
-pulls.blocked_by_changed_protected_files_1= "This Pull Request is blocked because it changes a protected file:"
-pulls.blocked_by_changed_protected_files_n= "This Pull Request is blocked because it changes protected files:"
+pulls.blocked_by_changed_protected_files_plural = "This Pull Request is blocked because it changes {{if .One}}a protected file{{else}}protected files{{end}}:"
pulls.can_auto_merge_desc = This pull request can be merged automatically.
pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts.
pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
-pulls.num_conflicting_files_1 = "%d conflicting file"
-pulls.num_conflicting_files_n = "%d conflicting files"
-pulls.approve_count_1 = "%d approval"
-pulls.approve_count_n = "%d approvals"
-pulls.reject_count_1 = "%d change request"
-pulls.reject_count_n = "%d change requests"
-pulls.waiting_count_1 = "%d waiting review"
-pulls.waiting_count_n = "%d waiting reviews"
+pulls.num_conflicting_files_plural = "%d conflicting {{if .One}}file{{else}}files{{end}}"
+pulls.approve_count_plural = "%d {{if .One}}approval{{else}}approvals{{end}}"
+pulls.reject_count_plural = "%d change {{if .One}}request{{else}}requests{{end}}"
+pulls.waiting_count_plural = "%d waiting {{if .One}}review{{else}}reviews{{end}}"
pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
@@ -1717,61 +1715,43 @@ activity.period.quarterly = 3 months
activity.period.semiyearly = 6 months
activity.period.yearly = 1 year
activity.overview = Overview
-activity.active_prs_count_1 = %d Active Pull Request
-activity.active_prs_count_n = %d Active Pull Requests
-activity.merged_prs_count_1 = Merged Pull Request
-activity.merged_prs_count_n = Merged Pull Requests
-activity.opened_prs_count_1 = Proposed Pull Request
-activity.opened_prs_count_n = Proposed Pull Requests
-activity.title.user_1 = %d user
-activity.title.user_n = %d users
-activity.title.prs_1 = %d Pull request
-activity.title.prs_n = %d Pull requests
+activity.active_prs_count_plural = %d Active Pull {{if .One}}Request{{else}}Requests{{end}}
+activity.merged_prs_count_plural = Merged Pull {{if .One}}Request{{else}}Requests{{end}}
+activity.opened_prs_count_plural = Proposed Pull {{if .One}}Request{{else}}Requests{{end}}
+activity.title.user_plural = %d {{if .One}}user{{else}}users{{end}}
+activity.title.prs_plural = %d Pull {{if .One}}request{{else}}requests{{end}}
activity.title.prs_merged_by = %s merged by %s
activity.title.prs_opened_by = %s proposed by %s
activity.merged_prs_label = Merged
activity.opened_prs_label = Proposed
-activity.active_issues_count_1 = %d Active Issue
-activity.active_issues_count_n = %d Active Issues
-activity.closed_issues_count_1 = Closed Issue
-activity.closed_issues_count_n = Closed Issues
-activity.title.issues_1 = %d Issue
-activity.title.issues_n = %d Issues
-activity.title.issues_closed_from = %s closed from %s
-activity.title.issues_created_by = %s created by %s
+activity.active_issues_count_plural = %d Active {{if .One}}Issue{{else}}Issues{{end}}
+activity.closed_issues_count_plural = Closed {{if .One}}Issue{{else}}Issues{{end}}
+activity.title.issues_plural = %d {{if .One}}Issue{{else}}Issues{{end}}
+activity.title.issues_closed_from = %closed from s
+activity.title.issues_created_by = %created by s
activity.closed_issue_label = Closed
-activity.new_issues_count_1 = New Issue
-activity.new_issues_count_n = New Issues
+activity.new_issues_count_plural = New {{if .One}}Issue{{else}}Issues{{end}}
activity.new_issue_label = Opened
-activity.title.unresolved_conv_1 = %d Unresolved Conversation
-activity.title.unresolved_conv_n = %d Unresolved Conversations
+activity.title.unresolved_conv_plural = %d Unresolved {{if .One}}Conversation{{else}}Conversations{{end}}
activity.unresolved_conv_desc = These recently changed issues and pull requests have not been resolved yet.
activity.unresolved_conv_label = Open
-activity.title.releases_1 = %d Release
-activity.title.releases_n = %d Releases
+activity.title.releases_plural = %d {{if .One}}Release{{else}}Releases{{end}}
activity.title.releases_published_by = %s published by %s
activity.published_release_label = Published
activity.no_git_activity = There has not been any commit activity in this period.
activity.git_stats_exclude_merges = Excluding merges,
-activity.git_stats_author_1 = %d author
-activity.git_stats_author_n = %d authors
-activity.git_stats_pushed_1 = has pushed
-activity.git_stats_pushed_n = have pushed
-activity.git_stats_commit_1 = %d commit
-activity.git_stats_commit_n = %d commits
+activity.git_stats_author_plural = %d {{if .One}}author{{else}}authors{{end}}
+activity.git_stats_pushed_plural = {{if .One}}has{{else}}have{{end}} pushed
+activity.git_stats_commit_plural = %d {{if .One}}commit{{else}}commits{{end}}
activity.git_stats_push_to_branch = to %s and
activity.git_stats_push_to_all_branches = to all branches.
activity.git_stats_on_default_branch = On %s,
-activity.git_stats_file_1 = %d file
-activity.git_stats_file_n = %d files
-activity.git_stats_files_changed_1 = has changed
-activity.git_stats_files_changed_n = have changed
+activity.git_stats_file_plural = %d {{if .One}}file{{else}}files{{end}}
+activity.git_stats_files_changed_plural = {{if .One}}has{{else}}have{{end}} changed
activity.git_stats_additions = and there have been
-activity.git_stats_addition_1 = %d addition
-activity.git_stats_addition_n = %d additions
+activity.git_stats_addition_plural = %d {{if .One}}addition{{else}}additions{{end}}
activity.git_stats_and_deletions = and
-activity.git_stats_deletion_1 = %d deletion
-activity.git_stats_deletion_n = %d deletions
+activity.git_stats_deletion_plural = %d {{if .One}}deletion{{else}}deletions{{end}}
search = Search
search.search_repo = Search repository
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index 393f8ed3d9316..27da66d50099f 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -82,7 +82,7 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case repo_model.IsErrReachLimitOfRepo(err):
maxCreationLimit := owner.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ msg := ctx.TrPlural(maxCreationLimit, "repo.form.reach_limit_of_creation_plural", maxCreationLimit)
ctx.RenderWithErr(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 3e746d3f058cf..1a067e59b48c2 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -167,7 +167,7 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error,
switch {
case repo_model.IsErrReachLimitOfRepo(err):
maxCreationLimit := owner.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ msg := ctx.TrPlural(maxCreationLimit, "repo.form.reach_limit_of_creation_plural", maxCreationLimit)
ctx.RenderWithErr(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index 2b5691ce88501..a46a6b9cb062b 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -640,7 +640,7 @@ func SettingsPost(ctx *context.Context) {
if !ctx.Repo.Owner.CanCreateRepo() {
maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ msg := ctx.TrPlural(maxCreationLimit, "repo.form.reach_limit_of_creation_plural", maxCreationLimit)
ctx.Flash.Error(msg)
ctx.Redirect(repo.Link() + "/settings")
return
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl
index d9f7aff4cce39..60438664e4f18 100644
--- a/templates/mail/issue/default.tmpl
+++ b/templates/mail/issue/default.tmpl
@@ -30,7 +30,7 @@
{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink | Str2html}}
{{else}}
- {{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
+ {{.locale.TrPlural (len .Comment.Commits) "mail.issue.action.push_plural" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
{{end}}
{{end}}
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index cc6ca95edbdb8..abf14c3f51207 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -41,7 +41,7 @@
{{end}}
- {{.locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
+ {{.locale.TrPlural .Activity.ActivePRCount "repo.activity.active_prs_count_plural" .Activity.ActivePRCount | Safe}}
{{end}}
{{if .Permission.CanRead $.UnitTypeIssues}}
@@ -56,7 +56,7 @@
{{end}}
- {{.locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
+ {{.locale.TrPlural .Activity.ActiveIssueCount "repo.activity.active_issues_count_plural" .Activity.ActiveIssueCount | Safe}}
{{end}}
@@ -64,21 +64,21 @@
{{if .Permission.CanRead $.UnitTypePullRequests}}
{{svg "octicon-git-pull-request"}} {{.Activity.MergedPRCount}}
- {{.locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
+ {{.locale.TrPlural .Activity.MergedPRCount "repo.activity.merged_prs_count_plural"}}
{{svg "octicon-git-branch"}} {{.Activity.OpenedPRCount}}
- {{.locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
+ {{.locale.TrPlural .Activity.OpenedPRCount "repo.activity.opened_prs_count_plural"}}
{{end}}
{{if .Permission.CanRead $.UnitTypeIssues}}
{{svg "octicon-issue-closed"}} {{.Activity.ClosedIssueCount}}
- {{.locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
+ {{.locale.TrPlural .Activity.ClosedIssueCount "repo.activity.closed_issues_count_plural"}}
{{svg "octicon-issue-opened"}} {{.Activity.OpenedIssueCount}}
- {{.locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
+ {{.locale.TrPlural .Activity.OpenedIssueCount "repo.activity.new_issues_count_plural"}}
{{end}}
@@ -94,19 +94,19 @@
{{.locale.Tr "repo.activity.git_stats_exclude_merges"}}
- {{.locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}
- {{.locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
- {{.locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}
+ {{.locale.TrPlural .Activity.Code.AuthorCount "repo.activity.git_stats_author_plural" .Activity.Code.AuthorCount}}
+ {{.locale.TrPlural .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_plural"}}
+ {{.locale.TrPlural .Activity.Code.CommitCount "repo.activity.git_stats_commit_plural" .Activity.Code.CommitCount}}
{{.locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
- {{.locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}
+ {{.locale.TrPlural .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_plural" .Activity.Code.CommitCountInAllBranches}}
{{.locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
{{.locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
- {{.locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}
- {{.locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
+ {{.locale.TrPlural .Activity.Code.ChangedFiles "repo.activity.git_stats_file_plural" .Activity.Code.ChangedFiles}}
+ {{.locale.TrPlural .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_plural"}}
{{.locale.Tr "repo.activity.git_stats_additions"}}
- {{.locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}
+ {{.locale.TrPlural .Activity.Code.Additions "repo.activity.git_stats_addition_plural" .Activity.Code.Additions}}
{{.locale.Tr "repo.activity.git_stats_and_deletions"}}
- {{.locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}.
+ {{.locale.TrPlural .Activity.Code.Deletions "repo.activity.git_stats_deletion_plural" .Activity.Code.Deletions}}.
@@ -119,8 +119,8 @@
@@ -141,8 +141,8 @@
@@ -160,8 +160,8 @@
@@ -179,8 +179,8 @@
@@ -198,8 +198,8 @@
@@ -216,7 +216,7 @@
{{if gt .Activity.UnresolvedIssueCount 0}}
{{.locale.Tr "repo.activity.unresolved_conv_desc"}}
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index b697573d24eaa..826cbff573fd6 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -3,7 +3,7 @@