/** * 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} */ 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);