85 lines
3.1 KiB
JavaScript
85 lines
3.1 KiB
JavaScript
// ── Chimera i18n engine ───────────────────────────────────────────────────────
|
|
// Loaded first so window.t() is available to all modules.
|
|
// Call window.i18n.load(code) to fetch a lang file and re-apply all translations.
|
|
|
|
window.chimeraI18nReady = new Promise(resolve => {
|
|
window.chimeraResolveI18nReady = resolve;
|
|
});
|
|
|
|
window.i18n = {
|
|
current: 'en',
|
|
strings: {},
|
|
_observer: null,
|
|
|
|
/** Translate a key. Falls back to `fallback` if provided, then the key itself. */
|
|
t(key, fallback) {
|
|
const v = this.strings[key];
|
|
if (v !== undefined) return v;
|
|
return fallback !== undefined ? fallback : key;
|
|
},
|
|
|
|
/** Walk every [data-i18n] element and set its textContent to the translated string.
|
|
* If no translation exists for a key the original text is left unchanged. */
|
|
apply(root = document) {
|
|
document.documentElement.lang = this.current || 'en';
|
|
const select = selector => [
|
|
...(root.matches?.(selector) ? [root] : []),
|
|
...(root.querySelectorAll?.(selector) || []),
|
|
];
|
|
select('[data-i18n]').forEach(el => {
|
|
const val = this.strings[el.dataset.i18n];
|
|
if (val !== undefined) el.textContent = val;
|
|
});
|
|
select('[data-i18n-placeholder]').forEach(el => {
|
|
const val = this.strings[el.dataset.i18nPlaceholder];
|
|
if (val !== undefined) el.placeholder = val;
|
|
});
|
|
select('[data-i18n-title]').forEach(el => {
|
|
const val = this.strings[el.dataset.i18nTitle];
|
|
if (val !== undefined) el.title = val;
|
|
});
|
|
select('[data-i18n-aria-label]').forEach(el => {
|
|
const val = this.strings[el.dataset.i18nAriaLabel];
|
|
if (val !== undefined) el.setAttribute('aria-label', val);
|
|
});
|
|
if (!this._observer && document.body) {
|
|
this._observer = new MutationObserver(records => {
|
|
for (const record of records) {
|
|
for (const node of record.addedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) this.apply(node);
|
|
}
|
|
}
|
|
});
|
|
this._observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
},
|
|
|
|
/** Load a language file by code (e.g. "en", "fr", "de") via IPC, then apply. */
|
|
async load(code) {
|
|
if (!code) code = 'en';
|
|
try {
|
|
const data = await window.pork.langLoad(code);
|
|
if (data && typeof data.strings === 'object') {
|
|
this.strings = data.strings;
|
|
this.current = data.meta?.code || code;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[i18n] Could not load language "' + code + '":', e.message || e);
|
|
// Fall back to empty (original HTML text remains visible)
|
|
this.strings = {};
|
|
this.current = 'en';
|
|
}
|
|
this.apply();
|
|
window.dispatchEvent(new CustomEvent('chimera:language-changed', {
|
|
detail: { code: this.current },
|
|
}));
|
|
},
|
|
};
|
|
|
|
/** Shorthand global translate function */
|
|
window.t = (key, fallback) => window.i18n.t(key, fallback);
|
|
window.tf = (key, fallback, values = {}) => Object.entries(values).reduce(
|
|
(text, [name, value]) => text.replaceAll(`{${name}}`, String(value)),
|
|
window.i18n.t(key, fallback),
|
|
);
|