mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
The language selector now shows how complete each translation is, so users can see at a glance which languages are lagging behind English. The native API's translation resource gained a termCount field holding the number of non-empty terms in each language file; the UI divides that by the term count of the bundled English file to get the percentage. The percentage is wrapped in a Unicode left-to-right isolate, otherwise it renders as "(%61)" beside right-to-left names such as Arabic and Persian. Sorting runs on the plain language name, before the percentage is appended. This also fixes prepareLanguage() mutating the bundled English translations: for the English locale it received the shared en object and aliased albumSong and playlistTrack onto it, growing en by 94 keys at runtime. That inflated the denominator and made every language read about 14 points low. The aliases now go on the merged copy instead.
144 lines
3.4 KiB
Go
144 lines
3.4 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/deluan/rest"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/resources"
|
|
)
|
|
|
|
type translation struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Data string `json:"data"`
|
|
TermCount int `json:"termCount"`
|
|
}
|
|
|
|
func newTranslationRepository(context.Context) rest.Repository {
|
|
return &translationRepository{}
|
|
}
|
|
|
|
type translationRepository struct{}
|
|
|
|
func (r *translationRepository) Read(id string) (any, error) {
|
|
translations, _ := loadTranslations()
|
|
if t, ok := translations[id]; ok {
|
|
return t, nil
|
|
}
|
|
return nil, rest.ErrNotFound
|
|
}
|
|
|
|
// Count simple implementation, does not support any `options`
|
|
func (r *translationRepository) Count(...rest.QueryOptions) (int64, error) {
|
|
_, count := loadTranslations()
|
|
return count, nil
|
|
}
|
|
|
|
// ReadAll simple implementation, only returns IDs. Does not support any `options`
|
|
func (r *translationRepository) ReadAll(...rest.QueryOptions) (any, error) {
|
|
translations, _ := loadTranslations()
|
|
var result []translation
|
|
for _, t := range translations {
|
|
t.Data = ""
|
|
result = append(result, t)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *translationRepository) EntityName() string {
|
|
return "translation"
|
|
}
|
|
|
|
func (r *translationRepository) NewInstance() any {
|
|
return &translation{}
|
|
}
|
|
|
|
var loadTranslations = sync.OnceValues(func() (map[string]translation, int64) {
|
|
translations := make(map[string]translation)
|
|
fsys := resources.FS()
|
|
dir, err := fsys.Open(consts.I18nFolder)
|
|
if err != nil {
|
|
log.Error("Error opening translation folder", err)
|
|
return translations, 0
|
|
}
|
|
files, err := dir.(fs.ReadDirFile).ReadDir(-1)
|
|
if err != nil {
|
|
log.Error("Error reading translation folder", err)
|
|
return translations, 0
|
|
}
|
|
var languages []string
|
|
for _, f := range files {
|
|
t, err := loadTranslation(fsys, f.Name())
|
|
if err != nil {
|
|
log.Error("Error loading translation file", "file", f.Name(), err)
|
|
continue
|
|
}
|
|
translations[t.ID] = t
|
|
languages = append(languages, t.ID)
|
|
}
|
|
log.Info("Loaded translations", "languages", languages)
|
|
return translations, int64(len(translations))
|
|
})
|
|
|
|
func loadTranslation(fsys fs.FS, fileName string) (translation translation, err error) {
|
|
// Get id and full path
|
|
name := path.Base(fileName)
|
|
id := strings.TrimSuffix(name, path.Ext(name))
|
|
filePath := path.Join(consts.I18nFolder, name)
|
|
|
|
// Load translation from json file
|
|
file, err := fsys.Open(filePath)
|
|
if err != nil {
|
|
return translation, err
|
|
}
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return translation, err
|
|
}
|
|
var out map[string]any
|
|
if err = json.Unmarshal(data, &out); err != nil {
|
|
return translation, err
|
|
}
|
|
|
|
// Compress JSON
|
|
buf := new(bytes.Buffer)
|
|
if err = json.Compact(buf, data); err != nil {
|
|
return translation, err
|
|
}
|
|
|
|
translation.Data = buf.String()
|
|
translation.Name = out["languageName"].(string)
|
|
translation.ID = id
|
|
translation.TermCount = countTranslatedTerms(out)
|
|
return translation, nil
|
|
}
|
|
|
|
// countTranslatedTerms counts non-empty leaf values, matching the UI's notion of a translated term
|
|
func countTranslatedTerms(obj map[string]any) int {
|
|
count := 0
|
|
for _, v := range obj {
|
|
switch v := v.(type) {
|
|
case map[string]any:
|
|
count += countTranslatedTerms(v)
|
|
case string:
|
|
if v != "" {
|
|
count++
|
|
}
|
|
default:
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
var _ rest.Repository = (*translationRepository)(nil)
|