1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-01 17:38:37 +00:00

Simplify locale package usage (refactoring)

This commit is contained in:
Frédéric Guillot 2018-09-22 15:04:55 -07:00
parent aae9b4eb83
commit b1e8f534ef
26 changed files with 443 additions and 299 deletions

36
locale/catalog.go Normal file
View file

@ -0,0 +1,36 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import (
"encoding/json"
"fmt"
)
type translationDict map[string]interface{}
type catalog map[string]translationDict
var defaultCatalog catalog
func init() {
defaultCatalog = make(catalog)
for language, data := range translations {
messages, err := parseTranslationDict(data)
if err != nil {
panic(err)
}
defaultCatalog[language] = messages
}
}
func parseTranslationDict(data string) (translationDict, error) {
var translations translationDict
if err := json.Unmarshal([]byte(data), &translations); err != nil {
return nil, fmt.Errorf("invalid translation file: %v", err)
}
return translations, nil
}

View file

@ -7,14 +7,14 @@ package locale // import "miniflux.app/locale"
import "testing"
func TestParserWithInvalidData(t *testing.T) {
_, err := parseCatalogMessages(`{`)
_, err := parseTranslationDict(`{`)
if err == nil {
t.Fatal(`An error should be returned when parsing invalid data`)
}
}
func TestParser(t *testing.T) {
translations, err := parseCatalogMessages(`{"k": "v"}`)
translations, err := parseTranslationDict(`{"k": "v"}`)
if err != nil {
t.Fatalf(`Unexpected parsing error: %v`, err)
}

View file

@ -1,50 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "fmt"
// Language represents a language in the system.
type Language struct {
language string
translations catalogMessages
}
// Get fetch the translation for the given key.
func (l *Language) Get(key string, args ...interface{}) string {
var translation string
str, found := l.translations[key]
if !found {
translation = key
} else {
translation = str.(string)
}
return fmt.Sprintf(translation, args...)
}
// Plural returns the translation of the given key by using the language plural form.
func (l *Language) Plural(key string, n int, args ...interface{}) string {
translation := key
slices, found := l.translations[key]
if found {
pluralForm, found := pluralForms[l.language]
if !found {
pluralForm = pluralForms["default"]
}
index := pluralForm(n)
translations := slices.([]interface{})
translation = key
if len(translations) > index {
translation = translations[index].(string)
}
}
return fmt.Sprintf(translation, args...)
}

View file

@ -1,23 +1,9 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "miniflux.app/logger"
// Load loads all translations.
func Load() *Translator {
translator := NewTranslator()
for language, tr := range translations {
logger.Debug("Loading translation: %s", language)
translator.AddLanguage(language, tr)
}
return translator
}
// AvailableLanguages returns the list of available languages.
func AvailableLanguages() map[string]string {
return map[string]string{

View file

@ -1,103 +1,24 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "testing"
func TestTranslateWithMissingLanguage(t *testing.T) {
translator := NewTranslator()
translation := translator.GetLanguage("en_US").Get("auth.username")
func TestAvailableLanguages(t *testing.T) {
results := AvailableLanguages()
for k, v := range results {
if k == "" {
t.Errorf(`Empty language key detected`)
}
if translation != "auth.username" {
t.Errorf("Wrong translation, got %s", translation)
}
}
func TestTranslateWithExistingKey(t *testing.T) {
data := `{"auth.username": "Username"}`
translator := NewTranslator()
translator.AddLanguage("en_US", data)
translation := translator.GetLanguage("en_US").Get("auth.username")
if translation != "Username" {
t.Errorf("Wrong translation, got %s", translation)
}
}
func TestTranslateWithMissingKey(t *testing.T) {
data := `{"auth.username": "Username"}`
translator := NewTranslator()
translator.AddLanguage("en_US", data)
translation := translator.GetLanguage("en_US").Get("auth.password")
if translation != "auth.password" {
t.Errorf("Wrong translation, got %s", translation)
}
}
func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
translator := NewTranslator()
translator.AddLanguage("fr_FR", "")
translation := translator.GetLanguage("fr_FR").Get("Status: %s", "ok")
if translation != "Status: ok" {
t.Errorf("Wrong translation, got %s", translation)
}
}
func TestTranslatePluralWithDefaultRule(t *testing.T) {
data := `{"number_of_users": ["Il y a %d utilisateur (%s)", "Il y a %d utilisateurs (%s)"]}`
translator := NewTranslator()
translator.AddLanguage("fr_FR", data)
language := translator.GetLanguage("fr_FR")
translation := language.Plural("number_of_users", 1, 1, "some text")
expected := "Il y a 1 utilisateur (some text)"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
}
translation = language.Plural("number_of_users", 2, 2, "some text")
expected = "Il y a 2 utilisateurs (some text)"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
}
}
func TestTranslatePluralWithRussianRule(t *testing.T) {
data := `{"key": ["из %d книги за %d день", "из %d книг за %d дня", "из %d книг за %d дней"]}`
translator := NewTranslator()
translator.AddLanguage("ru_RU", data)
language := translator.GetLanguage("ru_RU")
translation := language.Plural("key", 1, 1, 1)
expected := "из 1 книги за 1 день"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
}
translation = language.Plural("key", 2, 2, 2)
expected = "из 2 книг за 2 дня"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
}
translation = language.Plural("key", 5, 5, 5)
expected = "из 5 книг за 5 дней"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
}
}
func TestTranslatePluralWithMissingTranslation(t *testing.T) {
translator := NewTranslator()
translator.AddLanguage("fr_FR", "")
language := translator.GetLanguage("fr_FR")
translation := language.Plural("number_of_users", 2)
expected := "number_of_users"
if translation != expected {
t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected)
if v == "" {
t.Errorf(`Empty language value detected`)
}
}
if _, found := results["en_US"]; !found {
t.Errorf(`We must have at least the default language (en_US)`)
}
}

