From eed3fcf92aee72f352520213d34146aee171efde Mon Sep 17 00:00:00 2001 From: Julien Voisin Date: Tue, 10 Dec 2024 01:05:14 +0000 Subject: [PATCH] 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. --- internal/cli/cli.go | 5 ---- internal/locale/catalog.go | 15 +++++++++--- internal/locale/catalog_test.go | 6 ++--- internal/locale/locale.go | 42 ++++++++++++++++----------------- internal/locale/locale_test.go | 2 +- internal/locale/printer.go | 33 +++++++++++++++----------- internal/ui/settings_show.go | 2 +- internal/ui/settings_update.go | 2 +- internal/validator/user.go | 2 +- 9 files changed, 58 insertions(+), 51 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index ca4f47bd..fc074717 100644 --- a/internal/cli/cli.go +++ b/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)) } diff --git a/internal/locale/catalog.go b/internal/locale/catalog.go index 61f5f27d..8ecdab74 100644 --- a/internal/locale/catalog.go +++ b/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 diff --git a/internal/locale/catalog_test.go b/internal/locale/catalog_test.go index 75537911..687b1de2 100644 --- a/internal/locale/catalog_test.go +++ b/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) diff --git a/internal/locale/locale.go b/internal/locale/locale.go index a5a1010b..aa6165b8 100644 --- a/internal/locale/locale.go +++ b/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", } diff --git a/internal/locale/locale_test.go b/internal/locale/locale_test.go index 86b52820..32f6a40f 100644 --- a/internal/locale/locale_test.go +++ b/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`) diff --git a/internal/locale/printer.go b/internal/locale/printer.go index f85960fa..d997c1a7 100644 --- a/internal/locale/printer.go +++ b/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) { diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 179b9802..eae72a7f 100644 --- a/internal/ui/settings_show.go +++ b/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) diff --git a/internal/ui/settings_update.go b/internal/ui/settings_update.go index be99adb5..5610a9a9 100644 --- a/internal/ui/settings_update.go +++ b/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) diff --git a/internal/validator/user.go b/internal/validator/user.go index 2e79785b..b461f912 100644 --- a/internal/validator/user.go +++ b/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") }