forked from kevadesu/forgejo
Initial support for localization and pluralization with go-i18n-JSON-v2 format
This commit is contained in:
parent
376a2e19ea
commit
a2787bb09e
61 changed files with 1317 additions and 51 deletions
|
@ -22,20 +22,7 @@ func (k *KeyLocale) HasKey(trKey string) bool {
|
|||
|
||||
// TrHTML implements Locale.
|
||||
func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
||||
args := slices.Clone(trArgs)
|
||||
for i, v := range args {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
||||
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
||||
case string:
|
||||
args[i] = template.HTMLEscapeString(v)
|
||||
case fmt.Stringer:
|
||||
args[i] = template.HTMLEscapeString(v.String())
|
||||
default:
|
||||
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
return template.HTML(k.TrString(trKey, args...))
|
||||
return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...))
|
||||
}
|
||||
|
||||
// TrString implements Locale.
|
||||
|
@ -43,6 +30,11 @@ func (k *KeyLocale) TrString(trKey string, trArgs ...any) string {
|
|||
return FormatDummy(trKey, trArgs...)
|
||||
}
|
||||
|
||||
// TrPluralString implements Locale.
|
||||
func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
|
||||
return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...))
|
||||
}
|
||||
|
||||
func FormatDummy(trKey string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return fmt.Sprintf("(%s)", trKey)
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
|
||||
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
|
||||
ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist}
|
||||
ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist}
|
||||
ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist}
|
||||
ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument}
|
||||
)
|
||||
|
|
|
@ -8,11 +8,28 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
type (
|
||||
PluralFormIndex uint8
|
||||
PluralFormRule func(int64) PluralFormIndex
|
||||
)
|
||||
|
||||
const (
|
||||
PluralFormZero PluralFormIndex = iota
|
||||
PluralFormOne
|
||||
PluralFormTwo
|
||||
PluralFormFew
|
||||
PluralFormMany
|
||||
PluralFormOther
|
||||
)
|
||||
|
||||
var DefaultLocales = NewLocaleStore()
|
||||
|
||||
type Locale interface {
|
||||
// TrString translates a given key and arguments for a language
|
||||
TrString(trKey string, trArgs ...any) string
|
||||
// TrPluralString translates a given pluralized key and arguments for a language.
|
||||
// This function returns an error if new-style support for the given key is not available.
|
||||
TrPluralString(count any, trKey string, trArgs ...any) template.HTML
|
||||
// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
|
||||
TrHTML(trKey string, trArgs ...any) template.HTML
|
||||
// HasKey reports if a locale has a translation for a given key
|
||||
|
@ -31,8 +48,10 @@ type LocaleStore interface {
|
|||
Locale(langName string) (Locale, bool)
|
||||
// HasLang returns whether a given language is present in the store
|
||||
HasLang(langName string) bool
|
||||
// AddLocaleByIni adds a new language to the store
|
||||
AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error
|
||||
// AddLocaleByIni adds a new old-style language to the store
|
||||
AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error
|
||||
// AddLocaleByJSON adds new-style content to an existing language to the store
|
||||
AddToLocaleFromJSON(langName string, source []byte) error
|
||||
}
|
||||
|
||||
// ResetDefaultLocales resets the current default locales
|
||||
|
|
|
@ -12,6 +12,26 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex {
|
||||
if n == 0 {
|
||||
return PluralFormZero
|
||||
}
|
||||
if n == 1 {
|
||||
return PluralFormOne
|
||||
}
|
||||
if n >= 2 && n <= 4 {
|
||||
return PluralFormFew
|
||||
}
|
||||
return PluralFormOther
|
||||
}
|
||||
|
||||
var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex {
|
||||
if n == 1 {
|
||||
return PluralFormOne
|
||||
}
|
||||
return PluralFormOther
|
||||
}
|
||||
|
||||
func TestLocaleStore(t *testing.T) {
|
||||
testData1 := []byte(`
|
||||
.dot.name = Dot Name
|
||||
|
@ -27,11 +47,48 @@ fmt = %[2]s %[1]s
|
|||
|
||||
[section]
|
||||
sub = Changed Sub String
|
||||
commits = fallback value for commits
|
||||
`)
|
||||
|
||||
testDataJSON2 := []byte(`
|
||||
{
|
||||
"section.json": "the JSON is %s",
|
||||
"section.commits": {
|
||||
"one": "one %d commit",
|
||||
"few": "some %d commits",
|
||||
"other": "lots of %d commits"
|
||||
},
|
||||
"section.incomplete": {
|
||||
"few": "some %d objects (translated)"
|
||||
},
|
||||
"nested": {
|
||||
"outer": {
|
||||
"inner": {
|
||||
"json": "Hello World",
|
||||
"issue": {
|
||||
"one": "one %d issue",
|
||||
"few": "some %d issues",
|
||||
"other": "lots of %d issues"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
testDataJSON1 := []byte(`
|
||||
{
|
||||
"section.incomplete": {
|
||||
"one": "[untranslated] some %d object",
|
||||
"other": "[untranslated] some %d objects"
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil))
|
||||
require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1))
|
||||
require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2))
|
||||
ls.SetDefaultLang("lang1")
|
||||
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
|
@ -56,6 +113,45 @@ sub = Changed Sub String
|
|||
result2 := lang2.TrHTML("section.mixed", "a&b")
|
||||
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&b</span>`, result2)
|
||||
|
||||
result = lang2.TrString("section.json", "valid")
|
||||
assert.Equal(t, "the JSON is valid", result)
|
||||
|
||||
result = lang2.TrString("nested.outer.inner.json")
|
||||
assert.Equal(t, "Hello World", result)
|
||||
|
||||
result = lang2.TrString("section.commits")
|
||||
assert.Equal(t, "lots of %d commits", result)
|
||||
|
||||
result2 = lang2.TrPluralString(1, "section.commits", 1)
|
||||
assert.EqualValues(t, "one 1 commit", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(3, "section.commits", 3)
|
||||
assert.EqualValues(t, "some 3 commits", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(8, "section.commits", 8)
|
||||
assert.EqualValues(t, "lots of 8 commits", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(0, "section.commits")
|
||||
assert.EqualValues(t, "section.commits", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1)
|
||||
assert.EqualValues(t, "one 1 issue", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3)
|
||||
assert.EqualValues(t, "some 3 issues", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9)
|
||||
assert.EqualValues(t, "lots of 9 issues", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(3, "section.incomplete", 3)
|
||||
assert.EqualValues(t, "some 3 objects (translated)", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(1, "section.incomplete", 1)
|
||||
assert.EqualValues(t, "[untranslated] some 1 object", result2)
|
||||
|
||||
result2 = lang2.TrPluralString(7, "section.incomplete", 7)
|
||||
assert.EqualValues(t, "[untranslated] some 7 objects", result2)
|
||||
|
||||
langs, descs := ls.ListLangNameDesc()
|
||||
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
|
||||
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
|
||||
|
@ -77,7 +173,7 @@ c=22
|
|||
`)
|
||||
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2))
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
assert.Equal(t, "11", lang1.TrString("a"))
|
||||
assert.Equal(t, "21", lang1.TrString("b"))
|
||||
|
@ -118,7 +214,7 @@ func (e *errorPointerReceiver) Error() string {
|
|||
|
||||
func TestLocaleWithTemplate(t *testing.T) {
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), nil))
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
|
||||
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
|
||||
|
@ -181,7 +277,7 @@ func TestLocaleStoreQuirks(t *testing.T) {
|
|||
|
||||
for _, testData := range testDataList {
|
||||
ls := NewLocaleStore()
|
||||
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
|
||||
err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil)
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
require.NoError(t, err, testData.hint)
|
||||
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
|
||||
|
|
|
@ -8,8 +8,10 @@ import (
|
|||
"html/template"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// This file implements the static LocaleStore that will not watch for changes
|
||||
|
@ -18,6 +20,9 @@ type locale struct {
|
|||
store *localeStore
|
||||
langName string
|
||||
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
|
||||
|
||||
newStyleMessages map[string]string
|
||||
pluralRule PluralFormRule
|
||||
}
|
||||
|
||||
var _ Locale = (*locale)(nil)
|
||||
|
@ -38,8 +43,19 @@ func NewLocaleStore() LocaleStore {
|
|||
return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)}
|
||||
}
|
||||
|
||||
const (
|
||||
PluralFormSeparator string = "\036"
|
||||
)
|
||||
|
||||
// A note about pluralization rules.
|
||||
// go-i18n supports plural rules in theory.
|
||||
// In practice, it relies on another library that hardcodes a list of common languages
|
||||
// and their plural rules, and does not support languages not hardcoded there.
|
||||
// So we pretend that all languages are English and use our own function to extract
|
||||
// the correct plural form for a given count and language.
|
||||
|
||||
// AddLocaleByIni adds locale by ini into the store
|
||||
func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error {
|
||||
func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error {
|
||||
if _, ok := store.localeMap[langName]; ok {
|
||||
return ErrLocaleAlreadyExist
|
||||
}
|
||||
|
@ -47,7 +63,7 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
|
|||
store.langNames = append(store.langNames, langName)
|
||||
store.langDescs = append(store.langDescs, langDesc)
|
||||
|
||||
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
|
||||
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)}
|
||||
store.localeMap[l.langName] = l
|
||||
|
||||
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
|
||||
|
@ -78,6 +94,98 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
|
|||
return nil
|
||||
}
|
||||
|
||||
func RecursivelyAddTranslationsFromJSON(locale *locale, object map[string]any, prefix string) error {
|
||||
for key, value := range object {
|
||||
var fullkey string
|
||||
if prefix != "" {
|
||||
fullkey = prefix + "." + key
|
||||
} else {
|
||||
fullkey = key
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
// Check whether we are adding a plural form to the parent object, or a new nested JSON object.
|
||||
|
||||
if key == "zero" || key == "one" || key == "two" || key == "few" || key == "many" {
|
||||
locale.newStyleMessages[prefix+PluralFormSeparator+key] = v
|
||||
} else if key == "other" {
|
||||
locale.newStyleMessages[prefix] = v
|
||||
} else {
|
||||
locale.newStyleMessages[fullkey] = v
|
||||
}
|
||||
|
||||
case map[string]any:
|
||||
err := RecursivelyAddTranslationsFromJSON(locale, v, fullkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case nil:
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized JSON value '%s'", value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *localeStore) AddToLocaleFromJSON(langName string, source []byte) error {
|
||||
locale, ok := store.localeMap[langName]
|
||||
if !ok {
|
||||
return ErrLocaleDoesNotExist
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(source, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return RecursivelyAddTranslationsFromJSON(locale, result, "")
|
||||
}
|
||||
|
||||
func (l *locale) LookupNewStyleMessage(trKey string) string {
|
||||
if msg, ok := l.newStyleMessages[trKey]; ok {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *locale) LookupPlural(trKey string, count any) string {
|
||||
n, err := util.ToInt64(count)
|
||||
if err != nil {
|
||||
log.Error("Invalid plural count '%s'", count)
|
||||
return ""
|
||||
}
|
||||
|
||||
pluralForm := l.pluralRule(n)
|
||||
suffix := ""
|
||||
switch pluralForm {
|
||||
case PluralFormZero:
|
||||
suffix = PluralFormSeparator + "zero"
|
||||
case PluralFormOne:
|
||||
suffix = PluralFormSeparator + "one"
|
||||
case PluralFormTwo:
|
||||
suffix = PluralFormSeparator + "two"
|
||||
case PluralFormFew:
|
||||
suffix = PluralFormSeparator + "few"
|
||||
case PluralFormMany:
|
||||
suffix = PluralFormSeparator + "many"
|
||||
case PluralFormOther:
|
||||
// No suffix for the "other" string.
|
||||
default:
|
||||
log.Error("Invalid plural form index %d for count %d", pluralForm, count)
|
||||
return ""
|
||||
}
|
||||
|
||||
if result, ok := l.newStyleMessages[trKey+suffix]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
log.Error("Missing translation for plural form index %d for count %d", pluralForm, count)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (store *localeStore) HasLang(langName string) bool {
|
||||
_, ok := store.localeMap[langName]
|
||||
return ok
|
||||
|
@ -113,22 +221,37 @@ func (store *localeStore) Close() error {
|
|||
func (l *locale) TrString(trKey string, trArgs ...any) string {
|
||||
format := trKey
|
||||
|
||||
idx, ok := l.store.trKeyToIdxMap[trKey]
|
||||
found := false
|
||||
if ok {
|
||||
if msg, ok := l.idxToMsgMap[idx]; ok {
|
||||
format = msg // use the found translation
|
||||
found = true
|
||||
} else if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
// try to use default locale's translation
|
||||
if msg, ok := def.idxToMsgMap[idx]; ok {
|
||||
format = msg
|
||||
if msg := l.LookupNewStyleMessage(trKey); msg != "" {
|
||||
format = msg
|
||||
} else {
|
||||
// First fallback: old-style translation
|
||||
idx, ok := l.store.trKeyToIdxMap[trKey]
|
||||
found := false
|
||||
if ok {
|
||||
if msg, ok := l.idxToMsgMap[idx]; ok {
|
||||
format = msg // use the found translation
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Error("Missing translation %q", trKey)
|
||||
|
||||
if !found {
|
||||
// Second fallback: new-style default language
|
||||
if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
if msg := defaultLang.LookupNewStyleMessage(trKey); msg != "" {
|
||||
format = msg
|
||||
} else {
|
||||
// Third fallback: old-style default language
|
||||
if msg, ok := defaultLang.idxToMsgMap[idx]; ok {
|
||||
format = msg
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Error("Missing translation %q", trKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := Format(format, trArgs...)
|
||||
|
@ -138,7 +261,7 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
|
|||
return msg
|
||||
}
|
||||
|
||||
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
||||
func PrepareArgsForHTML(trArgs ...any) []any {
|
||||
args := slices.Clone(trArgs)
|
||||
for i, v := range args {
|
||||
switch v := v.(type) {
|
||||
|
@ -152,7 +275,30 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
|||
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
return template.HTML(l.TrString(trKey, args...))
|
||||
return args
|
||||
}
|
||||
|
||||
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
||||
return template.HTML(l.TrString(trKey, PrepareArgsForHTML(trArgs...)...))
|
||||
}
|
||||
|
||||
func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
|
||||
message := l.LookupPlural(trKey, count)
|
||||
|
||||
if message == "" {
|
||||
if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
message = defaultLang.LookupPlural(trKey, count)
|
||||
}
|
||||
if message == "" {
|
||||
message = trKey
|
||||
}
|
||||
}
|
||||
|
||||
message, err := Format(message, PrepareArgsForHTML(trArgs...)...)
|
||||
if err != nil {
|
||||
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
|
||||
}
|
||||
return template.HTML(message)
|
||||
}
|
||||
|
||||
// HasKey returns whether a key is present in this locale or not
|
||||
|
|
|
@ -31,6 +31,10 @@ func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
|||
return template.HTML(key1)
|
||||
}
|
||||
|
||||
func (l MockLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
|
||||
return template.HTML(trKey)
|
||||
}
|
||||
|
||||
func (l MockLocale) TrSize(s int64) ReadableSize {
|
||||
return ReadableSize{fmt.Sprint(s), ""}
|
||||
}
|
||||
|
|
253
modules/translation/plural_rules.go
Normal file
253
modules/translation/plural_rules.go
Normal file
|
@ -0,0 +1,253 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Some useful links:
|
||||
// https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html
|
||||
// https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information
|
||||
// https://github.com/WeblateOrg/language-data/blob/main/languages.csv
|
||||
// Note that in some cases there is ambiguity about the correct form for a given language. In this case, ask the locale's translators.
|
||||
|
||||
package translation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/translation/i18n"
|
||||
)
|
||||
|
||||
// The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync!
|
||||
const (
|
||||
PluralRuleDefault = 0
|
||||
PluralRuleBengali = 1
|
||||
PluralRuleIcelandic = 2
|
||||
PluralRuleFilipino = 3
|
||||
PluralRuleOneForm = 4
|
||||
PluralRuleCzech = 5
|
||||
PluralRuleRussian = 6
|
||||
PluralRulePolish = 7
|
||||
PluralRuleLatvian = 8
|
||||
PluralRuleLithuanian = 9
|
||||
PluralRuleFrench = 10
|
||||
PluralRuleCatalan = 11
|
||||
PluralRuleSlovenian = 12
|
||||
PluralRuleArabic = 13
|
||||
)
|
||||
|
||||
func GetPluralRuleImpl(langName string) int {
|
||||
// First, check for languages with country-specific plural rules.
|
||||
switch langName {
|
||||
case "pt-BR":
|
||||
return PluralRuleFrench
|
||||
|
||||
case "pt-PT":
|
||||
return PluralRuleCatalan
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Remove the country portion of the locale name.
|
||||
langName = strings.Split(strings.Split(langName, "_")[0], "-")[0]
|
||||
|
||||
// When adding a new language not in the list, add its plural rule definition here.
|
||||
switch langName {
|
||||
case "en", "aa", "ab", "abr", "ada", "ae", "aeb", "af", "afh", "aii", "ain", "akk", "ale", "aln", "alt", "ami", "an", "ang", "anp", "apc", "arc", "arp", "arq", "arw", "arz", "asa", "ast", "av", "avk", "awa", "ayc", "az", "azb", "ba", "bal", "ban", "bar", "bas", "bbc", "bci", "bej", "bem", "ber", "bew", "bez", "bg", "bgc", "bgn", "bhb", "bhi", "bi", "bik", "bin", "bjj", "bjn", "bla", "bnt", "bqi", "bra", "brb", "brh", "brx", "bua", "bug", "bum", "byn", "cad", "cak", "car", "ce", "cgg", "ch", "chb", "chg", "chk", "chm", "chn", "cho", "chp", "chr", "chy", "ckb", "co", "cop", "cpe", "cpf", "cr", "crp", "cu", "cv", "da", "dak", "dar", "dcc", "de", "del", "den", "dgr", "din", "dje", "dnj", "dnk", "dru", "dry", "dua", "dum", "dv", "dyu", "ee", "efi", "egl", "egy", "eka", "el", "elx", "enm", "eo", "et", "eu", "ewo", "ext", "fan", "fat", "fbl", "ffm", "fi", "fj", "fo", "fon", "frk", "frm", "fro", "frr", "frs", "fuq", "fur", "fuv", "fvr", "fy", "gaa", "gay", "gba", "gbm", "gez", "gil", "gl", "glk", "gmh", "gn", "goh", "gom", "gon", "gor", "got", "grb", "gsw", "guc", "gum", "gur", "guz", "gwi", "ha", "hai", "haw", "haz", "hil", "hit", "hmn", "hnd", "hne", "hno", "ho", "hoc", "hoj", "hrx", "ht", "hu", "hup", "hus", "hz", "ia", "iba", "ibb", "ie", "ik", "ilo", "inh", "io", "jam", "jgo", "jmc", "jpr", "jrb", "ka", "kaa", "kac", "kaj", "kam", "kaw", "kbd", "kcg", "kfr", "kfy", "kg", "kha", "khn", "kho", "ki", "kj", "kk", "kkj", "kl", "kln", "kmb", "kmr", "kok", "kpe", "kr", "krc", "kri", "krl", "kru", "ks", "ksb", "ku", "kum", "kut", "kv", "kxm", "ky", "la", "lad", "laj", "lam", "lb", "lez", "lfn", "lg", "li", "lij", "ljp", "lki", "lmn", "lmo", "lol", "loz", "lrc", "lu", "lua", "lui", "lun", "luo", "lus", "luy", "luz", "mad", "mag", "mai", "mak", "man", "mas", "mdf", "mdh", "mdr", "men", "mer", "mfa", "mga", "mgh", "mgo", "mh", "mhr", "mic", "min", "mjw", "ml", "mn", "mnc", "mni", "mnw", "moe", "moh", "mos", "mr", "mrh", "mtr", "mus", "mwk", "mwl", "mwr", "mxc", "myv", "myx", "mzn", "na", "nah", "nap", "nb", "nd", "ndc", "nds", "ne", "new", "ng", "ngl", "nia", "nij", "niu", "nl", "nn", "nnh", "nod", "noe", "nog", "non", "nr", "nuk", "nv", "nwc", "ny", "nym", "nyn", "nyo", "nzi", "oj", "om", "or", "os", "ota", "otk", "ovd", "pag", "pal", "pam", "pap", "pau", "pbb", "pdt", "peo", "phn", "pi", "pms", "pon", "pro", "ps", "pwn", "qu", "quc", "qug", "qya", "raj", "rap", "rar", "rcf", "rej", "rhg", "rif", "rkt", "rm", "rmt", "rn", "rng", "rof", "rom", "rue", "rup", "rw", "rwk", "sad", "sai", "sam", "saq", "sas", "sc", "sck", "sco", "sd", "sdh", "sef", "seh", "sel", "sga", "sgn", "sgs", "shn", "sid", "sjd", "skr", "sm", "sml", "sn", "snk", "so", "sog", "sou", "sq", "srn", "srr", "ss", "ssy", "st", "suk", "sus", "sux", "sv", "sw", "swg", "swv", "sxu", "syc", "syl", "syr", "szy", "ta", "tay", "tcy", "te", "tem", "teo", "ter", "tet", "tig", "tiv", "tk", "tkl", "tli", "tly", "tmh", "tn", "tog", "tr", "trv", "ts", "tsg", "tsi", "tsj", "tts", "tum", "tvl", "tw", "ty", "tyv", "tzj", "tzl", "udm", "ug", "uga", "umb", "und", "unr", "ur", "uz", "vai", "ve", "vls", "vmf", "vmw", "vo", "vot", "vro", "vun", "wae", "wal", "war", "was", "wbq", "wbr", "wep", "wtm", "xal", "xh", "xnr", "xog", "yao", "yap", "yi", "yua", "za", "zap", "zbl", "zen", "zgh", "zun", "zza":
|
||||
return PluralRuleDefault
|
||||
|
||||
case "ach", "ady", "ak", "am", "arn", "as", "bh", "bho", "bn", "csw", "doi", "fa", "ff", "frc", "frp", "gu", "gug", "gun", "guw", "hi", "hy", "kab", "kn", "ln", "mfe", "mg", "mi", "mia", "nso", "oc", "pa", "pcm", "pt", "qdt", "qtp", "si", "tg", "ti", "wa", "zu":
|
||||
return PluralRuleBengali
|
||||
|
||||
case "is":
|
||||
return PluralRuleIcelandic
|
||||
|
||||
case "fil":
|
||||
return PluralRuleFilipino
|
||||
|
||||
case "ace", "ay", "bm", "bo", "cdo", "cpx", "crh", "dz", "gan", "hak", "hnj", "hsn", "id", "ig", "ii", "ja", "jbo", "jv", "kde", "kea", "km", "ko", "kos", "lkt", "lo", "lzh", "ms", "my", "nan", "nqo", "osa", "sah", "ses", "sg", "son", "su", "th", "tlh", "to", "tok", "tpi", "tt", "vi", "wo", "wuu", "yo", "yue", "zh":
|
||||
return PluralRuleOneForm
|
||||
|
||||
case "cpp", "cs", "sk":
|
||||
return PluralRuleCzech
|
||||
|
||||
case "be", "bs", "cnr", "hr", "ru", "sr", "uk", "wen":
|
||||
return PluralRuleRussian
|
||||
|
||||
case "csb", "pl", "szl":
|
||||
return PluralRulePolish
|
||||
|
||||
case "lv", "prg":
|
||||
return PluralRuleLatvian
|
||||
|
||||
case "lt":
|
||||
return PluralRuleLithuanian
|
||||
|
||||
case "fr":
|
||||
return PluralRuleFrench
|
||||
|
||||
case "ca", "es", "it":
|
||||
return PluralRuleCatalan
|
||||
|
||||
case "sl":
|
||||
return PluralRuleSlovenian
|
||||
|
||||
case "ar":
|
||||
return PluralRuleArabic
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
log.Error("No plural rule defined for language %s", langName)
|
||||
return PluralRuleDefault
|
||||
}
|
||||
|
||||
var PluralRules = []i18n.PluralFormRule{
|
||||
// [ 0] Common 2-form, e.g. English, German
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n != 1 {
|
||||
return i18n.PluralFormOther
|
||||
}
|
||||
return i18n.PluralFormOne
|
||||
},
|
||||
|
||||
// [ 1] Bengali
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n > 1 {
|
||||
return i18n.PluralFormOther
|
||||
}
|
||||
return i18n.PluralFormOne
|
||||
},
|
||||
|
||||
// [ 2] Icelandic
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n%10 != 1 || n%100 == 11 {
|
||||
return i18n.PluralFormOther
|
||||
}
|
||||
return i18n.PluralFormOne
|
||||
},
|
||||
|
||||
// [ 3] Filipino
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n != 1 && n != 2 && n != 3 && (n%10 == 4 || n%10 == 6 || n%10 == 9) {
|
||||
return i18n.PluralFormOther
|
||||
}
|
||||
return i18n.PluralFormOne
|
||||
},
|
||||
|
||||
// [ 4] OneForm
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [ 5] Czech
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n >= 2 && n <= 4 {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [ 6] Russian
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n%10 == 1 && n%100 != 11 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
return i18n.PluralFormMany
|
||||
},
|
||||
|
||||
// [ 7] Polish
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
return i18n.PluralFormMany
|
||||
},
|
||||
|
||||
// [ 8] Latvian
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n%10 == 0 || n%100 >= 11 && n%100 <= 19 {
|
||||
return i18n.PluralFormZero
|
||||
}
|
||||
if n%10 == 1 && n%100 != 11 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [ 9] Lithuanian
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n%10 == 1 && (n%100 < 11 || n%100 > 19) {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n%10 >= 2 && n%10 <= 9 && (n%100 < 11 || n%100 > 19) {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
return i18n.PluralFormMany
|
||||
},
|
||||
|
||||
// [10] French
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n == 0 || n == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n != 0 && n%1000000 == 0 {
|
||||
return i18n.PluralFormMany
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [11] Catalan
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n != 0 && n%1000000 == 0 {
|
||||
return i18n.PluralFormMany
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [12] Slovenian
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n%100 == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n%100 == 2 {
|
||||
return i18n.PluralFormTwo
|
||||
}
|
||||
if n%100 == 3 || n%100 == 4 {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
|
||||
// [13] Arabic
|
||||
func(n int64) i18n.PluralFormIndex {
|
||||
if n == 0 {
|
||||
return i18n.PluralFormZero
|
||||
}
|
||||
if n == 1 {
|
||||
return i18n.PluralFormOne
|
||||
}
|
||||
if n == 2 {
|
||||
return i18n.PluralFormTwo
|
||||
}
|
||||
if n%100 >= 3 && n%100 <= 10 {
|
||||
return i18n.PluralFormFew
|
||||
}
|
||||
if n%100 >= 11 {
|
||||
return i18n.PluralFormMany
|
||||
}
|
||||
return i18n.PluralFormOther
|
||||
},
|
||||
}
|
|
@ -32,6 +32,9 @@ type Locale interface {
|
|||
TrString(string, ...any) string
|
||||
|
||||
Tr(key string, args ...any) template.HTML
|
||||
// New-style pluralized strings
|
||||
TrPluralString(count any, trKey string, trArgs ...any) template.HTML
|
||||
// Old-style pseudo-pluralized strings, deprecated
|
||||
TrN(cnt any, key1, keyN string, args ...any) template.HTML
|
||||
|
||||
TrSize(size int64) ReadableSize
|
||||
|
@ -100,8 +103,17 @@ func InitLocales(ctx context.Context) {
|
|||
}
|
||||
|
||||
key := "locale_" + setting.Langs[i] + ".ini"
|
||||
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil {
|
||||
log.Error("Failed to set messages to %s: %v", setting.Langs[i], err)
|
||||
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil {
|
||||
log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err)
|
||||
}
|
||||
|
||||
key = "locale_next/locale_" + setting.Langs[i] + ".json"
|
||||
if bytes, err := options.AssetFS().ReadFile(key); err == nil {
|
||||
if err = i18n.DefaultLocales.AddToLocaleFromJSON(setting.Langs[i], bytes); err != nil {
|
||||
log.Error("Failed to add new-style messages to %s: %v", setting.Langs[i], err)
|
||||
}
|
||||
} else {
|
||||
log.Error("Failed to open new-style messages for %s: %v", setting.Langs[i], err)
|
||||
}
|
||||
}
|
||||
if len(setting.Langs) != 0 {
|
||||
|
|
|
@ -48,3 +48,111 @@ func TestPrettyNumber(t *testing.T) {
|
|||
assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000))
|
||||
assert.EqualValues(t, "1,000,000.1", l.PrettyNumber(1000000.1))
|
||||
}
|
||||
|
||||
func TestGetPluralRule(t *testing.T) {
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en"))
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US"))
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK"))
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds"))
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE"))
|
||||
|
||||
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh"))
|
||||
assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja"))
|
||||
|
||||
assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn"))
|
||||
|
||||
assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is"))
|
||||
|
||||
assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil"))
|
||||
|
||||
assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs"))
|
||||
|
||||
assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru"))
|
||||
|
||||
assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl"))
|
||||
|
||||
assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv"))
|
||||
|
||||
assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt"))
|
||||
|
||||
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr"))
|
||||
|
||||
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca"))
|
||||
|
||||
assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl"))
|
||||
|
||||
assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar"))
|
||||
|
||||
assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT"))
|
||||
assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR"))
|
||||
|
||||
assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid"))
|
||||
}
|
||||
|
||||
func TestApplyPluralRule(t *testing.T) {
|
||||
testCases := []struct {
|
||||
expect i18n.PluralFormIndex
|
||||
pluralRule int
|
||||
values []int64
|
||||
}{
|
||||
{i18n.PluralFormOne, PluralRuleDefault, []int64{1}},
|
||||
{i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}},
|
||||
|
||||
{i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}},
|
||||
{i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}},
|
||||
{i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}},
|
||||
{i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleCzech, []int64{1}},
|
||||
{i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}},
|
||||
{i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}},
|
||||
{i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}},
|
||||
{i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRulePolish, []int64{1}},
|
||||
{i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}},
|
||||
{i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}},
|
||||
|
||||
{i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}},
|
||||
{i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}},
|
||||
{i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}},
|
||||
{i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}},
|
||||
{i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}},
|
||||
{i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}},
|
||||
{i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleCatalan, []int64{1}},
|
||||
{i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}},
|
||||
{i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}},
|
||||
|
||||
{i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}},
|
||||
{i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}},
|
||||
{i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}},
|
||||
{i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}},
|
||||
|
||||
{i18n.PluralFormZero, PluralRuleArabic, []int64{0}},
|
||||
{i18n.PluralFormOne, PluralRuleArabic, []int64{1}},
|
||||
{i18n.PluralFormTwo, PluralRuleArabic, []int64{2}},
|
||||
{i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}},
|
||||
{i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}},
|
||||
{i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
for _, n := range tc.values {
|
||||
assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue