456 files
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import { css } from "lit";
|
||||
import EN from "./locales/en.json";
|
||||
import { BUNDLE_VERSION } from "./helpers/bundle-version";
|
||||
|
||||
// Display fallback when the backend hasn't sent a currency symbol. The backend
|
||||
// derives the real symbol from const.BUDGET_CURRENCIES[DEFAULT_BUDGET_CURRENCY].
|
||||
@@ -24,84 +23,19 @@ export { STATUS_COLORS, STATUS_ICONS } from "./status-constants";
|
||||
|
||||
/* ─── i18n ─── */
|
||||
|
||||
interface Translations {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// English is bundled (imported above) as the always-available fallback so
|
||||
// the panel renders instantly with a complete table — never an
|
||||
// untranslated-key flash, even if a locale fetch fails. The other 17
|
||||
// languages live in served JSON (frontend/locales/<lang>.json) and are
|
||||
// fetched on demand by ensureLocale(), so a translation edit needs no bundle
|
||||
// rebuild — the fix for the recurring "stale bundle ships English to non-EN
|
||||
// users" pitfall. Steady-state t() is the same sync lookup as before.
|
||||
const DEFAULT_LANG = "en";
|
||||
|
||||
// The locale store MUST be shared across bundle copies of this module.
|
||||
// maintenance-card.js is loaded globally (extra_module_url) and defines the
|
||||
// dialog custom elements first-wins — so a <maintenance-task-dialog> inside
|
||||
// the panel runs the CARD bundle's copy of this module. With a module-scoped
|
||||
// store, the panel's ensureLocale() fills only the PANEL copy and the dialog
|
||||
// stays English while the rest of the panel is localized (v2.17.0 regression,
|
||||
// invisible before the runtime-locale split because every bundle inlined all
|
||||
// languages). One window-scoped store + inflight map keeps every copy reading
|
||||
// and writing the same tables.
|
||||
interface LocaleGlobals {
|
||||
store: Record<string, Translations>;
|
||||
inflight: Record<string, Promise<void>>;
|
||||
}
|
||||
const _localeGlobals: LocaleGlobals = (() => {
|
||||
const w = window as unknown as { __msLocales?: LocaleGlobals };
|
||||
if (!w.__msLocales) w.__msLocales = { store: {}, inflight: {} };
|
||||
return w.__msLocales;
|
||||
})();
|
||||
|
||||
const STORE = _localeGlobals.store;
|
||||
|
||||
/** Seed/extend the shared English table with this bundle's copy.
|
||||
*
|
||||
* MERGE, not first-wins (#135 regression): after an update, a cached app
|
||||
* shell can still list an OLD sibling bundle (the card) next to the fresh
|
||||
* panel. First-wins let the stale bundle's EN — missing every new key —
|
||||
* claim the store, and the fresh panel rendered raw keys
|
||||
* ("BATTERY_FLEET_ADD"). Existing entries win conflicts (steady state is
|
||||
* unchanged); every bundle contributes the keys it knows, so a newer
|
||||
* bundle's keys can never be shadowed by an older first-loader.
|
||||
*/
|
||||
export function seedEnglish(en: Record<string, string>): void {
|
||||
STORE.en = Object.assign({}, en, STORE.en ?? {});
|
||||
}
|
||||
// untranslated-key flash, even if a locale fetch fails. The other languages
|
||||
// live in served JSON (frontend/locales/<lang>.json) and are fetched on
|
||||
// demand by ensureLocale(), so a translation edit needs no bundle rebuild.
|
||||
// The store and lookup live in the Lit-free helpers/locale-core.ts (the
|
||||
// dashboard strategy uses them too); re-exported here so every existing
|
||||
// `from "./styles"` import keeps working.
|
||||
import { ensureLocale, isLocaleLoaded, normLang, seedEnglish, t, type Translations } from "./helpers/locale-core";
|
||||
import { primeBackendErrors } from "./helpers/backend-errors";
|
||||
export { ensureLocale, isLocaleLoaded, seedEnglish, setLocale, t } from "./helpers/locale-core";
|
||||
seedEnglish(EN as Translations);
|
||||
|
||||
/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */
|
||||
const SUPPORTED_LANGS = new Set<string>([
|
||||
"de", "nl", "fr", "it", "es", "pt", "pt-br", "ru", "uk", "pl", "cs", "sv", "zh",
|
||||
"da", "fi", "nb", "ja", "hi", "hu", "ko", "tr",
|
||||
]);
|
||||
|
||||
/** Served base for the runtime locale files (mirrors LOCALES_URL in const.py). */
|
||||
const LOCALES_BASE = "/maintenance_supporter_locales";
|
||||
|
||||
const _localeInflight = _localeGlobals.inflight;
|
||||
|
||||
/** Normalize an HA language code to our table key.
|
||||
*
|
||||
* Brazilian Portuguese is the one regional variant with its OWN table
|
||||
* ("pt-br") — it must not collapse into European "pt". Mirrors
|
||||
* normalize_language_code in helpers/i18n.py.
|
||||
*/
|
||||
function normLang(lang?: string): string {
|
||||
const l = (lang || DEFAULT_LANG).toLowerCase();
|
||||
if (l.startsWith("pt") && l.endsWith("br")) return "pt-br";
|
||||
return l.substring(0, 2);
|
||||
}
|
||||
|
||||
/** Get a localized string. Falls back to English, then to the key itself. */
|
||||
export function t(key: string, lang?: string): string {
|
||||
const l = normLang(lang);
|
||||
return STORE[l]?.[key] ?? STORE.en[key] ?? key;
|
||||
}
|
||||
|
||||
/** The per-`updated()` locale boot every top-level surface needs: date/time
|
||||
* prefs follow the HA profile (#97), and the user's UI language is lazily
|
||||
* fetched with a re-render once it arrives (EN is bundled, so strings read
|
||||
@@ -121,6 +55,8 @@ export function syncLocaleFromHass(
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => host.requestUpdate());
|
||||
}
|
||||
// Refusals the backend sends with a translation key read in this language.
|
||||
primeBackendErrors(host.hass as Parameters<typeof primeBackendErrors>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,55 +70,6 @@ export function langOf(hass?: { language?: string }): string {
|
||||
return hass?.language || "en";
|
||||
}
|
||||
|
||||
/** True when *lang*'s table is in memory (English is always bundled). */
|
||||
export function isLocaleLoaded(lang?: string): boolean {
|
||||
const l = normLang(lang);
|
||||
return l === DEFAULT_LANG || l in STORE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch *lang*'s table once and cache it. Resolves immediately for English,
|
||||
* an already-loaded language, or an unsupported one (which keeps the English
|
||||
* fallback). Never rejects: a failed fetch silently leaves English in place,
|
||||
* matching t()'s fall-through. Callers re-render on resolution.
|
||||
*/
|
||||
export function ensureLocale(lang?: string): Promise<void> {
|
||||
const l = normLang(lang);
|
||||
if (l === DEFAULT_LANG || l in STORE || !SUPPORTED_LANGS.has(l)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!(l in _localeInflight)) {
|
||||
// Version-busted (#135, same family as #124): the locale files are served
|
||||
// without Cache-Control, so browsers cache them heuristically — after an
|
||||
// update a stale table would miss every new key and silently fall back to
|
||||
// English for them. "dev" builds keep a stable URL.
|
||||
_localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json?v=${BUNDLE_VERSION}`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
STORE[l] = data as Translations;
|
||||
} else {
|
||||
// Don't make one failed fetch sticky for the whole page session —
|
||||
// the next ensureLocale() call retries.
|
||||
delete _localeInflight[l];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
delete _localeInflight[l];
|
||||
});
|
||||
}
|
||||
return _localeInflight[l];
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously seed a locale table, bypassing the fetch. For tests and for
|
||||
* callers that already hold a table (preload/SSR); the loader and t() share the
|
||||
* same STORE, so a seeded language reads immediately.
|
||||
*/
|
||||
export function setLocale(lang: string, table: Translations): void {
|
||||
STORE[normLang(lang)] = table;
|
||||
}
|
||||
|
||||
/** Map language table key to BCP-47 locale for date formatting. */
|
||||
function langToLocale(lang?: string): string {
|
||||
const l = normLang(lang);
|
||||
|
||||
Reference in New Issue
Block a user