aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2018-09-22 15:04:55 -0700
committerGravatar Frédéric Guillot <fred@miniflux.net>2018-09-22 15:04:55 -0700
commitb1e8f534eff7569dc2e8dab4dee851d1b709f71b (patch)
treeec9d5cbebc78704727c9ce959f442b3df5cc7d76
parentaae9b4eb835c72c0b7ecd8fa6565eacce3963d00 (diff)
Simplify locale package usage (refactoring)
-rw-r--r--daemon/daemon.go6
-rw-r--r--daemon/routes.go7
-rw-r--r--daemon/server.go5
-rw-r--r--errors/errors.go4
-rw-r--r--locale/catalog.go36
-rw-r--r--locale/catalog_test.go (renamed from locale/parser_test.go)4
-rw-r--r--locale/language.go50
-rwxr-xr-xlocale/locale.go16
-rw-r--r--locale/locale_test.go105
-rw-r--r--locale/parser.go21
-rw-r--r--locale/plural.go (renamed from locale/plurals.go)6
-rw-r--r--locale/plural_test.go63
-rw-r--r--locale/printer.go67
-rw-r--r--locale/printer_test.go174
-rw-r--r--locale/translations_test.go12
-rw-r--r--locale/translator.go31
-rw-r--r--reader/feed/handler.go17
-rw-r--r--template/engine.go28
-rw-r--r--template/functions.go22
-rw-r--r--template/functions_test.go30
-rw-r--r--ui/controller.go5
-rw-r--r--ui/integration_pocket.go9
-rw-r--r--ui/integration_update.go6
-rw-r--r--ui/oauth2_callback.go6
-rw-r--r--ui/oauth2_unlink.go6
-rw-r--r--ui/settings_update.go2
26 files changed, 441 insertions, 297 deletions
diff --git a/daemon/daemon.go b/daemon/daemon.go
index b33d882..a1fbf8e 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -13,7 +13,6 @@ import (
"time"
"miniflux.app/config"
- "miniflux.app/locale"
"miniflux.app/logger"
"miniflux.app/reader/feed"
"miniflux.app/scheduler"
@@ -39,10 +38,9 @@ func Run(cfg *config.Config, store *storage.Storage) {
}
}()
- translator := locale.Load()
- feedHandler := feed.NewFeedHandler(store, translator)
+ feedHandler := feed.NewFeedHandler(store)
pool := scheduler.NewWorkerPool(feedHandler, cfg.WorkerPoolSize())
- server := newServer(cfg, store, pool, feedHandler, translator)
+ server := newServer(cfg, store, pool, feedHandler)
scheduler.NewFeedScheduler(
store,
diff --git a/daemon/routes.go b/daemon/routes.go
index 891207e..88b8c20 100644
--- a/daemon/routes.go
+++ b/daemon/routes.go
@@ -10,7 +10,6 @@ import (
"miniflux.app/api"
"miniflux.app/config"
"miniflux.app/fever"
- "miniflux.app/locale"
"miniflux.app/middleware"
"miniflux.app/reader/feed"
"miniflux.app/scheduler"
@@ -21,12 +20,12 @@ import (
"github.com/gorilla/mux"
)
-func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
+func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
router := mux.NewRouter()
- templateEngine := template.NewEngine(cfg, router, translator)
+ templateEngine := template.NewEngine(cfg, router)
apiController := api.NewController(store, feedHandler)
feverController := fever.NewController(cfg, store)
- uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, translator, router)
+ uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, router)
middleware := middleware.New(cfg, store, router)
if cfg.BasePath() != "" {
diff --git a/daemon/server.go b/daemon/server.go
index 841e3b3..38afd0a 100644
--- a/daemon/server.go
+++ b/daemon/server.go
@@ -10,7 +10,6 @@ import (
"time"
"miniflux.app/config"
- "miniflux.app/locale"
"miniflux.app/logger"
"miniflux.app/reader/feed"
"miniflux.app/scheduler"
@@ -19,7 +18,7 @@ import (
"golang.org/x/crypto/acme/autocert"
)
-func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, translator *locale.Translator) *http.Server {
+func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *http.Server {
certFile := cfg.CertFile()
keyFile := cfg.KeyFile()
certDomain := cfg.CertDomain()
@@ -29,7 +28,7 @@ func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.Worke
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
Addr: cfg.ListenAddr(),
- Handler: routes(cfg, store, feedHandler, pool, translator),
+ Handler: routes(cfg, store, feedHandler, pool),
}
if certDomain != "" && certCache != "" {
diff --git a/errors/errors.go b/errors/errors.go
index b3c5530..e6a979a 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -22,8 +22,8 @@ func (l LocalizedError) Error() string {
}
// Localize returns the translated error message.
-func (l LocalizedError) Localize(translation *locale.Language) string {
- return translation.Get(l.message, l.args...)
+func (l LocalizedError) Localize(printer *locale.Printer) string {
+ return printer.Printf(l.message, l.args...)
}
// NewLocalizedError returns a new LocalizedError.
diff --git a/locale/catalog.go b/locale/catalog.go
new file mode 100644
index 0000000..926298c
--- /dev/null
+++ b/locale/catalog.go
@@ -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
+}
diff --git a/locale/parser_test.go b/locale/catalog_test.go
index 619d8db..0be9995 100644
--- a/locale/parser_test.go
+++ b/locale/catalog_test.go
@@ -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)
}
diff --git a/locale/language.go b/locale/language.go
deleted file mode 100644
index 5738163..0000000
--- a/locale/language.go
+++ /dev/null
@@ -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...)
-}
diff --git a/locale/locale.go b/locale/locale.go
index 59174b0..a59d4c4 100755
--- a/locale/locale.go
+++ b/locale/locale.go
@@ -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{
diff --git a/locale/locale_test.go b/locale/locale_test.go
index 28e2a80..9f49b60 100644
--- a/locale/locale_test.go
+++ b/locale/locale_test.go
@@ -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")
-
- 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")
+func TestAvailableLanguages(t *testing.T) {
+ results := AvailableLanguages()
+ for k, v := range results {
+ if k == "" {
+ t.Errorf(`Empty language key detected`)
+ }
- if translation != "Username" {
- t.Errorf("Wrong translation, got %s", translation)
+ if v == "" {
+ t.Errorf(`Empty language value detected`)
+ }
}
-}
-
-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 _, found := results["en_US"]; !found {
+ t.Errorf(`We must have at least the default language (en_US)`)
}
}
diff --git a/locale/parser.go b/locale/parser.go
deleted file mode 100644
index 03591c1..0000000
--- a/locale/parser.go
+++ /dev/null
@@ -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
-}
diff --git a/locale/plurals.go b/locale/plural.go
index c4e47cf..3b14c38 100644
--- a/locale/plurals.go
+++ b/locale/plural.go
@@ -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 {
diff --git a/locale/plural_test.go b/locale/plural_test.go
new file mode 100644
index 0000000..e7694bd
--- /dev/null
+++ b/locale/plural_test.go
@@ -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)
+ }
+ }
+ }
+}
diff --git a/locale/printer.go b/locale/printer.go
new file mode 100644
index 0000000..ef04e05
--- /dev/null
+++ b/locale/printer.go
@@ -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}
+}
diff --git a/locale/printer_test.go b/locale/printer_test.go
new file mode 100644
index 0000000..1d8f58d
--- /dev/null
+++ b/locale/printer_test.go
@@ -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)
+ }
+}
diff --git a/locale/translations_test.go b/locale/translations_test.go
index f196a28..55775ba 100644
--- a/locale/translations_test.go
+++ b/locale/translations_test.go
@@ -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 {
diff --git a/locale/translator.go b/locale/translator.go
deleted file mode 100644
index 0c38c56..0000000
--- a/locale/translator.go
+++ /dev/null
@@ -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)}
-}
diff --git a/reader/feed/handler.go b/reader/feed/handler.go
index 252d178..0b81c67 100644
--- a/reader/feed/handler.go
+++ b/reader/feed/handler.go
@@ -33,7 +33,6 @@ var (
// Handler contains all the logic to create and refresh feeds.
type Handler struct {
store *storage.Storage
- translator *locale.Translator
}
// CreateFeed fetch, parse and store a new feed.
@@ -124,7 +123,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
userLanguage = "en_US"
}
- currentLanguage := h.translator.GetLanguage(userLanguage)
+ printer := locale.NewPrinter(userLanguage)
originalFeed, err := h.store.FeedByID(userID, feedID)
if err != nil {
@@ -149,7 +148,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
}
originalFeed.ParsingErrorCount++
- originalFeed.ParsingErrorMsg = customErr.Localize(currentLanguage)
+ originalFeed.ParsingErrorMsg = customErr.Localize(printer)
h.store.UpdateFeed(originalFeed)
return customErr
}
@@ -159,7 +158,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
if response.IsNotFound() {
err := errors.NewLocalizedError(errResourceNotFound)
originalFeed.ParsingErrorCount++
- originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
+ originalFeed.ParsingErrorMsg = err.Localize(printer)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -167,7 +166,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
if response.HasServerFailure() {
err := errors.NewLocalizedError(errServerFailure, response.StatusCode)
originalFeed.ParsingErrorCount++
- originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
+ originalFeed.ParsingErrorMsg = err.Localize(printer)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -179,7 +178,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
if response.ContentLength == 0 {
err := errors.NewLocalizedError(errEmptyFeed)
originalFeed.ParsingErrorCount++
- originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
+ originalFeed.ParsingErrorMsg = err.Localize(printer)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -192,7 +191,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
subscription, parseErr := parseFeed(body)
if parseErr != nil {
originalFeed.ParsingErrorCount++
- originalFeed.ParsingErrorMsg = parseErr.Localize(currentLanguage)
+ originalFeed.ParsingErrorMsg = parseErr.Localize(printer)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -236,6 +235,6 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
}
// NewFeedHandler returns a feed handler.
-func NewFeedHandler(store *storage.Storage, translator *locale.Translator) *Handler {
- return &Handler{store, translator}
+func NewFeedHandler(store *storage.Storage) *Handler {
+ return &Handler{store}
}
diff --git a/template/engine.go b/template/engine.go
index bd99742..e4cdc96 100644
--- a/template/engine.go
+++ b/template/engine.go
@@ -19,9 +19,8 @@ import (
// Engine handles the templating system.
type Engine struct {
- templates map[string]*template.Template
- translator *locale.Translator
- funcMap *funcMap
+ templates map[string]*template.Template
+ funcMap *funcMap
}
func (e *Engine) parseAll() {
@@ -43,29 +42,29 @@ func (e *Engine) Render(name, language string, data interface{}) []byte {
logger.Fatal("[Template] The template %s does not exists", name)
}
- lang := e.translator.GetLanguage(language)
+ printer := locale.NewPrinter(language)
// Functions that need to be declared at runtime.
tpl.Funcs(template.FuncMap{
"elapsed": func(timezone string, t time.Time) string {
- return elapsedTime(lang, timezone, t)
+ return elapsedTime(printer, timezone, t)
},
"t": func(key interface{}, args ...interface{}) string {
- switch key.(type) {
+ switch k := key.(type) {
case string:
- return lang.Get(key.(string), args...)
+ return printer.Printf(k, args...)
case errors.LocalizedError:
- return key.(errors.LocalizedError).Localize(lang)
+ return k.Localize(printer)
case *errors.LocalizedError:
- return key.(*errors.LocalizedError).Localize(lang)
+ return k.Localize(printer)
case error:
- return key.(error).Error()
+ return k.Error()
default:
return ""
}
},
"plural": func(key string, n int, args ...interface{}) string {
- return lang.Plural(key, n, args...)
+ return printer.Plural(key, n, args...)
},
})
@@ -79,11 +78,10 @@ func (e *Engine) Render(name, language string, data interface{}) []byte {
}
// NewEngine returns a new template engine.
-func NewEngine(cfg *config.Config, router *mux.Router, translator *locale.Translator) *Engine {
+func NewEngine(cfg *config.Config, router *mux.Router) *Engine {
tpl := &Engine{
- templates: make(map[string]*template.Template),
- translator: translator,
- funcMap: newFuncMap(cfg, router),
+ templates: make(map[string]*template.Template),
+ funcMap: newFuncMap(cfg, router),
}
tpl.parseAll()
diff --git a/template/functions.go b/template/functions.go
index aac6c75..289de3d 100644
--- a/template/functions.go
+++ b/template/functions.go
@@ -135,15 +135,15 @@ func isEmail(str string) bool {
return true
}
-func elapsedTime(language *locale.Language, tz string, t time.Time) string {
+func elapsedTime(printer *locale.Printer, tz string, t time.Time) string {
if t.IsZero() {
- return language.Get("time_elapsed.not_yet")
+ return printer.Printf("time_elapsed.not_yet")
}
now := timezone.Now(tz)
t = timezone.Convert(tz, t)
if now.Before(t) {
- return language.Get("time_elapsed.not_yet")
+ return printer.Printf("time_elapsed.not_yet")
}
diff := now.Sub(t)
@@ -153,25 +153,25 @@ func elapsedTime(language *locale.Language, tz string, t time.Time) string {
d := int(s / 86400)
switch {
case s < 60:
- return language.Get("time_elapsed.now")
+ return printer.Printf("time_elapsed.now")
case s < 3600:
minutes := int(diff.Minutes())
- return language.Plural("time_elapsed.minutes", minutes, minutes)
+ return printer.Plural("time_elapsed.minutes", minutes, minutes)
case s < 86400:
hours := int(diff.Hours())
- return language.Plural("time_elapsed.hours", hours, hours)
+ return printer.Plural("time_elapsed.hours", hours, hours)
case d == 1:
- return language.Get("time_elapsed.yesterday")
+ return printer.Printf("time_elapsed.yesterday")
case d < 7:
- return language.Plural("time_elapsed.days", d, d)
+ return printer.Plural("time_elapsed.days", d, d)
case d < 31:
weeks := int(math.Ceil(float64(d) / 7))
- return language.Plural("time_elapsed.weeks", weeks, weeks)
+ return printer.Plural("time_elapsed.weeks", weeks, weeks)
case d < 365:
months := int(math.Ceil(float64(d) / 30))
- return language.Plural("time_elapsed.months", months, months)
+ return printer.Plural("time_elapsed.months", months, months)
default:
years := int(math.Ceil(float64(d) / 365))
- return language.Plural("time_elapsed.years", years, years)
+ return printer.Plural("time_elapsed.years", years, years)
}
}
diff --git a/template/functions_test.go b/template/functions_test.go
index 04982c7..10d5535 100644
--- a/template/functions_test.go
+++ b/template/functions_test.go
@@ -97,28 +97,26 @@ func TestIsEmail(t *testing.T) {
}
func TestElapsedTime(t *testing.T) {
- translator := locale.Load()
- language := translator.GetLanguage("fr_FR")
-
+ printer := locale.NewPrinter("en_US")
var dt = []struct {
in time.Time
out string
}{
- {time.Time{}, language.Get("time_elapsed.not_yet")},
- {time.Now().Add(time.Hour), language.Get("time_elapsed.not_yet")},
- {time.Now(), language.Get("time_elapsed.now")},
- {time.Now().Add(-time.Minute), language.Plural("time_elapsed.minutes", 1, 1)},
- {time.Now().Add(-time.Minute * 40), language.Plural("time_elapsed.minutes", 40, 40)},
- {time.Now().Add(-time.Hour), language.Plural("time_elapsed.hours", 1, 1)},
- {time.Now().Add(-time.Hour * 3), language.Plural("time_elapsed.hours", 3, 3)},
- {time.Now().Add(-time.Hour * 32), language.Get("time_elapsed.yesterday")},
- {time.Now().Add(-time.Hour * 24 * 3), language.Plural("time_elapsed.days", 3, 3)},
- {time.Now().Add(-time.Hour * 24 * 14), language.Plural("time_elapsed.weeks", 2, 2)},
- {time.Now().Add(-time.Hour * 24 * 60), language.Plural("time_elapsed.months", 2, 2)},
- {time.Now().Add(-time.Hour * 24 * 365 * 3), language.Plural("time_elapsed.years", 3, 3)},
+ {time.Time{}, printer.Printf("time_elapsed.not_yet")},
+ {time.Now().Add(time.Hour), printer.Printf("time_elapsed.not_yet")},
+ {time.Now(), printer.Printf("time_elapsed.now")},
+ {time.Now().Add(-time.Minute), printer.Plural("time_elapsed.minutes", 1, 1)},
+ {time.Now().Add(-time.Minute * 40), printer.Plural("time_elapsed.minutes", 40, 40)},
+ {time.Now().Add(-time.Hour), printer.Plural("time_elapsed.hours", 1, 1)},
+ {time.Now().Add(-time.Hour * 3), printer.Plural("time_elapsed.hours", 3, 3)},
+ {time.Now().Add(-time.Hour * 32), printer.Printf("time_elapsed.yesterday")},
+ {time.Now().Add(-time.Hour * 24 * 3), printer.Plural("time_elapsed.days", 3, 3)},
+ {time.Now().Add(-time.Hour * 24 * 14), printer.Plural("time_elapsed.weeks", 2, 2)},
+ {time.Now().Add(-time.Hour * 24 * 60), printer.Plural("time_elapsed.months", 2, 2)},
+ {time.Now().Add(-time.Hour * 24 * 365 * 3), printer.Plural("time_elapsed.years", 3, 3)},
}
for i, tt := range dt {
- if out := elapsedTime(language, "Local", tt.in); out != tt.out {
+ if out := elapsedTime(printer, "Local", tt.in); out != tt.out {
t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out)
}
}
diff --git a/ui/controller.go b/ui/controller.go
index 005b863..b08c253 100644
--- a/ui/controller.go
+++ b/ui/controller.go
@@ -6,7 +6,6 @@ package ui // import "miniflux.app/ui"
import (
"miniflux.app/config"
- "miniflux.app/locale"
"miniflux.app/reader/feed"
"miniflux.app/scheduler"
"miniflux.app/storage"
@@ -23,18 +22,16 @@ type Controller struct {
feedHandler *feed.Handler
tpl *template.Engine
router *mux.Router
- translator *locale.Translator
}
// NewController returns a new Controller.
-func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, tpl *template.Engine, translator *locale.Translator, router *mux.Router) *Controller {
+func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, tpl *template.Engine, router *mux.Router) *Controller {
return &Controller{
cfg: cfg,
store: store,
pool: pool,
feedHandler: feedHandler,
tpl: tpl,
- translator: translator,
router: router,
}
}
diff --git a/ui/integration_pocket.go b/ui/integration_pocket.go
index 432069a..407731e 100644
--- a/ui/integration_pocket.go
+++ b/ui/integration_pocket.go
@@ -12,12 +12,14 @@ import (
"miniflux.app/http/response/html"
"miniflux.app/http/route"
"miniflux.app/integration/pocket"
+ "miniflux.app/locale"
"miniflux.app/logger"
"miniflux.app/ui/session"
)
// PocketAuthorize redirects the end-user to Pocket website to authorize the application.
func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) {
+ printer := locale.NewPrinter(request.UserLanguage(r))
user, err := c.store.UserByID(request.UserID(r))
if err != nil {
html.ServerError(w, err)
@@ -36,7 +38,7 @@ func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) {
requestToken, err := connector.RequestToken(redirectURL)
if err != nil {
logger.Error("[Pocket:Authorize] %v", err)
- sess.NewFlashErrorMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("error.pocket_request_token"))
+ sess.NewFlashErrorMessage(printer.Printf("error.pocket_request_token"))
response.Redirect(w, r, route.Path(c.router, "integrations"))
return
}
@@ -47,6 +49,7 @@ func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) {
// PocketCallback saves the personal access token after the authorization step.
func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) {
+ printer := locale.NewPrinter(request.UserLanguage(r))
sess := session.New(c.store, request.SessionID(r))
user, err := c.store.UserByID(request.UserID(r))
@@ -65,7 +68,7 @@ func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) {
accessToken, err := connector.AccessToken(request.PocketRequestToken(r))
if err != nil {
logger.Error("[Pocket:Callback] %v", err)
- sess.NewFlashErrorMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("error.pocket_access_token"))
+ sess.NewFlashErrorMessage(printer.Printf("error.pocket_access_token"))
response.Redirect(w, r, route.Path(c.router, "integrations"))
return
}
@@ -79,6 +82,6 @@ func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) {
return
}
- sess.NewFlashMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("alert.pocket_linked"))
+ sess.NewFlashMessage(printer.Printf("alert.pocket_linked"))
response.Redirect(w, r, route.Path(c.router, "integrations"))
}
diff --git a/ui/integration_update.go b/ui/integration_update.go
index 2d10413..f470e4b 100644
--- a/ui/integration_update.go
+++ b/ui/integration_update.go
@@ -13,12 +13,14 @@ import (
"miniflux.app/http/request"
"miniflux.app/http/response/html"
"miniflux.app/http/route"
+ "miniflux.app/locale"
"miniflux.app/ui/form"
"miniflux.app/ui/session"
)
// UpdateIntegration updates integration settings.
func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
+ printer := locale.NewPrinter(request.UserLanguage(r))
sess := session.New(c.store, request.SessionID(r))
user, err := c.store.UserByID(request.UserID(r))
if err != nil {
@@ -36,7 +38,7 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
integrationForm.Merge(integration)
if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
- sess.NewFlashErrorMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("error.duplicate_fever_username"))
+ sess.NewFlashErrorMessage(printer.Printf("error.duplicate_fever_username"))
response.Redirect(w, r, route.Path(c.router, "integrations"))
return
}
@@ -53,6 +55,6 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
return
}
- sess.NewFlashMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("alert.prefs_saved"))
+ sess.NewFlashMessage(printer.Printf("alert.prefs_saved"))
response.Redirect(w, r, route.Path(c.router, "integrations"))
}
diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go
index 546158f..9f51d0a 100644
--- a/ui/oauth2_callback.go
+++ b/ui/oauth2_callback.go
@@ -12,6 +12,7 @@ import (
"miniflux.app/http/response"
"miniflux.app/http/response/html"
"miniflux.app/http/route"
+ "miniflux.app/locale"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/ui/session"
@@ -19,6 +20,7 @@ import (
// OAuth2Callback receives the authorization code and create a new session.
func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
+ printer := locale.NewPrinter(request.UserLanguage(r))
sess := session.New(c.store, request.SessionID(r))
provider := request.Param(r, "provider", "")
@@ -65,7 +67,7 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
if user != nil {
logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", request.UserID(r), user.Username)
- sess.NewFlashErrorMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("error.duplicate_linked_account"))
+ sess.NewFlashErrorMessage(printer.Printf("error.duplicate_linked_account"))
response.Redirect(w, r, route.Path(c.router, "settings"))
return
}
@@ -75,7 +77,7 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
return
}
- sess.NewFlashMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("alert.account_linked"))
+ sess.NewFlashMessage(printer.Printf("alert.account_linked"))
response.Redirect(w, r, route.Path(c.router, "settings"))
return
}
diff --git a/ui/oauth2_unlink.go b/ui/oauth2_unlink.go
index 191d78d..4435733 100644
--- a/ui/oauth2_unlink.go
+++ b/ui/oauth2_unlink.go
@@ -11,12 +11,14 @@ import (
"miniflux.app/http/response"
"miniflux.app/http/response/html"
"miniflux.app/http/route"
+ "miniflux.app/locale"
"miniflux.app/logger"
"miniflux.app/ui/session"
)
// OAuth2Unlink unlink an account from the external provider.
func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
+ printer := locale.NewPrinter(request.UserLanguage(r))
provider := request.Param(r, "provider", "")
if provider == "" {
logger.Info("[OAuth2] Invalid or missing provider")
@@ -40,7 +42,7 @@ func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
}
if !hasPassword {
- sess.NewFlashErrorMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("error.unlink_account_without_password"))
+ sess.NewFlashErrorMessage(printer.Printf("error.unlink_account_without_password"))
response.Redirect(w, r, route.Path(c.router, "settings"))
return
}
@@ -50,6 +52,6 @@ func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
return
}
- sess.NewFlashMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("alert.account_unlinked"))
+ sess.NewFlashMessage(printer.Printf("alert.account_unlinked"))
response.Redirect(w, r, route.Path(c.router, "settings"))
}
diff --git a/ui/settings_update.go b/ui/settings_update.go
index 6daf28c..ed424e3 100644
--- a/ui/settings_update.go
+++ b/ui/settings_update.go
@@ -69,6 +69,6 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
sess.SetLanguage(user.Language)
sess.SetTheme(user.Theme)
- sess.NewFlashMessage(c.translator.GetLanguage(request.UserLanguage(r)).Get("alert.prefs_saved"))
+ sess.NewFlashMessage(locale.NewPrinter(request.UserLanguage(r)).Printf("alert.prefs_saved"))
response.Redirect(w, r, route.Path(c.router, "settings"))
}