187 files
This commit is contained in:
1 parent
16c9b4d7f4
commit
d6413e19b5
187 files changed
+15542
-2765
No files matched your search
@@ -106,7 +106,7 @@ export function syncLocaleFromHass(
|
||||
changedProps: Map<string, unknown>,
|
||||
): void {
|
||||
if (changedProps.has("hass")) {
|
||||
setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0], host.hass?.config?.country);
|
||||
setProfilePrefs(host.hass?.locale as Parameters<typeof setProfilePrefs>[0], host.hass?.config?.country);
|
||||
}
|
||||
const lang = host.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
@@ -188,38 +188,88 @@ function langToLocale(lang?: string): string {
|
||||
return map[l] ?? "en-US";
|
||||
}
|
||||
|
||||
/** HA per-user date/time format prefs (issue #97).
|
||||
/** HA per-user format prefs (issue #97, #163).
|
||||
*
|
||||
* hass.locale.date_format ("language"|"system"|"DMY"|"MDY"|"YMD") and
|
||||
* time_format ("language"|"system"|"12"|"24") are frontend PROFILE settings —
|
||||
* deriving the format from the UI language alone showed en users mm/dd/yyyy
|
||||
* even with dd/mm/yyyy configured. Stored on a window singleton because each
|
||||
* bundle (panel / cards / dialog-mount) gets its own module scope; every
|
||||
* surface calls setDateTimePrefs() on hass updates and whichever runs first
|
||||
* wins for all of them.
|
||||
* hass.locale.date_format ("language"|"system"|"DMY"|"MDY"|"YMD"),
|
||||
* time_format ("language"|"system"|"12"|"24") and number_format ("language"|
|
||||
* "system"|"comma_decimal"|"decimal_comma"|"space_comma"|"none") are frontend
|
||||
* PROFILE settings — deriving the format from the UI language alone showed
|
||||
* en users mm/dd/yyyy even with dd/mm/yyyy configured. Stored on a window
|
||||
* singleton because each bundle (panel / cards / dialog-mount) gets its own
|
||||
* module scope; every surface calls setProfilePrefs() on hass updates and
|
||||
* whichever runs first wins for all of them. (The window key keeps its
|
||||
* original name so a stale cached bundle still shares the object.)
|
||||
*/
|
||||
interface DateTimePrefs {
|
||||
interface ProfilePrefs {
|
||||
date?: string;
|
||||
time?: string;
|
||||
number?: string;
|
||||
country?: string;
|
||||
}
|
||||
const _w = window as unknown as { __msDateTimePrefs?: DateTimePrefs };
|
||||
const DT_PREFS: DateTimePrefs = (_w.__msDateTimePrefs ??= {});
|
||||
const _w = window as unknown as { __msDateTimePrefs?: ProfilePrefs };
|
||||
const DT_PREFS: ProfilePrefs = (_w.__msDateTimePrefs ??= {});
|
||||
|
||||
/** Feed HA's per-user date/time format into formatDate/formatDateTime.
|
||||
/** Feed HA's per-user date/time/number formats into the formatters below.
|
||||
* `country` is the SERVER's configured country (#140): with the profile
|
||||
* format left on "language" it regionalizes the language default —
|
||||
* date format left on "language" it regionalizes the language default —
|
||||
* "en" + AU renders DD/MM/YYYY instead of the hard en-US mapping. */
|
||||
export function setDateTimePrefs(
|
||||
locale?: { date_format?: string; time_format?: string },
|
||||
export function setProfilePrefs(
|
||||
locale?: { date_format?: string; time_format?: string; number_format?: string },
|
||||
country?: string | null,
|
||||
): void {
|
||||
if (!locale) return;
|
||||
DT_PREFS.date = locale.date_format;
|
||||
DT_PREFS.time = locale.time_format;
|
||||
DT_PREFS.number = locale.number_format;
|
||||
if (country !== undefined) DT_PREFS.country = country || undefined;
|
||||
}
|
||||
|
||||
/** Locale for the profile number format — mirrors HA's own
|
||||
* numberFormatToLocale so our figures match the entity cards next to them:
|
||||
* the three explicit styles pin a representative locale, "system" is the
|
||||
* browser, and the "language" default is the UI language WITHOUT the
|
||||
* server-country regionalization dates get (#140) — HA formats "de" as
|
||||
* 1.234,56 whatever the country. */
|
||||
function numberLocale(lang?: string): string | string[] | undefined {
|
||||
switch (DT_PREFS.number) {
|
||||
case "comma_decimal": return ["en-US", "en"];
|
||||
case "decimal_comma": return ["de", "es", "it"];
|
||||
case "space_comma": return ["fr", "sv", "cs"];
|
||||
case "system": return undefined;
|
||||
default: return langToLocale(lang);
|
||||
}
|
||||
}
|
||||
|
||||
/** Format a number for display honouring the HA profile number format.
|
||||
* `digits` pins the fraction digits (a number = exactly that many, like
|
||||
* toFixed) or passes raw Intl options; the default is at most 2, no trailing
|
||||
* zeros. "none" mirrors HA: plain digits, no grouping, trailing zeros
|
||||
* dropped. Every human-facing number in the UI goes through here — SVG
|
||||
* coordinates use chart-utils px() instead, which must stay locale-free. */
|
||||
export function formatNumber(n: number, lang?: string, digits?: number | Intl.NumberFormatOptions): string {
|
||||
if (!Number.isFinite(n)) return String(n);
|
||||
const opts: Intl.NumberFormatOptions =
|
||||
typeof digits === "number"
|
||||
? { minimumFractionDigits: digits, maximumFractionDigits: digits }
|
||||
: { maximumFractionDigits: 2, ...digits };
|
||||
if (DT_PREFS.number === "none") {
|
||||
const f = Math.pow(10, opts.maximumFractionDigits ?? 2);
|
||||
return String(Math.round(n * f) / f);
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat(numberLocale(lang), opts).format(n);
|
||||
} catch {
|
||||
return new Intl.NumberFormat(undefined, opts).format(n);
|
||||
}
|
||||
}
|
||||
|
||||
/** Amount + currency symbol ("12.50 €"; the symbol is optional) — every cost
|
||||
* figure in the UI and the printables goes through here. */
|
||||
export function formatCost(amount: number, symbol?: string, lang?: string, digits = 2): string {
|
||||
const num = formatNumber(amount, lang, digits);
|
||||
return symbol ? `${num} ${symbol}` : num;
|
||||
}
|
||||
|
||||
/** BCP-47 locale for the "language" default: the UI language regionalized
|
||||
* by the server country when Intl knows the combination (#140). Explicit
|
||||
* profile formats (DMY/MDY/YMD/system) never reach this path. */
|
||||
@@ -253,7 +303,10 @@ function _formatDateObj(d: Date, lang?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function _formatTimeObj(d: Date, lang?: string): string {
|
||||
/** Time of day honouring the HA profile time format (12/24h). Every
|
||||
* clock-time string in the UI goes through here — chart crosshairs included
|
||||
* (#163: a bare toLocaleDateString({hour, minute}) ignored the profile). */
|
||||
export function formatTimeOfDay(d: Date, lang?: string): string {
|
||||
switch (DT_PREFS.time) {
|
||||
case "12": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
|
||||
case "24": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
@@ -279,7 +332,7 @@ export function formatDateTime(iso: string | null | undefined, lang?: string): s
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return _formatDateObj(d, lang) + " " + _formatTimeObj(d, lang);
|
||||
return _formatDateObj(d, lang) + " " + formatTimeOfDay(d, lang);
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
@@ -306,9 +359,37 @@ export function formatInterval(
|
||||
}
|
||||
|
||||
/** Localized weekday name (0=Mon … 6=Sun) via Intl — 2024-01-01 is a Monday.
|
||||
* Single source for the dialog selectors AND formatRecurrence (DRY). */
|
||||
* Single source for the dialog selectors AND formatRecurrence (DRY).
|
||||
*
|
||||
* Name-only helpers (weekday / month / "Jul 3") follow the UI language like
|
||||
* HA's own formatDateShort does — the profile date_format only orders
|
||||
* NUMERIC dates. They still live here, not in the consumers: styles.ts is
|
||||
* the only module allowed to call Intl for dates and numbers (see
|
||||
* __tests__/profile-format-single-source.test.ts, #163). */
|
||||
export function weekdayName(i: number, lang?: string, style: "long" | "short" = "long"): string {
|
||||
return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(langToLocale(lang), { weekday: style, timeZone: "UTC" });
|
||||
return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(resolveLocale(lang), { weekday: style, timeZone: "UTC" });
|
||||
}
|
||||
|
||||
/** Localized month name (0=Jan … 11=Dec). */
|
||||
export function monthName(i: number, lang?: string, style: "long" | "short" = "long"): string {
|
||||
return new Date(Date.UTC(2024, i, 1)).toLocaleDateString(resolveLocale(lang), { month: style, timeZone: "UTC" });
|
||||
}
|
||||
|
||||
/** Weekday / month name of a local Date (calendar day rows). */
|
||||
export function formatWeekday(d: Date, lang?: string, style: "long" | "short" = "long"): string {
|
||||
return weekdayName((d.getDay() + 6) % 7, lang, style);
|
||||
}
|
||||
export function formatMonth(d: Date, lang?: string, style: "long" | "short" = "long"): string {
|
||||
return monthName(d.getMonth(), lang, style);
|
||||
}
|
||||
|
||||
/** Short day+month ("Jul 3" / "3. Juli"), with a 2-digit year when the
|
||||
* caller's range needs disambiguating — chart axis ticks and tooltips. */
|
||||
export function formatDateShort(d: Date, lang?: string, withYear = false): string {
|
||||
return d.toLocaleDateString(
|
||||
resolveLocale(lang),
|
||||
withYear ? { month: "short", day: "numeric", year: "2-digit" } : { month: "short", day: "numeric" },
|
||||
);
|
||||
}
|
||||
|
||||
/** Recurrence shape carried on the WS payload (see types.TaskSchedule). */
|
||||
|
||||
Reference in new issue
Block a user