From 88abeada44666e9edde84e2b3a021f16dadbb13d Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Tue, 9 Jan 2024 09:06:50 -0500 Subject: [PATCH] Remove Intl polyfill on connection and consistently resolve time zone (#19326) --- src/common/datetime/format_date.ts | 25 ++++++++++--------- src/common/datetime/format_date_time.ts | 9 ++++--- src/common/datetime/format_time.ts | 9 ++++--- src/common/datetime/localize_date.ts | 1 + src/common/datetime/resolve-time-zone.ts | 15 +++++++++++ src/onboarding/onboarding-core-config.ts | 4 +-- .../calendar/dialog-calendar-event-detail.ts | 7 ++++-- .../calendar/dialog-calendar-event-editor.ts | 12 ++++----- .../config/core/ha-config-section-general.ts | 5 +--- src/panels/profile/ha-pick-time-zone-row.ts | 8 +++--- src/panels/todo/dialog-todo-item-editor.ts | 10 ++++---- src/state/connection-mixin.ts | 12 +-------- 12 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 src/common/datetime/resolve-time-zone.ts diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts index 5f94177db0..2369eb46b6 100644 --- a/src/common/datetime/format_date.ts +++ b/src/common/datetime/format_date.ts @@ -1,7 +1,8 @@ import { HassConfig } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; -import { FrontendLocaleData, DateFormat } from "../../data/translation"; +import { DateFormat, FrontendLocaleData } from "../../data/translation"; import "../../resources/intl-polyfill"; +import { resolveTimeZone } from "./resolve-time-zone"; // Tuesday, August 10 export const formatDateWeekdayDay = ( @@ -16,7 +17,7 @@ const formatDateWeekdayDayMem = memoizeOne( weekday: "long", month: "long", day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -33,7 +34,7 @@ const formatDateMem = memoizeOne( year: "numeric", month: "long", day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -50,7 +51,7 @@ const formatDateShortMem = memoizeOne( year: "numeric", month: "short", day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -105,7 +106,7 @@ const formatDateNumericMem = memoizeOne( year: "numeric", month: "numeric", day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }); } @@ -113,7 +114,7 @@ const formatDateNumericMem = memoizeOne( year: "numeric", month: "numeric", day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }); } ); @@ -130,7 +131,7 @@ const formatDateVeryShortMem = memoizeOne( new Intl.DateTimeFormat(locale.language, { day: "numeric", month: "short", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -146,7 +147,7 @@ const formatDateMonthYearMem = memoizeOne( new Intl.DateTimeFormat(locale.language, { month: "long", year: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -161,7 +162,7 @@ const formatDateMonthMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => new Intl.DateTimeFormat(locale.language, { month: "long", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -176,7 +177,7 @@ const formatDateYearMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => new Intl.DateTimeFormat(locale.language, { year: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -191,7 +192,7 @@ const formatDateWeekdayMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => new Intl.DateTimeFormat(locale.language, { weekday: "long", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -206,6 +207,6 @@ const formatDateWeekdayShortMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => new Intl.DateTimeFormat(locale.language, { weekday: "short", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index 52700b4a23..76a32e1acc 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -4,6 +4,7 @@ import { FrontendLocaleData } from "../../data/translation"; import "../../resources/intl-polyfill"; import { formatDateNumeric } from "./format_date"; import { formatTime } from "./format_time"; +import { resolveTimeZone } from "./resolve-time-zone"; import { useAmPm } from "./use_am_pm"; // August 9, 2021, 8:23 AM @@ -22,7 +23,7 @@ const formatDateTimeMem = memoizeOne( hour: useAmPm(locale) ? "numeric" : "2-digit", minute: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -42,7 +43,7 @@ const formatShortDateTimeWithYearMem = memoizeOne( hour: useAmPm(locale) ? "numeric" : "2-digit", minute: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -61,7 +62,7 @@ const formatShortDateTimeMem = memoizeOne( hour: useAmPm(locale) ? "numeric" : "2-digit", minute: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -82,7 +83,7 @@ const formatDateTimeWithSecondsMem = memoizeOne( minute: "2-digit", second: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); diff --git a/src/common/datetime/format_time.ts b/src/common/datetime/format_time.ts index 948eb553fe..ed3e6b603d 100644 --- a/src/common/datetime/format_time.ts +++ b/src/common/datetime/format_time.ts @@ -2,6 +2,7 @@ import { HassConfig } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; import { FrontendLocaleData } from "../../data/translation"; import "../../resources/intl-polyfill"; +import { resolveTimeZone } from "./resolve-time-zone"; import { useAmPm } from "./use_am_pm"; // 9:15 PM || 21:15 @@ -17,7 +18,7 @@ const formatTimeMem = memoizeOne( hour: "numeric", minute: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -35,7 +36,7 @@ const formatTimeWithSecondsMem = memoizeOne( minute: "2-digit", second: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -53,7 +54,7 @@ const formatTimeWeekdayMem = memoizeOne( hour: useAmPm(locale) ? "numeric" : "2-digit", minute: "2-digit", hourCycle: useAmPm(locale) ? "h12" : "h23", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); @@ -71,6 +72,6 @@ const formatTime24hMem = memoizeOne( hour: "numeric", minute: "2-digit", hour12: false, - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), }) ); diff --git a/src/common/datetime/localize_date.ts b/src/common/datetime/localize_date.ts index 428261801d..9fa851d7c2 100644 --- a/src/common/datetime/localize_date.ts +++ b/src/common/datetime/localize_date.ts @@ -1,4 +1,5 @@ import memoizeOne from "memoize-one"; +import "../../resources/intl-polyfill"; export const localizeWeekdays = memoizeOne( (language: string, short: boolean): string[] => { diff --git a/src/common/datetime/resolve-time-zone.ts b/src/common/datetime/resolve-time-zone.ts new file mode 100644 index 0000000000..d085edac6a --- /dev/null +++ b/src/common/datetime/resolve-time-zone.ts @@ -0,0 +1,15 @@ +import { TimeZone } from "../../data/translation"; + +// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support. +// Alternatively, we could fallback to a fixed offset IANA zone (e.g. "Etc/GMT+5") using +// Date.prototype.getTimeOffset(), but IANA only has whole hour Etc zones, and problems +// might occur with relative time due to DST. +// Use optional chain instead of polyfill import since polyfill will always return UTC +export const LOCAL_TIME_ZONE = + Intl.DateTimeFormat?.().resolvedOptions?.().timeZone ?? "UTC"; + +// Pick time zone based on user profile option. Core zone is used when local cannot be determined. +export const resolveTimeZone = (option: TimeZone, serverTimeZone: string) => + option === TimeZone.local && LOCAL_TIME_ZONE !== "UTC" + ? LOCAL_TIME_ZONE + : serverTimeZone; diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index 61cdad8d01..73debff9cb 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -9,6 +9,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeFunc } from "../common/translations/localize"; import "../components/ha-alert"; @@ -33,8 +34,7 @@ class OnboardingCoreConfig extends LitElement { private _elevation = "0"; - private _timeZone: ConfigUpdateValues["time_zone"] = - Intl.DateTimeFormat?.().resolvedOptions?.().timeZone; + private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE; private _language: ConfigUpdateValues["language"] = getLocalLanguage(); diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts index 5962aa83f1..fc07fea31f 100644 --- a/src/panels/calendar/dialog-calendar-event-detail.ts +++ b/src/panels/calendar/dialog-calendar-event-detail.ts @@ -25,6 +25,7 @@ import { renderRRuleAsText } from "./recurrence"; import { showConfirmEventDialog } from "./show-confirm-event-dialog-box"; import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail"; import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor"; +import { resolveTimeZone } from "../../common/datetime/resolve-time-zone"; class DialogCalendarEventDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -138,8 +139,10 @@ class DialogCalendarEventDetail extends LitElement { } private _formatDateRange() { - // Parse a dates in the browser timezone - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const timeZone = resolveTimeZone( + this.hass.locale.time_zone, + this.hass.config.time_zone + ); const start = toDate(this._data!.dtstart, { timeZone: timeZone }); const endValue = toDate(this._data!.dtend, { timeZone: timeZone }); // All day events should be displayed as a day earlier diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts index e1db40da69..377f4ef65f 100644 --- a/src/panels/calendar/dialog-calendar-event-editor.ts +++ b/src/panels/calendar/dialog-calendar-event-editor.ts @@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { resolveTimeZone } from "../../common/datetime/resolve-time-zone"; import { fireEvent } from "../../common/dom/fire_event"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { supportsFeature } from "../../common/entity/supports-feature"; @@ -32,7 +33,6 @@ import { deleteCalendarEvent, updateCalendarEvent, } from "../../data/calendar"; -import { TimeZone } from "../../data/translation"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "../lovelace/components/hui-generic-entity-row"; @@ -68,7 +68,7 @@ class DialogCalendarEventEditor extends LitElement { @state() private _submitting = false; - // Dates are manipulated and displayed in the browser timezone + // Dates are displayed in the timezone according to the user's profile // which may be different from the Home Assistant timezone. When // events are persisted, they are relative to the Home Assistant // timezone, but floating without a timezone. @@ -85,10 +85,10 @@ class DialogCalendarEventEditor extends LitElement { computeStateDomain(stateObj) === "calendar" && supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT) )?.entity_id; - this._timeZone = - this.hass.locale.time_zone === TimeZone.local - ? Intl.DateTimeFormat().resolvedOptions().timeZone - : this.hass.config.time_zone; + this._timeZone = resolveTimeZone( + this.hass.locale.time_zone, + this.hass.config.time_zone + ); if (params.entry) { const entry = params.entry!; this._allDay = isDate(entry.dtstart); diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 22df34f25c..eb6681a26a 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -294,10 +294,7 @@ class HaConfigSectionGeneral extends LitElement { this._country = this.hass.config.country; this._language = this.hass.config.language; this._elevation = this.hass.config.elevation; - this._timeZone = - this.hass.config.time_zone || - Intl.DateTimeFormat?.().resolvedOptions?.().timeZone || - "Etc/GMT"; + this._timeZone = this.hass.config.time_zone || "Etc/GMT"; this._name = this.hass.config.location_name; this._updateUnits = true; } diff --git a/src/panels/profile/ha-pick-time-zone-row.ts b/src/panels/profile/ha-pick-time-zone-row.ts index 06ab7ffa1e..ef732bec1a 100644 --- a/src/panels/profile/ha-pick-time-zone-row.ts +++ b/src/panels/profile/ha-pick-time-zone-row.ts @@ -2,6 +2,7 @@ import "@material/mwc-list/mwc-list-item"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTimeNumeric } from "../../common/datetime/format_date_time"; +import { resolveTimeZone } from "../../common/datetime/resolve-time-zone"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; import "../../components/ha-select"; @@ -48,10 +49,9 @@ class TimeZoneRow extends LitElement { >${this.hass.localize( `ui.panel.profile.time_zone.options.${format}`, { - timezone: (format === "server" - ? this.hass.config.time_zone - : Intl.DateTimeFormat?.().resolvedOptions?.().timeZone || - "" + timezone: resolveTimeZone( + format, + this.hass.config.time_zone ).replace("_", " "), } )}>( } this._updateHass({ areas }); }); - subscribeConfig(conn, (config) => { - if (this.hass?.config?.time_zone !== config.time_zone) { - import("../resources/intl-polyfill").then(() => { - if ("__setDefaultTimeZone" in Intl.DateTimeFormat) { - // @ts-ignore - Intl.DateTimeFormat.__setDefaultTimeZone(config.time_zone); - } - }); - } - this._updateHass({ config }); - }); + subscribeConfig(conn, (config) => this._updateHass({ config })); subscribeServices(conn, (services) => this._updateHass({ services })); subscribePanels(conn, (panels) => this._updateHass({ panels })); subscribeFrontendUserData(conn, "core", (userData) =>