1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-06-27 16:36:00 +00:00

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.
This commit is contained in:
Julien Voisin 2024-12-10 01:05:14 +00:00 committed by GitHub
parent d5cfcf8956
commit eed3fcf92a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 58 additions and 51 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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