View file

@ -1,21 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import (
"encoding/json"
"fmt"
)
type catalogMessages map[string]interface{}
type catalog map[string]catalogMessages
func parseCatalogMessages(data string) (catalogMessages, error) {
var translations catalogMessages
if err := json.Unmarshal([]byte(data), &translations); err != nil {
return nil, fmt.Errorf("invalid translation file: %v", err)
}
return translations, nil
}

View file

@ -1,12 +1,14 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
type pluralFormFunc func(n int) int
// See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html
// And http://www.unicode.org/cldr/charts/29/supplemental/language_plural_rules.html
var pluralForms = map[string]func(n int) int{
var pluralForms = map[string]pluralFormFunc{
// nplurals=2; plural=(n != 1);
"default": func(n int) int {
if n != 1 {

63
locale/plural_test.go Normal file
View file

@ -0,0 +1,63 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "testing"
func TestPluralRules(t *testing.T) {
scenarios := map[string]map[int]int{
"default": map[int]int{
1: 0,
2: 1,
5: 1,
},
"ar_AR": map[int]int{
0: 0,
1: 1,
2: 2,
5: 3,
11: 4,
200: 5,
},
"cs_CZ": map[int]int{
1: 0,
2: 1,
5: 2,
},
"pl_PL": map[int]int{
1: 0,
2: 1,
5: 2,
},
"pt_BR": map[int]int{
1: 0,
2: 1,
5: 1,
},
"ru_RU": map[int]int{
1: 0,
2: 1,
5: 2,
},
"sr_RS": map[int]int{
1: 0,
2: 1,
5: 2,
},
"zh_CN": map[int]int{
1: 0,
5: 0,
},
}
for rule, values := range scenarios {
for input, expected := range values {
result := pluralForms[rule](input)
if result != expected {
t.Errorf(`Unexpected result for %q rule, got %d instead of %d for %d as input`, rule, result, expected, input)
}
}
}
}

67
locale/printer.go Normal file
View file

@ -0,0 +1,67 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "fmt"
// Printer converts translation keys to language-specific strings.
type Printer struct {
language string
}
// Printf is like fmt.Printf, but using language-specific formatting.
func (p *Printer) Printf(key string, args ...interface{}) string {
var translation string
str, found := defaultCatalog[p.language][key]
if !found {
translation = key
} else {
var valid bool
translation, valid = str.(string)
if !valid {
translation = key
}
}
return fmt.Sprintf(translation, args...)
}
// Plural returns the translation of the given key by using the language plural form.
func (p *Printer) Plural(key string, n int, args ...interface{}) string {
choices, found := defaultCatalog[p.language][key]
if found {
var plurals []string
switch v := choices.(type) {
case []interface{}:
for _, v := range v {
plurals = append(plurals, fmt.Sprint(v))
}
case []string:
plurals = v
default:
return key
}
pluralForm, found := pluralForms[p.language]
if !found {
pluralForm = pluralForms["default"]
}
index := pluralForm(n)
if len(plurals) > index {
return fmt.Sprintf(plurals[index], args...)
}
}
return key
}
// NewPrinter creates a new Printer.
func NewPrinter(language string) *Printer {
return &Printer{language}
}

174
locale/printer_test.go Normal file
View file

@ -0,0 +1,174 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
import "testing"
func TestTranslateWithMissingLanguage(t *testing.T) {
defaultCatalog = catalog{}
translation := NewPrinter("invalid").Printf("missing.key")
if translation != "missing.key" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslateWithMissingKey(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"k": "v",
},
}
translation := NewPrinter("en_US").Printf("missing.key")
if translation != "missing.key" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslateWithExistingKey(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"auth.username": "Login",
},
}
translation := NewPrinter("en_US").Printf("auth.username")
if translation != "Login" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"key": "Test: %s",
},
"fr_FR": translationDict{
"key": "Test : %s",
},
}
translation := NewPrinter("fr_FR").Printf("key", "ok")
if translation != "Test : ok" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"auth.username": "Login",
},
"fr_FR": translationDict{
"auth.username": "Identifiant",
},
}
translation := NewPrinter("fr_FR").Printf("Status: %s", "ok")
if translation != "Status: ok" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslateWithInvalidValue(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"auth.username": "Login",
},
"fr_FR": translationDict{
"auth.username": true,
},
}
translation := NewPrinter("fr_FR").Printf("auth.username")
if translation != "auth.username" {
t.Errorf(`Wrong translation, got %q`, translation)
}
}
func TestTranslatePluralWithDefaultRule(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
},
"fr_FR": translationDict{
"number_of_users": []string{"%d utilisateur (%s)", "%d utilisateurs (%s)"},
},
}
printer := NewPrinter("fr_FR")
translation := printer.Plural("number_of_users", 1, 1, "some text")
expected := "1 utilisateur (some text)"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
translation = printer.Plural("number_of_users", 2, 2, "some text")
expected = "2 utilisateurs (some text)"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
}
func TestTranslatePluralWithRussianRule(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"time_elapsed.years": []string{"%d year", "%d years"},
},
"ru_RU": translationDict{
"time_elapsed.years": []string{"%d год назад", "%d года назад", "%d лет назад"},
},
}
printer := NewPrinter("ru_RU")
translation := printer.Plural("time_elapsed.years", 1, 1)
expected := "1 год назад"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
translation = printer.Plural("time_elapsed.years", 2, 2)
expected = "2 года назад"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
translation = printer.Plural("time_elapsed.years", 5, 5)
expected = "5 лет назад"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
}
func TestTranslatePluralWithMissingTranslation(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
},
"fr_FR": translationDict{},
}
translation := NewPrinter("fr_FR").Plural("number_of_users", 2)
expected := "number_of_users"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
}
func TestTranslatePluralWithInvalidValues(t *testing.T) {
defaultCatalog = catalog{
"en_US": translationDict{
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
},
"fr_FR": translationDict{
"number_of_users": "must be a slice",
},
}
translation := NewPrinter("fr_FR").Plural("number_of_users", 2)
expected := "number_of_users"
if translation != expected {
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
}
}

