Add setting to localize numeric dates independent from language (#15770) (#16489)

This commit is contained in:
Christoph Wen, B.Sc 2023-05-30 21:39:27 +02:00 committed by GitHub
parent 7e5a85dbe7
commit 2ad6253b72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 314 additions and 25 deletions

View File

@ -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",

View File

@ -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`

View File

@ -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`
<mwc-list>
<div class="container header">
<div>Language</div>
<div class="center">Default (lang)</div>
<div class="center">Day-Month-Year</div>
<div class="center">Month-Day-Year</div>
<div class="center">Year-Month-Day</div>
</div>
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.language,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
})}
</div>
<div class="center">
${formatDateNumeric(date, {
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
})}
</div>
</div>
`
)}
</mwc-list>
`;
}
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;
}
}

View File

@ -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

View File

@ -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)}`;

View File

@ -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;
}

View File

@ -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,

View File

@ -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}
></ha-pick-time-format-row>
<ha-pick-date-format-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-date-format-row>
<ha-pick-first-weekday-row
.narrow=${this.narrow}
.hass=${this.hass}

View File

@ -0,0 +1,64 @@
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../../common/datetime/format_date";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "../../components/ha-select";
import "../../components/ha-settings-row";
import { DateFormat } from "../../data/translation";
import { HomeAssistant } from "../../types";
@customElement("ha-pick-date-format-row")
class DateFormatRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
protected render(): TemplateResult {
const date = new Date();
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize("ui.panel.profile.date_format.header")}
</span>
<span slot="description">
${this.hass.localize("ui.panel.profile.date_format.description")}
</span>
<ha-select
.label=${this.hass.localize(
"ui.panel.profile.date_format.dropdown_label"
)}
.disabled=${this.hass.locale === undefined}
.value=${this.hass.locale.date_format}
@selected=${this._handleFormatSelection}
naturalMenuWidth
>
${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`<mwc-list-item .value=${format} twoline>
<span>${value}</span>
<span slot="secondary">${formattedDate}</span>
</mwc-list-item>`;
})}
</ha-select>
</ha-settings-row>
`;
}
private async _handleFormatSelection(ev) {
fireEvent(this, "hass-date-format-select", ev.target.value);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-pick-date-format-row": DateFormatRow;
}
}

View File

@ -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 = <T extends Constructor<HassBaseEl>>(
language,
number_format: NumberFormat.language,
time_format: TimeFormat.language,
date_format: DateFormat.language,
first_weekday: FirstWeekday.language,
},
resources: null as any,

View File

@ -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 <T extends Constructor<HassBaseEl>>(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 <T extends Constructor<HassBaseEl>>(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 <T extends Constructor<HassBaseEl>>(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

View File

@ -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",

View File

@ -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,
};
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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,
};

View File

@ -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,
};
});

View File

@ -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,
};