diff --git a/gallery/sidebar.js b/gallery/sidebar.js index 767aecf64c..6c6d12ed97 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -45,6 +45,10 @@ export default [ header: "Users", pages: ["user-types", "configuration-menu"], }, + { + category: "date-time", + header: "Date and Time", + }, { category: "design.home-assistant.io", header: "About", diff --git a/gallery/src/pages/date-time/date.markdown b/gallery/src/pages/date-time/date.markdown new file mode 100644 index 0000000000..5eb64bb19e --- /dev/null +++ b/gallery/src/pages/date-time/date.markdown @@ -0,0 +1,7 @@ +--- +title: (Numeric) Date Formatting +--- + +This pages lists all supported languages with their available (numeric) date formats. + +Formatting function: `const formatDateNumeric: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date.ts b/gallery/src/pages/date-time/date.ts new file mode 100644 index 0000000000..c344f6f7cc --- /dev/null +++ b/gallery/src/pages/date-time/date.ts @@ -0,0 +1,106 @@ +import { html, css, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { HomeAssistant } from "../../../../src/types"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateNumeric } from "../../../../src/common/datetime/format_date"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date") +export class DemoDateTimeDate extends LitElement { + @property({ attribute: false }) hass!: HomeAssistant; + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + }; + const date = new Date(); + return html` + +
+
Language
+
Default (lang)
+
Day-Month-Year
+
Month-Day-Year
+
Year-Month-Day
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateNumeric(date, { + ...defaultLocale, + language: key, + date_format: DateFormat.language, + })} +
+
+ ${formatDateNumeric(date, { + ...defaultLocale, + language: key, + date_format: DateFormat.DMY, + })} +
+
+ ${formatDateNumeric(date, { + ...defaultLocale, + language: key, + date_format: DateFormat.MDY, + })} +
+
+ ${formatDateNumeric(date, { + ...defaultLocale, + language: key, + date_format: DateFormat.YMD, + })} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 600px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date": DemoDateTimeDate; + } +} diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts index 36c022ab0c..761d53703d 100644 --- a/src/common/datetime/format_date.ts +++ b/src/common/datetime/format_date.ts @@ -1,5 +1,5 @@ import memoizeOne from "memoize-one"; -import { FrontendLocaleData } from "../../data/translation"; +import { FrontendLocaleData, DateFormat } from "../../data/translation"; import "../../resources/intl-polyfill"; // Tuesday, August 10 @@ -32,15 +32,50 @@ const formatDateMem = memoizeOne( // 10/08/2021 export const formatDateNumeric = (dateObj: Date, locale: FrontendLocaleData) => - formatDateNumericMem(locale).format(dateObj); + formatDateNumericMem(locale, dateObj); const formatDateNumericMem = memoizeOne( - (locale: FrontendLocaleData) => - new Intl.DateTimeFormat(locale.language, { + (locale: FrontendLocaleData, dateObj: Date) => { + const localeString = + locale.date_format === DateFormat.system ? undefined : locale.language; + + if ( + locale.date_format === DateFormat.language || + locale.date_format === DateFormat.system + ) { + return new Intl.DateTimeFormat(localeString, { + year: "numeric", + month: "numeric", + day: "numeric", + }).format(dateObj); + } + + const parts = new Intl.DateTimeFormat(localeString, { year: "numeric", month: "numeric", day: "numeric", - }) + }).formatToParts(dateObj); + + const literal = parts.find((value) => value.type === "literal")?.value; + const day = parts.find((value) => value.type === "day")?.value; + const month = parts.find((value) => value.type === "month")?.value; + const year = parts.find((value) => value.type === "year")?.value; + + const lastPart = parts.at(parts.length - 1); + let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : ""; + + if (localeString === "bg" && locale.date_format === DateFormat.YMD) { + lastLiteral = ""; + } + + const formats = { + [DateFormat.DMY]: `${day}${literal}${month}${literal}${year}${lastLiteral}`, + [DateFormat.MDY]: `${month}${literal}${day}${literal}${year}${lastLiteral}`, + [DateFormat.YMD]: `${year}${literal}${month}${literal}${day}${lastLiteral}`, + }; + + return formats[locale.date_format]; + } ); // Aug 10 diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index ba5b73e2e9..7c38d1fd18 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -2,6 +2,8 @@ import memoizeOne from "memoize-one"; import { FrontendLocaleData } from "../../data/translation"; import "../../resources/intl-polyfill"; import { useAmPm } from "./use_am_pm"; +import { formatDateNumeric } from "./format_date"; +import { formatTime } from "./format_time"; // August 9, 2021, 8:23 AM export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) => @@ -97,21 +99,4 @@ const formatDateTimeWithSecondsMem = memoizeOne( export const formatDateTimeNumeric = ( dateObj: Date, locale: FrontendLocaleData -) => formatDateTimeNumericMem(locale).format(dateObj); - -const formatDateTimeNumericMem = memoizeOne( - (locale: FrontendLocaleData) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "2-digit", - hour12: useAmPm(locale), - } - ) -); +) => `${formatDateNumeric(dateObj, locale)}, ${formatTime(dateObj, locale)}`; diff --git a/src/data/translation.ts b/src/data/translation.ts index 382c5a804d..fb6fdaeeb9 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -17,6 +17,14 @@ export enum TimeFormat { twenty_four = "24", } +export enum DateFormat { + language = "language", + system = "system", + DMY = "DMY", + MDY = "MDY", + YMD = "YMD", +} + export enum FirstWeekday { language = "language", monday = "monday", @@ -32,6 +40,7 @@ export interface FrontendLocaleData { language: string; number_format: NumberFormat; time_format: TimeFormat; + date_format: DateFormat; first_weekday: FirstWeekday; } diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 5263dadcaf..6116fbc015 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -6,7 +6,12 @@ import { import { fireEvent } from "../common/dom/fire_event"; import { computeLocalize } from "../common/translations/localize"; import { DEFAULT_PANEL } from "../data/panel"; -import { FirstWeekday, NumberFormat, TimeFormat } from "../data/translation"; +import { + FirstWeekday, + NumberFormat, + DateFormat, + TimeFormat, +} from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; import { HomeAssistant } from "../types"; import { getLocalLanguage, getTranslation } from "../util/common-translation"; @@ -224,6 +229,7 @@ export const provideHass = ( language: localLanguage, number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }, resources: null as any, diff --git a/src/panels/profile/ha-panel-profile.ts b/src/panels/profile/ha-panel-profile.ts index f5465fd676..628c44ef38 100644 --- a/src/panels/profile/ha-panel-profile.ts +++ b/src/panels/profile/ha-panel-profile.ts @@ -27,6 +27,7 @@ import "./ha-pick-language-row"; import "./ha-pick-number-format-row"; import "./ha-pick-theme-row"; import "./ha-pick-time-format-row"; +import "./ha-pick-date-format-row"; import "./ha-push-notifications-row"; import "./ha-refresh-tokens-card"; import "./ha-set-suspend-row"; @@ -96,6 +97,10 @@ class HaPanelProfile extends LitElement { .narrow=${this.narrow} .hass=${this.hass} > + + + ${this.hass.localize("ui.panel.profile.date_format.header")} + + + ${this.hass.localize("ui.panel.profile.date_format.description")} + + + ${Object.values(DateFormat).map((format) => { + const formattedDate = formatDateNumeric(date, { + ...this.hass.locale, + date_format: format, + }); + const value = this.hass.localize( + `ui.panel.profile.date_format.formats.${format}` + ); + return html` + ${value} + ${formattedDate} + `; + })} + + + `; + } + + private async _handleFormatSelection(ev) { + fireEvent(this, "hass-date-format-select", ev.target.value); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-pick-date-format-row": DateFormatRow; + } +} diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index 2e1a7927fe..b02e26d837 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -18,7 +18,12 @@ import { subscribeFrontendUserData } from "../data/frontend"; import { forwardHaptic } from "../data/haptics"; import { DEFAULT_PANEL } from "../data/panel"; import { serviceCallWillDisconnect } from "../data/service"; -import { FirstWeekday, NumberFormat, TimeFormat } from "../data/translation"; +import { + FirstWeekday, + NumberFormat, + DateFormat, + TimeFormat, +} from "../data/translation"; import { subscribePanels } from "../data/ws-panels"; import { translationMetadata } from "../resources/translations-metadata"; import { Constructor, HomeAssistant, ServiceCallResponse } from "../types"; @@ -57,6 +62,7 @@ export const connectionMixin = >( language, number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }, resources: null as any, diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index bb8ac9560b..a926329e04 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -13,6 +13,7 @@ import { NumberFormat, saveTranslationPreferences, TimeFormat, + DateFormat, TranslationCategory, } from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; @@ -37,6 +38,9 @@ declare global { "hass-time-format-select": { time_format: TimeFormat; }; + "hass-date-format-select": { + date_format: DateFormat; + }; "hass-first-weekday-select": { first_weekday: FirstWeekday; }; @@ -82,6 +86,9 @@ export default >(superClass: T) => this.addEventListener("hass-time-format-select", (e) => { this._selectTimeFormat((e as CustomEvent).detail, true); }); + this.addEventListener("hass-date-format-select", (e) => { + this._selectDateFormat((e as CustomEvent).detail, true); + }); this.addEventListener("hass-first-weekday-select", (e) => { this._selectFirstWeekday((e as CustomEvent).detail, true); }); @@ -123,6 +130,13 @@ export default >(superClass: T) => // We just got time_format from backend, no need to save back this._selectTimeFormat(locale.time_format, false); } + if ( + locale?.date_format && + this.hass!.locale.date_format !== locale.date_format + ) { + // We just got date_format from backend, no need to save back + this._selectDateFormat(locale.date_format, false); + } if ( locale?.first_weekday && this.hass!.locale.first_weekday !== locale.first_weekday @@ -177,6 +191,18 @@ export default >(superClass: T) => } } + private _selectDateFormat(date_format: DateFormat, saveToBackend: boolean) { + this._updateHass({ + locale: { + ...this.hass!.locale, + date_format: date_format, + }, + }); + if (saveToBackend) { + saveTranslationPreferences(this.hass!, this.hass!.locale); + } + } + private _selectFirstWeekday( first_weekday: FirstWeekday, saveToBackend: boolean diff --git a/src/translations/en.json b/src/translations/en.json index 769cb458e9..b07a7addb0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4905,6 +4905,18 @@ "24": "24 hours" } }, + "date_format": { + "header": "Date Format", + "dropdown_label": "Date format", + "description": "Choose how dates are formatted.", + "formats": { + "language": "Auto (use language setting)", + "system": "Use system locale", + "DMY": "Day-Month-Year", + "MDY": "Month-Day-Year", + "YMD": "Year-Month-Day" + } + }, "first_weekday": { "header": "First day of the week", "dropdown_label": "First day of the week", diff --git a/src/util/common-translation.ts b/src/util/common-translation.ts index 9f186314c9..be14097dd8 100644 --- a/src/util/common-translation.ts +++ b/src/util/common-translation.ts @@ -76,6 +76,7 @@ export async function getUserLocale( const language = result?.language; const number_format = result?.number_format; const time_format = result?.time_format; + const date_format = result?.date_format; const first_weekday = result?.first_weekday; if (language) { const availableLanguage = findAvailableLanguage(language); @@ -84,6 +85,7 @@ export async function getUserLocale( language: availableLanguage, number_format, time_format, + date_format: date_format, first_weekday, }; } @@ -91,6 +93,7 @@ export async function getUserLocale( return { number_format, time_format, + date_format: date_format, first_weekday, }; } diff --git a/test/common/datetime/format_date.ts b/test/common/datetime/format_date.ts index de2293719e..9d6841ea7d 100644 --- a/test/common/datetime/format_date.ts +++ b/test/common/datetime/format_date.ts @@ -5,6 +5,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; describe("formatDate", () => { @@ -16,6 +17,7 @@ describe("formatDate", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "November 18, 2017" diff --git a/test/common/datetime/format_date_time.ts b/test/common/datetime/format_date_time.ts index a7692cbe97..5632d60615 100644 --- a/test/common/datetime/format_date_time.ts +++ b/test/common/datetime/format_date_time.ts @@ -8,6 +8,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; describe("formatDateTime", () => { @@ -19,6 +20,7 @@ describe("formatDateTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "November 18, 2017 at 11:12 PM" @@ -28,6 +30,7 @@ describe("formatDateTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.twenty_four, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "November 18, 2017 at 23:12" @@ -44,6 +47,7 @@ describe("formatDateTimeWithSeconds", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "November 18, 2017 at 11:12:13 PM" @@ -53,6 +57,7 @@ describe("formatDateTimeWithSeconds", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.twenty_four, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "November 18, 2017 at 23:12:13" diff --git a/test/common/datetime/format_time.ts b/test/common/datetime/format_time.ts index 17b2a932d4..3c7e7899c7 100644 --- a/test/common/datetime/format_time.ts +++ b/test/common/datetime/format_time.ts @@ -9,6 +9,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; describe("formatTime", () => { @@ -20,6 +21,7 @@ describe("formatTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "11:12 PM" @@ -29,6 +31,7 @@ describe("formatTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.twenty_four, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "23:12" @@ -45,6 +48,7 @@ describe("formatTimeWithSeconds", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "11:12:13 PM" @@ -54,6 +58,7 @@ describe("formatTimeWithSeconds", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.twenty_four, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "23:12:13" @@ -70,6 +75,7 @@ describe("formatTimeWeekday", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "Saturday 11:12 PM" @@ -79,6 +85,7 @@ describe("formatTimeWeekday", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.twenty_four, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }), "Saturday 23:12" diff --git a/test/common/datetime/relative_time.ts b/test/common/datetime/relative_time.ts index c7b4f87fc6..ebdf39185b 100644 --- a/test/common/datetime/relative_time.ts +++ b/test/common/datetime/relative_time.ts @@ -5,6 +5,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; describe("relativeTime", () => { @@ -12,6 +13,7 @@ describe("relativeTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }; @@ -19,6 +21,7 @@ describe("relativeTime", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.monday, }; diff --git a/test/common/entity/compute_state_display.ts b/test/common/entity/compute_state_display.ts index ff3a30a599..f09df55c5d 100644 --- a/test/common/entity/compute_state_display.ts +++ b/test/common/entity/compute_state_display.ts @@ -6,6 +6,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; let localeData: FrontendLocaleData; @@ -20,6 +21,7 @@ describe("computeStateDisplay", () => { language: "en", number_format: NumberFormat.comma_decimal, time_format: TimeFormat.am_pm, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, }; }); diff --git a/test/common/string/format_number.ts b/test/common/string/format_number.ts index 999928e38a..986ab9a01b 100644 --- a/test/common/string/format_number.ts +++ b/test/common/string/format_number.ts @@ -11,6 +11,7 @@ import { NumberFormat, TimeFormat, FirstWeekday, + DateFormat, } from "../../../src/data/translation"; describe("formatNumber", () => { @@ -19,6 +20,7 @@ describe("formatNumber", () => { language: "en", number_format: NumberFormat.language, time_format: TimeFormat.language, + date_format: DateFormat.language, first_weekday: FirstWeekday.language, };