ソースを参照

Simplify locale package usage (refactoring)

Frédéric Guillot 7 年 前
コミット
b1e8f534ef

+ 2 - 4
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,

+ 3 - 4
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() != "" {

+ 2 - 3
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 != "" {

+ 2 - 2
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.

+ 36 - 0
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
+}

+ 2 - 2
locale/parser_test.go → 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)
 	}

+ 0 - 50
locale/language.go

@@ -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...)
-}

+ 1 - 15
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{

+ 13 - 92
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)`)
 	}
 }

+ 0 - 21
locale/parser.go

@@ -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
-}

+ 4 - 2
locale/plurals.go → 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 {

+ 63 - 0
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)
+			}
+		}
+	}
+}

+ 67 - 0
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}
+}

+ 174 - 0
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)
+	}
+}

+ 6 - 6
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 {

+ 0 - 31
locale/translator.go

@@ -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)}
-}

+ 8 - 9
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}
 }

+ 13 - 15
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()

+ 11 - 11
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)
 	}
 }

+ 14 - 16
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)
 		}
 	}

+ 1 - 4
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,
 	}
 }

+ 6 - 3
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"))
 }

+ 4 - 2
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"))
 }

+ 4 - 2
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
 	}

+ 4 - 2
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"))
 }

+ 1 - 1
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"))
 }