456 files

This commit is contained in:
Home Assistant Version Control
2026-10-01 05:38:21 +00:00
parent 4192ecbaae
commit 5d6d845ebf
456 changed files with 64951 additions and 3828 deletions
@@ -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);