Files
idle_clicker/public/js/services/i18n.js
T

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;
}