0e26f12426
getTranslationDict lazily populates the package-level defaultCatalog map and runs on concurrent request goroutines via template functions, flash messages, and error translation. Two concurrent requests for languages not yet cached triggered Go's fatal "concurrent map writes" and killed the daemon. Guard the catalog with a sync.RWMutex using double-checked locking so each language is loaded once. A failed load is no longer cached as an empty dictionary, so unknown languages now return the error on every call; both callers treat an error and an empty dictionary identically, so rendered output is unchanged. Add a regression test exercising concurrent lazy population; it fails under the race detector on the previous implementation.
107 lines
2.6 KiB
Go
107 lines
2.6 KiB
Go
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package locale // import "miniflux.app/v2/internal/locale"
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
type translationDict struct {
|
|
singulars map[string]string
|
|
plurals map[string][]string
|
|
}
|
|
type catalog map[string]translationDict
|
|
|
|
// defaultCatalog is populated lazily by getTranslationDict, which runs on
|
|
// concurrent request goroutines, so every access must hold defaultCatalogMutex.
|
|
var (
|
|
defaultCatalog = make(catalog, len(AvailableLanguages))
|
|
defaultCatalogMutex sync.RWMutex
|
|
)
|
|
|
|
//go:embed translations/*.json
|
|
var translationFiles embed.FS
|
|
|
|
func getTranslationDict(language string) (translationDict, error) {
|
|
defaultCatalogMutex.RLock()
|
|
dict, found := defaultCatalog[language]
|
|
defaultCatalogMutex.RUnlock()
|
|
if found {
|
|
return dict, nil
|
|
}
|
|
|
|
defaultCatalogMutex.Lock()
|
|
defer defaultCatalogMutex.Unlock()
|
|
|
|
if dict, found := defaultCatalog[language]; found {
|
|
return dict, nil
|
|
}
|
|
|
|
dict, err := loadTranslationFile(language)
|
|
if err != nil {
|
|
return translationDict{}, err
|
|
}
|
|
defaultCatalog[language] = dict
|
|
return dict, nil
|
|
}
|
|
|
|
func loadTranslationFile(language string) (translationDict, error) {
|
|
translationFileData, err := translationFiles.ReadFile("translations/" + language + ".json")
|
|
if err != nil {
|
|
return translationDict{}, err
|
|
}
|
|
|
|
translationMessages, err := parseTranslationMessages(translationFileData)
|
|
if err != nil {
|
|
return translationDict{}, err
|
|
}
|
|
|
|
return translationMessages, nil
|
|
}
|
|
|
|
func (t *translationDict) UnmarshalJSON(data []byte) error {
|
|
var tmpMap map[string]any
|
|
err := json.Unmarshal(data, &tmpMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m := translationDict{
|
|
singulars: make(map[string]string),
|
|
plurals: make(map[string][]string),
|
|
}
|
|
|
|
for key, value := range tmpMap {
|
|
switch vtype := value.(type) {
|
|
case string:
|
|
m.singulars[key] = vtype
|
|
case []any:
|
|
for _, translation := range vtype {
|
|
if translationStr, ok := translation.(string); ok {
|
|
m.plurals[key] = append(m.plurals[key], translationStr)
|
|
} else {
|
|
return fmt.Errorf("invalid type for translation in an array: %v", translation)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid type (%T) for translation: %v", vtype, value)
|
|
}
|
|
}
|
|
|
|
*t = m
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseTranslationMessages(data []byte) (translationDict, error) {
|
|
var translationMessages translationDict
|
|
if err := json.Unmarshal(data, &translationMessages); err != nil {
|
|
return translationDict{}, fmt.Errorf(`invalid translation file: %w`, err)
|
|
}
|
|
return translationMessages, nil
|
|
}
|