Skip to content

Commit 142775c

Browse files
jvoisinWShihan
authored andcommitted
refactor(locale): delay parsing of translations until they're used
While doing some profiling for miniflux#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.
1 parent 4b10e61 commit 142775c

File tree

9 files changed

+58
-51
lines changed

9 files changed

+58
-51
lines changed

internal/cli/cli.go

-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"miniflux.app/v2/internal/config"
1515
"miniflux.app/v2/internal/database"
16-
"miniflux.app/v2/internal/locale"
1716
"miniflux.app/v2/internal/storage"
1817
"miniflux.app/v2/internal/ui/static"
1918
"miniflux.app/v2/internal/version"
@@ -153,10 +152,6 @@ func Parse() {
153152
slog.Info("The default value for DATABASE_URL is used")
154153
}
155154

156-
if err := locale.LoadCatalogMessages(); err != nil {
157-
printErrorAndExit(fmt.Errorf("unable to load translations: %v", err))
158-
}
159-
160155
if err := static.CalculateBinaryFileChecksums(); err != nil {
161156
printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err))
162157
}

internal/locale/catalog.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ import (
1212
type translationDict map[string]interface{}
1313
type catalog map[string]translationDict
1414

15-
var defaultCatalog catalog
15+
var defaultCatalog = make(catalog, len(AvailableLanguages))
1616

1717
//go:embed translations/*.json
1818
var translationFiles embed.FS
1919

20+
func GetTranslationDict(language string) (translationDict, error) {
21+
if _, ok := defaultCatalog[language]; !ok {
22+
var err error
23+
if defaultCatalog[language], err = loadTranslationFile(language); err != nil {
24+
return nil, err
25+
}
26+
}
27+
return defaultCatalog[language], nil
28+
}
29+
2030
// LoadCatalogMessages loads and parses all translations encoded in JSON.
2131
func LoadCatalogMessages() error {
2232
var err error
23-
defaultCatalog = make(catalog, len(AvailableLanguages()))
2433

25-
for language := range AvailableLanguages() {
34+
for language := range AvailableLanguages {
2635
defaultCatalog[language], err = loadTranslationFile(language)
2736
if err != nil {
2837
return err

internal/locale/catalog_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func TestLoadCatalog(t *testing.T) {
3939
}
4040

4141
func TestAllKeysHaveValue(t *testing.T) {
42-
for language := range AvailableLanguages() {
42+
for language := range AvailableLanguages {
4343
messages, err := loadTranslationFile(language)
4444
if err != nil {
4545
t.Fatalf(`Unable to load translation messages for language %q`, language)
@@ -71,7 +71,7 @@ func TestMissingTranslations(t *testing.T) {
7171
t.Fatal(`Unable to parse reference language`)
7272
}
7373

74-
for language := range AvailableLanguages() {
74+
for language := range AvailableLanguages {
7575
if language == refLang {
7676
continue
7777
}
@@ -110,7 +110,7 @@ func TestTranslationFilePluralForms(t *testing.T) {
110110
"uk_UA": 3,
111111
"id_ID": 1,
112112
}
113-
for language := range AvailableLanguages() {
113+
for language := range AvailableLanguages {
114114
messages, err := loadTranslationFile(language)
115115
if err != nil {
116116
t.Fatalf(`Unable to load translation messages for language %q`, language)

internal/locale/locale.go

+20-22
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,24 @@
33

44
package locale // import "miniflux.app/v2/internal/locale"
55

6-
// AvailableLanguages returns the list of available languages.
7-
func AvailableLanguages() map[string]string {
8-
return map[string]string{
9-
"en_US": "English",
10-
"es_ES": "Español",
11-
"fr_FR": "Français",
12-
"de_DE": "Deutsch",
13-
"pl_PL": "Polski",
14-
"pt_BR": "Português Brasileiro",
15-
"zh_CN": "简体中文",
16-
"zh_TW": "繁體中文",
17-
"nl_NL": "Nederlands",
18-
"ru_RU": "Русский",
19-
"it_IT": "Italiano",
20-
"ja_JP": "日本語",
21-
"tr_TR": "Türkçe",
22-
"el_EL": "Ελληνικά",
23-
"fi_FI": "Suomi",
24-
"hi_IN": "हिन्दी",
25-
"uk_UA": "Українська",
26-
"id_ID": "Bahasa Indonesia",
27-
}
6+
// AvailableLanguages is the list of available languages.
7+
var AvailableLanguages = map[string]string{
8+
"en_US": "English",
9+
"es_ES": "Español",
10+
"fr_FR": "Français",
11+
"de_DE": "Deutsch",
12+
"pl_PL": "Polski",
13+
"pt_BR": "Português Brasileiro",
14+
"zh_CN": "简体中文",
15+
"zh_TW": "繁體中文",
16+
"nl_NL": "Nederlands",
17+
"ru_RU": "Русский",
18+
"it_IT": "Italiano",
19+
"ja_JP": "日本語",
20+
"tr_TR": "Türkçe",
21+
"el_EL": "Ελληνικά",
22+
"fi_FI": "Suomi",
23+
"hi_IN": "हिन्दी",
24+
"uk_UA": "Українська",
25+
"id_ID": "Bahasa Indonesia",
2826
}

internal/locale/locale_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package locale // import "miniflux.app/v2/internal/locale"
66
import "testing"
77

88
func TestAvailableLanguages(t *testing.T) {
9-
results := AvailableLanguages()
9+
results := AvailableLanguages
1010
for k, v := range results {
1111
if k == "" {
1212
t.Errorf(`Empty language key detected`)

internal/locale/printer.go

+19-14
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,28 @@ type Printer struct {
1111
}
1212

1313
func (p *Printer) Print(key string) string {
14-
if str, ok := defaultCatalog[p.language][key]; ok {
15-
if translation, ok := str.(string); ok {
16-
return translation
14+
if dict, err := GetTranslationDict(p.language); err == nil {
15+
if str, ok := dict[key]; ok {
16+
if translation, ok := str.(string); ok {
17+
return translation
18+
}
1719
}
1820
}
1921
return key
2022
}
2123

2224
// Printf is like fmt.Printf, but using language-specific formatting.
2325
func (p *Printer) Printf(key string, args ...interface{}) string {
24-
var translation string
26+
translation := key
2527

26-
str, found := defaultCatalog[p.language][key]
27-
if !found {
28-
translation = key
29-
} else {
30-
var valid bool
31-
translation, valid = str.(string)
32-
if !valid {
33-
translation = key
28+
if dict, err := GetTranslationDict(p.language); err == nil {
29+
str, found := dict[key]
30+
if found {
31+
var valid bool
32+
translation, valid = str.(string)
33+
if !valid {
34+
translation = key
35+
}
3436
}
3537
}
3638

@@ -39,9 +41,12 @@ func (p *Printer) Printf(key string, args ...interface{}) string {
3941

4042
// Plural returns the translation of the given key by using the language plural form.
4143
func (p *Printer) Plural(key string, n int, args ...interface{}) string {
42-
choices, found := defaultCatalog[p.language][key]
44+
dict, err := GetTranslationDict(p.language)
45+
if err != nil {
46+
return key
47+
}
4348

44-
if found {
49+
if choices, found := dict[key]; found {
4550
var plurals []string
4651

4752
switch v := choices.(type) {

internal/ui/settings_show.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
7171
"MarkAsReadOnlyOnPlayerCompletion": form.MarkAsReadOnlyOnPlayerCompletion,
7272
})
7373
view.Set("themes", model.Themes())
74-
view.Set("languages", locale.AvailableLanguages())
74+
view.Set("languages", locale.AvailableLanguages)
7575
view.Set("timezones", timezones)
7676
view.Set("menu", "settings")
7777
view.Set("user", user)

internal/ui/settings_update.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
4444
view := view.New(h.tpl, r, sess)
4545
view.Set("form", settingsForm)
4646
view.Set("themes", model.Themes())
47-
view.Set("languages", locale.AvailableLanguages())
47+
view.Set("languages", locale.AvailableLanguages)
4848
view.Set("timezones", timezones)
4949
view.Set("menu", "settings")
5050
view.Set("user", loggedUser)

internal/validator/user.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func validateTheme(theme string) *locale.LocalizedError {
155155
}
156156

157157
func validateLanguage(language string) *locale.LocalizedError {
158-
languages := locale.AvailableLanguages()
158+
languages := locale.AvailableLanguages
159159
if _, found := languages[language]; !found {
160160
return locale.NewLocalizedError("error.invalid_language")
161161
}

0 commit comments

Comments
 (0)