Files
HomeAssistantVS/custom_components/taskmate/www/taskmate-localize.js
T
2026-07-08 10:43:39 -04:00

150 lines
4.6 KiB
JavaScript

/**
* TaskMate i18n Module
* Lightweight translation loader for TaskMate custom cards.
*
* Usage in a card:
* // loaded via window.__taskmate_localize (registered globally)
*
* localize(this.hass, 'common.today') → "Today"
* localize(this.hass, 'common.points_received',
* { count: 5, name: 'Stars' }) → "5 Stars received"
*
* Language resolution: nb → nb.json → en-GB.json → key
* The base (fallback) language is en-GB since that is the source locale.
*/
const _cache = {}; // lang → { key: value } (null = tried and failed)
const _pending = {}; // lang → Promise
const _BASE = '/taskmate/locales';
const _FALLBACK = 'en-GB'; // base language file
/**
* Load a locale file by language code. Returns cached result if available.
* Caches failures (as null) so we don't retry on every render.
*/
async function _loadLocale(lang) {
if (lang in _cache) return _cache[lang];
if (_pending[lang]) return _pending[lang];
_pending[lang] = (async () => {
try {
const resp = await fetch(`${_BASE}/${lang}.json?v=${Date.now()}`);
if (!resp.ok) throw new Error(resp.status);
const data = await resp.json();
_cache[lang] = data;
_refreshTaskMateCards();
return data;
} catch {
_cache[lang] = null;
return null;
} finally {
delete _pending[lang];
}
})();
return _pending[lang];
}
/**
* Resolve language to a list of codes to try (most specific first).
* e.g. "en-GB" → ["en-GB"]
* "nb" → ["nb"]
* "pt-BR" → ["pt-BR", "pt"]
*/
function _langCandidates(lang) {
const candidates = [lang];
if (lang.includes('-')) {
candidates.push(lang.split('-')[0]);
}
return candidates;
}
/**
* Synchronous lookup — returns the translated string or the fallback.
* Triggers async load in the background if the locale isn't cached yet.
*
* @param {object} hass - Home Assistant hass object (for hass.language)
* @param {string} key - Dot-notation key, e.g. 'common.today'
* @param {object} [params] - Replacement params, e.g. { count: 5 }
* @returns {string}
*/
function localize(hass, key, params) {
const lang = (hass && hass.language) || _FALLBACK;
const candidates = _langCandidates(lang);
// Try each candidate language, then fall back to base language
let str = undefined;
for (const candidate of candidates) {
if (_cache[candidate] && _cache[candidate][key]) {
str = _cache[candidate][key];
break;
}
}
if (str === undefined && _cache[_FALLBACK]) {
str = _cache[_FALLBACK][key];
}
if (str === undefined) {
str = key;
}
// Trigger background loads for any language not yet in cache
for (const candidate of candidates) {
if (!(candidate in _cache)) _loadLocale(candidate);
}
if (!(_FALLBACK in _cache)) _loadLocale(_FALLBACK);
// Replace {placeholder} tokens
if (params && typeof str === 'string') {
for (const [k, v] of Object.entries(params)) {
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
}
}
return str;
}
/**
* Pre-load a locale so translations are ready before first render.
* Call this in connectedCallback() of your card.
*
* @param {object} hass - Home Assistant hass object
* @returns {Promise<void>}
*/
async function loadTranslations(hass) {
const lang = (hass && hass.language) || _FALLBACK;
const candidates = _langCandidates(lang);
await Promise.all([
_loadLocale(_FALLBACK),
...candidates.filter(c => c !== _FALLBACK).map(c => _loadLocale(c)),
]);
}
// Walk the DOM across shadow roots and trigger a re-render on TaskMate cards.
// Called after locale files load so cards pick up the new translations.
function _refreshTaskMateCards() {
const stack = [document];
while (stack.length) {
const node = stack.pop();
if (!node) continue;
if (node.tagName && node.tagName.startsWith('TASKMATE-')) {
if (typeof node.requestUpdate === 'function') {
try { node.requestUpdate(); } catch (_e) { /* ignore */ }
}
}
if (node.shadowRoot) stack.push(node.shadowRoot);
const children = node.children;
if (children) {
for (let i = 0; i < children.length; i++) stack.push(children[i]);
}
}
}
// Expose globally so cards can access without ES module imports
window.__taskmate_localize = localize;
window.__taskmate_loadTranslations = loadTranslations;
// Pre-load the fallback locale so translations are ready as soon as possible.
// Cards that render before this completes will show raw keys momentarily,
// then _loadLocale calls _refreshTaskMateCards to re-render them.
_loadLocale(_FALLBACK);