navidrome/ui/src/i18n/provider.js
Deluan Quintão 4b1218eec0
feat(ui): show translation completion percentage in the language selector (#5979)
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.
2026-08-18 09:32:26 -04:00

67 lines
2.1 KiB
JavaScript

import polyglotI18nProvider from 'ra-i18n-polyglot'
import deepmerge from 'deepmerge'
import dataProvider from '../dataProvider'
import en from './en.json'
import { i18nProvider } from './index'
// Only returns current selected locale if its translations are found in localStorage
const defaultLocale = function () {
const locale = localStorage.getItem('locale')
const current = JSON.parse(localStorage.getItem('translation'))
if (current && current.id === locale) {
// Asynchronously reload the translation from the server
retrieveTranslation(locale).then(() => {
i18nProvider.changeLocale(locale)
})
return locale
}
return 'en'
}
export function retrieveTranslation(locale) {
return dataProvider.getOne('translation', { id: locale }).then((res) => {
localStorage.setItem('translation', JSON.stringify(res.data))
return prepareLanguage(JSON.parse(res.data.data))
})
}
const removeEmpty = (obj) => {
for (let k in obj) {
if (
Object.prototype.hasOwnProperty.call(obj, k) &&
typeof obj[k] === 'object'
) {
removeEmpty(obj[k])
} else {
if (!obj[k]) {
delete obj[k]
}
}
}
}
const prepareLanguage = (lang) => {
removeEmpty(lang)
// Aliases below go on the merged copy: mutating `en` would corrupt the completion baseline
const merged = deepmerge(en, lang)
// Make "albumSong" and "playlistTrack" resource use the same translations as "song"
merged.resources.albumSong = merged.resources.song
merged.resources.playlistTrack = merged.resources.song
// ra.boolean.null should always be empty
merged.ra.boolean.null = ''
return merged
}
export default polyglotI18nProvider((locale) => {
// English is bundled
if (locale === 'en') {
return prepareLanguage(en)
}
// If the requested locale is in already loaded, return it
const current = JSON.parse(localStorage.getItem('translation'))
if (current && current.id === locale) {
return prepareLanguage(JSON.parse(current.data))
}
// If not, get it from the server, and store it in localStorage
return retrieveTranslation(locale)
}, defaultLocale())