82 lines
2.8 KiB
JavaScript
82 lines
2.8 KiB
JavaScript
// Internationalisation.
|
|
//
|
|
// Two kinds of locale files are merged into one flat dictionary:
|
|
// locales/<lang>.json core UI texts (shared by all themes)
|
|
// themes/<id>/locales/<lang>.json theme texts, all under the "theme." root
|
|
// Missing keys fall back to English, then to the key itself.
|
|
//
|
|
// Strings support {param} interpolation and {@some.key} to embed another entry
|
|
// (e.g. "{@theme.res.material}"), so core texts can use theme vocabulary.
|
|
|
|
export const FALLBACK_LOCALE = 'en';
|
|
const MAX_EMBED_DEPTH = 5;
|
|
|
|
let dictionary = {};
|
|
let currentLocale = FALLBACK_LOCALE;
|
|
|
|
export const getLocale = () => currentLocale;
|
|
|
|
function flatten(object, prefix = '', out = {}) {
|
|
for (const [key, value] of Object.entries(object ?? {})) {
|
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
if (value && typeof value === 'object') flatten(value, path, out);
|
|
else out[path] = String(value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function fetchJson(url) {
|
|
try {
|
|
const response = await fetch(url);
|
|
return response.ok ? await response.json() : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export async function loadDictionary(locale, themeId) {
|
|
const order = locale === FALLBACK_LOCALE ? [FALLBACK_LOCALE] : [FALLBACK_LOCALE, locale];
|
|
const files = order.flatMap((l) => [`locales/${l}.json`, `themes/${themeId}/locales/${l}.json`]);
|
|
const parts = await Promise.all(files.map(fetchJson));
|
|
dictionary = Object.assign({}, ...parts.map((p) => flatten(p)));
|
|
currentLocale = locale;
|
|
document.documentElement.lang = locale;
|
|
}
|
|
|
|
export function has(key) {
|
|
return key in dictionary;
|
|
}
|
|
|
|
export function t(key, params = {}, depth = 0) {
|
|
const text = dictionary[key];
|
|
if (text === undefined) return key;
|
|
return text.replace(/\{(@?)([\w.]+)\}/g, (match, embed, name) => {
|
|
if (embed) return depth < MAX_EMBED_DEPTH ? t(name, params, depth + 1) : '';
|
|
return name in params ? String(params[name]) : match;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translate static markup:
|
|
* data-i18n="key" -> textContent
|
|
* data-i18n-attr="title:key;aria-label:key" -> attributes
|
|
*/
|
|
export function applyTranslations(root = document) {
|
|
for (const el of root.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
|
|
for (const el of root.querySelectorAll('[data-i18n-attr]')) {
|
|
for (const pair of el.dataset.i18nAttr.split(';')) {
|
|
const [attr, key] = pair.split(':').map((s) => s.trim());
|
|
if (attr && key) el.setAttribute(attr, t(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Display name of a theme in `locale` (falls back to English, then the id). */
|
|
export async function fetchThemeName(themeId, locale) {
|
|
for (const l of [locale, FALLBACK_LOCALE]) {
|
|
const file = await fetchJson(`themes/${themeId}/locales/${l}.json`);
|
|
if (file?.theme?.name) return file.theme.name;
|
|
}
|
|
return themeId;
|
|
}
|