View file

@ -9,16 +9,16 @@ import "testing"
func TestAllLanguagesHaveCatalog(t *testing.T) {
for language := range AvailableLanguages() {
if _, found := translations[language]; !found {
t.Fatalf(`This language do not have a catalog: %s`, language)
t.Fatalf(`This language do not have a catalog: %q`, language)
}
}
}
func TestAllKeysHaveValue(t *testing.T) {
for language := range AvailableLanguages() {
messages, err := parseCatalogMessages(translations[language])
messages, err := parseTranslationDict(translations[language])
if err != nil {
t.Fatalf(`Parsing error language %s`, language)
t.Fatalf(`Parsing error for language %q`, language)
}
if len(messages) == 0 {
@ -42,7 +42,7 @@ func TestAllKeysHaveValue(t *testing.T) {
func TestMissingTranslations(t *testing.T) {
refLang := "en_US"
references, err := parseCatalogMessages(translations[refLang])
references, err := parseTranslationDict(translations[refLang])
if err != nil {
t.Fatal(`Unable to parse reference language`)
}
@ -52,9 +52,9 @@ func TestMissingTranslations(t *testing.T) {
continue
}
messages, err := parseCatalogMessages(translations[language])
messages, err := parseTranslationDict(translations[language])
if err != nil {
t.Fatalf(`Parsing error language %s`, language)
t.Fatalf(`Parsing error for language %q`, language)
}
for key := range references {

View file

@ -1,31 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package locale // import "miniflux.app/locale"
// Translator manage supported locales.
type Translator struct {
locales catalog
}
// AddLanguage loads a new language into the system.
func (t *Translator) AddLanguage(language, data string) (err error) {
t.locales[language], err = parseCatalogMessages(data)
return err
}
// GetLanguage returns the given language handler.
func (t *Translator) GetLanguage(language string) *Language {
translations, found := t.locales[language]
if !found {
return &Language{language: language}
}
return &Language{language: language, translations: translations}
}
// NewTranslator creates a new Translator.
func NewTranslator() *Translator {
return &Translator{locales: make(catalog)}
}