Ver código fonte

refactor(locale): delay parsing of translations until they're used

While doing some profiling for #2900, I noticed that
`miniflux.app/v2/internal/locale.LoadCatalogMessages` is responsible for more
than 10% of the consumed memory. As most miniflux instances won't have enough
diverse users to use all the available translations at the same time, it
makes sense to load them on demand.

The overhead is a single function call and a check in a map, per call to
translation-related functions.
Julien Voisin 1 ano atrás
pai
commit
eed3fcf92a

+ 0 - 5
internal/cli/cli.go

@@ -13,7 +13,6 @@ import (
 
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/database"
-	"miniflux.app/v2/internal/locale"
 	"miniflux.app/v2/internal/storage"
 	"miniflux.app/v2/internal/ui/static"
 	"miniflux.app/v2/internal/version"
@@ -153,10 +152,6 @@ func Parse() {
 		slog.Info("The default value for DATABASE_URL is used")
 	}
 
-	if err := locale.LoadCatalogMessages(); err != nil {
-		printErrorAndExit(fmt.Errorf("unable to load translations: %v", err))
-	}
-
 	if err := static.CalculateBinaryFileChecksums(); err != nil {
 		printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err))
 	}

+ 12 - 3
internal/locale/catalog.go

@@ -12,17 +12,26 @@ import (
 type translationDict map[string]interface{}
 type catalog map[string]translationDict
 
-var defaultCatalog catalog
+var defaultCatalog = make(catalog, len(AvailableLanguages))
 
 //go:embed translations/*.json
 var translationFiles embed.FS
 
+func GetTranslationDict(language string) (translationDict, error) {
+	if _, ok := defaultCatalog[language]; !ok {
+		var err error
+		if defaultCatalog[language], err = loadTranslationFile(language); err != nil {
+			return nil, err
+		}
+	}
+	return defaultCatalog[language], nil
+}
+
 // LoadCatalogMessages loads and parses all translations encoded in JSON.
 func LoadCatalogMessages() error {
 	var err error
-	defaultCatalog = make(catalog, len(AvailableLanguages()))
 
-	for language := range AvailableLanguages() {
+	for language := range AvailableLanguages {
 		defaultCatalog[language], err = loadTranslationFile(language)
 		if err != nil {
 			return err

+ 3 - 3
internal/locale/catalog_test.go

@@ -39,7 +39,7 @@ func TestLoadCatalog(t *testing.T) {
 }
 
 func TestAllKeysHaveValue(t *testing.T) {
-	for language := range AvailableLanguages() {
+	for language := range AvailableLanguages {
 		messages, err := loadTranslationFile(language)
 		if err != nil {
 			t.Fatalf(`Unable to load translation messages for language %q`, language)
@@ -71,7 +71,7 @@ func TestMissingTranslations(t *testing.T) {
 		t.Fatal(`Unable to parse reference language`)
 	}
 
-	for language := range AvailableLanguages() {
+	for language := range AvailableLanguages {
 		if language == refLang {
 			continue
 		}
@@ -110,7 +110,7 @@ func TestTranslationFilePluralForms(t *testing.T) {
 		"uk_UA": 3,
 		"id_ID": 1,
 	}
-	for language := range AvailableLanguages() {
+	for language := range AvailableLanguages {
 		messages, err := loadTranslationFile(language)
 		if err != nil {
 			t.Fatalf(`Unable to load translation messages for language %q`, language)

+ 20 - 22
internal/locale/locale.go

@@ -3,26 +3,24 @@
 
 package locale // import "miniflux.app/v2/internal/locale"
 
-// AvailableLanguages returns the list of available languages.
-func AvailableLanguages() map[string]string {
-	return map[string]string{
-		"en_US": "English",
-		"es_ES": "Español",
-		"fr_FR": "Français",
-		"de_DE": "Deutsch",
-		"pl_PL": "Polski",
-		"pt_BR": "Português Brasileiro",
-		"zh_CN": "简体中文",
-		"zh_TW": "繁體中文",
-		"nl_NL": "Nederlands",
-		"ru_RU": "Русский",
-		"it_IT": "Italiano",
-		"ja_JP": "日本語",
-		"tr_TR": "Türkçe",
-		"el_EL": "Ελληνικά",
-		"fi_FI": "Suomi",
-		"hi_IN": "हिन्दी",
-		"uk_UA": "Українська",
-		"id_ID": "Bahasa Indonesia",
-	}
+// AvailableLanguages is the list of available languages.
+var AvailableLanguages = map[string]string{
+	"en_US": "English",
+	"es_ES": "Español",
+	"fr_FR": "Français",
+	"de_DE": "Deutsch",
+	"pl_PL": "Polski",
+	"pt_BR": "Português Brasileiro",
+	"zh_CN": "简体中文",
+	"zh_TW": "繁體中文",
+	"nl_NL": "Nederlands",
+	"ru_RU": "Русский",
+	"it_IT": "Italiano",
+	"ja_JP": "日本語",
+	"tr_TR": "Türkçe",
+	"el_EL": "Ελληνικά",
+	"fi_FI": "Suomi",
+	"hi_IN": "हिन्दी",
+	"uk_UA": "Українська",
+	"id_ID": "Bahasa Indonesia",
 }

+ 1 - 1
internal/locale/locale_test.go

@@ -6,7 +6,7 @@ package locale // import "miniflux.app/v2/internal/locale"
 import "testing"
 
 func TestAvailableLanguages(t *testing.T) {
-	results := AvailableLanguages()
+	results := AvailableLanguages
 	for k, v := range results {
 		if k == "" {
 			t.Errorf(`Empty language key detected`)

+ 19 - 14
internal/locale/printer.go

@@ -11,9 +11,11 @@ type Printer struct {
 }
 
 func (p *Printer) Print(key string) string {
-	if str, ok := defaultCatalog[p.language][key]; ok {
-		if translation, ok := str.(string); ok {
-			return translation
+	if dict, err := GetTranslationDict(p.language); err == nil {
+		if str, ok := dict[key]; ok {
+			if translation, ok := str.(string); ok {
+				return translation
+			}
 		}
 	}
 	return key
@@ -21,16 +23,16 @@ func (p *Printer) Print(key string) string {
 
 // Printf is like fmt.Printf, but using language-specific formatting.
 func (p *Printer) Printf(key string, args ...interface{}) string {
-	var translation string
+	translation := key
 
-	str, found := defaultCatalog[p.language][key]
-	if !found {
-		translation = key
-	} else {
-		var valid bool
-		translation, valid = str.(string)
-		if !valid {
-			translation = key
+	if dict, err := GetTranslationDict(p.language); err == nil {
+		str, found := dict[key]
+		if found {
+			var valid bool
+			translation, valid = str.(string)
+			if !valid {
+				translation = key
+			}
 		}
 	}
 
@@ -39,9 +41,12 @@ func (p *Printer) Printf(key string, args ...interface{}) string {
 
 // 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]
+	dict, err := GetTranslationDict(p.language)
+	if err != nil {
+		return key
+	}
 
-	if found {
+	if choices, found := dict[key]; found {
 		var plurals []string
 
 		switch v := choices.(type) {

+ 1 - 1
internal/ui/settings_show.go

@@ -71,7 +71,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 		"MarkAsReadOnlyOnPlayerCompletion":           form.MarkAsReadOnlyOnPlayerCompletion,
 	})
 	view.Set("themes", model.Themes())
-	view.Set("languages", locale.AvailableLanguages())
+	view.Set("languages", locale.AvailableLanguages)
 	view.Set("timezones", timezones)
 	view.Set("menu", "settings")
 	view.Set("user", user)

+ 1 - 1
internal/ui/settings_update.go

@@ -44,7 +44,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
 	view := view.New(h.tpl, r, sess)
 	view.Set("form", settingsForm)
 	view.Set("themes", model.Themes())
-	view.Set("languages", locale.AvailableLanguages())
+	view.Set("languages", locale.AvailableLanguages)
 	view.Set("timezones", timezones)
 	view.Set("menu", "settings")
 	view.Set("user", loggedUser)

+ 1 - 1
internal/validator/user.go

@@ -155,7 +155,7 @@ func validateTheme(theme string) *locale.LocalizedError {
 }
 
 func validateLanguage(language string) *locale.LocalizedError {
-	languages := locale.AvailableLanguages()
+	languages := locale.AvailableLanguages
 	if _, found := languages[language]; !found {
 		return locale.NewLocalizedError("error.invalid_language")
 	}