Allow users to select time format for UI rendering (#9042)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Philip Allgaier 2021-05-20 16:23:53 +02:00 committed by GitHub
parent 87e4c209f4
commit 70a1edd1dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 397 additions and 175 deletions

View File

@ -1,21 +1,32 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
const formatDateMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
})
);
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "longDate");
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
month: "short",
month: "long",
day: "numeric",
})
);
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "dddd, MMM D");

View File

@ -1,26 +1,42 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
const formatDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
);
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
})
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
);
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeWithSecondsMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");

View File

@ -1,29 +1,52 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
import { useAmPm } from "./use_am_pm";
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
: (dateObj: Date) => format(dateObj, "shortTime");
);
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
const formatTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
})
: (dateObj: Date) => format(dateObj, "mediumTime");
);
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWithSecondsMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
: (dateObj: Date) => format(dateObj, "dddd, HH:mm");
);
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWeekdayMem(locale).format(dateObj)
: (dateObj: Date, locale: FrontendLocaleData) =>
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");

View File

@ -0,0 +1,15 @@
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
export const useAmPm = (locale: FrontendLocaleData): boolean => {
if (
locale.time_format === TimeFormat.language ||
locale.time_format === TimeFormat.system
) {
const testLanguage =
locale.time_format === TimeFormat.language ? locale.language : undefined;
const test = new Date().toLocaleString(testLanguage);
return test.includes("AM") || test.includes("PM");
}
return locale.time_format === TimeFormat.am_pm;
};

View File

@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendTranslationData } from "../../data/translation";
import { FrontendLocaleData } from "../../data/translation";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
@ -11,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendTranslationData,
locale: FrontendLocaleData,
state?: string
): string => {
const compareState = state !== undefined ? state : stateObj.state;

View File

@ -1,4 +1,4 @@
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
/**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
@ -9,7 +9,7 @@ import { FrontendTranslationData, NumberFormat } from "../../data/translation";
*/
export const formatNumber = (
num: string | number,
locale?: FrontendTranslationData,
locale?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): string => {
let format: string | string[] | undefined;

View File

@ -14,6 +14,7 @@ import {
} from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import "./date-range-picker";
@ -43,7 +44,7 @@ export class HaDateRangePicker extends LitElement {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
this._hour24format = this._compute24hourFormat();
this._hour24format = !useAmPm(this.hass.locale);
this._rtlDirection = computeRTLDirection(this.hass);
}
}
@ -106,16 +107,6 @@ export class HaDateRangePicker extends LitElement {
`;
}
private _compute24hourFormat() {
return (
new Intl.DateTimeFormat(this.hass.language, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")!.value.length === 2
);
}
private _setDateRange(ev: CustomEvent<ActionDetail>) {
const dateRange = Object.values(this.ranges!)[ev.detail.index];
const dateRangePicker = this._dateRangePicker;

View File

@ -4,7 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { FrontendLocaleData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => {
@ -23,7 +23,7 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0;
@property() public locale!: FrontendTranslationData;
@property() public locale!: FrontendLocaleData;
@property() public label = "";

View File

@ -1,10 +1,13 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import { TimeSelector } from "../../data/selector";
import { FrontendLocaleData } from "../../data/translation";
import { HomeAssistant } from "../../types";
import "../paper-time-input";
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@ -17,13 +20,12 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _useAmPm = memoizeOne((language: string) => {
const test = new Date().toLocaleString(language);
return test.includes("AM") || test.includes("PM");
});
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean =>
useAmPm(locale)
);
protected render() {
const useAMPM = this._useAmPm(this.hass.locale.language);
const useAMPM = this._useAmPmMem(this.hass.locale);
const parts = this.value?.split(":") || [];
const hours = parts[0];
@ -48,7 +50,7 @@ export class HaTimeSelector extends LitElement {
private _timeChanged(ev) {
let value = ev.target.value;
const useAMPM = this._useAmPm(this.hass.locale.language);
const useAMPM = this._useAmPmMem(this.hass.locale);
let hours = Number(ev.target.hour || 0);
if (value && useAMPM) {
if (ev.target.amPm === "PM") {

View File

@ -4,7 +4,7 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { FrontendTranslationData } from "./translation";
import { FrontendLocaleData } from "./translation";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const LINE_ATTRIBUTES_TO_KEEP = [
@ -109,7 +109,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = (
localize: LocalizeFunc,
language: FrontendTranslationData,
language: FrontendLocaleData,
states: HassEntity[]
): TimelineEntity => {
const data: TimelineState[] = [];

View File

@ -10,14 +10,22 @@ export enum NumberFormat {
none = "none",
}
export interface FrontendTranslationData {
export enum TimeFormat {
language = "language",
system = "system",
am_pm = "12",
twenty_four = "24",
}
export interface FrontendLocaleData {
language: string;
number_format: NumberFormat;
time_format: TimeFormat;
}
declare global {
interface FrontendUserData {
language: FrontendTranslationData;
language: FrontendLocaleData;
}
}
@ -36,7 +44,7 @@ export const fetchTranslationPreferences = (hass: HomeAssistant) =>
export const saveTranslationPreferences = (
hass: HomeAssistant,
data: FrontendTranslationData
data: FrontendLocaleData
) => saveFrontendUserData(hass.connection, "language", data);
export const getHassTranslations = async (

View File

@ -5,7 +5,7 @@ import {
} from "../common/dom/apply_themes_on_element";
import { computeLocalize } from "../common/translations/localize";
import { DEFAULT_PANEL } from "../data/panel";
import { NumberFormat } from "../data/translation";
import { NumberFormat, TimeFormat } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
@ -215,6 +215,7 @@ export const provideHass = (
locale: {
language: localLanguage,
number_format: NumberFormat.language,
time_format: TimeFormat.language,
},
resources: null as any,
localize: () => "",

View File

@ -1,8 +1,8 @@
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import { formatTimeWithSeconds } from "../../../common/datetime/format_time";
import { FrontendTranslationData } from "../../../data/translation";
import { FrontendLocaleData } from "../../../data/translation";
export const formatSystemLogTime = (date, locale: FrontendTranslationData) => {
export const formatSystemLogTime = (date, locale: FrontendLocaleData) => {
const today = new Date().setHours(0, 0, 0, 0);
const dateTime = new Date(date * 1000);
const dateTimeDay = new Date(date * 1000).setHours(0, 0, 0, 0);

View File

@ -4,12 +4,12 @@ import { formatDate } from "../../../common/datetime/format_date";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { formatTime } from "../../../common/datetime/format_time";
import relativeTime from "../../../common/datetime/relative_time";
import { FrontendTranslationData } from "../../../data/translation";
import { FrontendLocaleData } from "../../../data/translation";
import { HomeAssistant } from "../../../types";
import { TimestampRenderingFormats } from "./types";
const FORMATS: {
[key: string]: (ts: Date, lang: FrontendTranslationData) => string;
[key: string]: (ts: Date, lang: FrontendLocaleData) => string;
} = {
date: formatDate,
datetime: formatDateTime,

View File

@ -3,7 +3,7 @@ import {
LovelaceCardConfig,
LovelaceConfig,
} from "../../data/lovelace";
import { FrontendTranslationData } from "../../data/translation";
import { FrontendLocaleData } from "../../data/translation";
import { Constructor, HomeAssistant } from "../../types";
import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
@ -23,7 +23,7 @@ export interface Lovelace {
editMode: boolean;
urlPath: string | null;
mode: "generated" | "yaml" | "storage";
locale: FrontendTranslationData;
locale: FrontendLocaleData;
enableFullEditMode: () => void;
setEditMode: (editMode: boolean) => void;
saveConfig: (newConfig: LovelaceConfig) => Promise<void>;

View File

@ -29,6 +29,7 @@ import "./ha-pick-dashboard-row";
import "./ha-pick-language-row";
import "./ha-pick-number-format-row";
import "./ha-pick-theme-row";
import "./ha-pick-time-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-number-format-row>
<ha-pick-time-format-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-time-format-row>
<ha-pick-theme-row
.narrow=${this.narrow}
.hass=${this.hass}

View File

@ -40,7 +40,7 @@ class NumberFormatRow extends LitElement {
>
${Object.values(NumberFormat).map((format) => {
const formattedNumber = formatNumber(1234567.89, {
language: this.hass.locale.language,
...this.hass.locale,
number_format: format,
});
const value = this.hass.localize(
@ -48,7 +48,7 @@ class NumberFormatRow extends LitElement {
);
const twoLine = value.slice(value.length - 2) !== "89"; // Display explicit number formats on one line
return html`
<paper-item .format=${format}>
<paper-item .format=${format} .label=${value}>
<paper-item-body ?two-line=${twoLine}>
<div>${value}</div>
${twoLine

View File

@ -0,0 +1,72 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { formatTime } from "../../common/datetime/format_time";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-settings-row";
import { TimeFormat } from "../../data/translation";
import { HomeAssistant } from "../../types";
@customElement("ha-pick-time-format-row")
class TimeFormatRow 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.time_format.header")}
</span>
<span slot="description">
${this.hass.localize("ui.panel.profile.time_format.description")}
</span>
<ha-paper-dropdown-menu
.label=${this.hass.localize(
"ui.panel.profile.time_format.dropdown_label"
)}
dynamic-align
.disabled=${this.hass.locale === undefined}
>
<paper-listbox
slot="dropdown-content"
.selected=${this.hass.locale.time_format}
@iron-select=${this._handleFormatSelection}
attr-for-selected="format"
>
${Object.values(TimeFormat).map((format) => {
const formattedTime = formatTime(date, {
...this.hass.locale,
time_format: format,
});
const value = this.hass.localize(
`ui.panel.profile.time_format.formats.${format}`
);
return html` <paper-item .format=${format} .label=${value}>
<paper-item-body two-line>
<div>${value}</div>
<div secondary>${formattedTime}</div>
</paper-item-body>
</paper-item>`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row>
`;
}
private async _handleFormatSelection(ev: CustomEvent) {
fireEvent(this, "hass-time-format-select", ev.detail.item.format);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-pick-time-format-row": TimeFormatRow;
}
}

View File

@ -15,7 +15,7 @@ import { subscribeFrontendUserData } from "../data/frontend";
import { forwardHaptic } from "../data/haptics";
import { DEFAULT_PANEL } from "../data/panel";
import { serviceCallWillDisconnect } from "../data/service";
import { NumberFormat } from "../data/translation";
import { NumberFormat, TimeFormat } from "../data/translation";
import { subscribePanels } from "../data/ws-panels";
import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, ServiceCallResponse } from "../types";
@ -49,6 +49,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
locale: {
language,
number_format: NumberFormat.language,
time_format: TimeFormat.language,
},
resources: null as any,
localize: () => "",

View File

@ -7,6 +7,7 @@ import {
getHassTranslationsPre109,
NumberFormat,
saveTranslationPreferences,
TimeFormat,
TranslationCategory,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
@ -28,6 +29,9 @@ declare global {
"hass-number-format-select": {
number_format: NumberFormat;
};
"hass-time-format-select": {
time_format: TimeFormat;
};
}
}
@ -64,6 +68,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this.addEventListener("hass-number-format-select", (e) => {
this._selectNumberFormat((e as CustomEvent).detail, true);
});
this.addEventListener("hass-time-format-select", (e) => {
this._selectTimeFormat((e as CustomEvent).detail, true);
});
this._loadCoreTranslations(getLocalLanguage());
}
@ -95,6 +102,13 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
// We just got number_format from backend, no need to save back
this._selectNumberFormat(locale.number_format, false);
}
if (
locale?.time_format &&
this.hass!.locale.time_format !== locale.time_format
) {
// We just got time_format from backend, no need to save back
this._selectTimeFormat(locale.time_format, false);
}
});
this.hass!.connection.subscribeEvents(
@ -133,6 +147,15 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
}
private _selectTimeFormat(time_format: TimeFormat, saveToBackend: boolean) {
this._updateHass({
locale: { ...this.hass!.locale, time_format: time_format },
});
if (saveToBackend) {
saveTranslationPreferences(this.hass!, this.hass!.locale);
}
}
private _selectLanguage(language: string, saveToBackend: boolean) {
if (!this.hass) {
// should not happen, do it to avoid use this.hass!

View File

@ -3213,6 +3213,17 @@
"none": "None"
}
},
"time_format": {
"header": "Time Format",
"dropdown_label": "Time format",
"description": "Choose how times are formatted.",
"formats": {
"language": "Auto (use language setting)",
"system": "Use system locale",
"12": "12 hours (AM/PM)",
"24": "24 hours"
}
},
"themes": {
"header": "Theme",
"error_no_theme": "No themes available.",

View File

@ -9,10 +9,7 @@ import {
} from "home-assistant-js-websocket";
import { LocalizeFunc } from "./common/translations/localize";
import { CoreFrontendUserData } from "./data/frontend";
import {
FrontendTranslationData,
getHassTranslations,
} from "./data/translation";
import { FrontendLocaleData, getHassTranslations } from "./data/translation";
import { Themes } from "./data/ws-themes";
import { ExternalMessaging } from "./external_app/external_messaging";
@ -198,14 +195,14 @@ export interface HomeAssistant {
panelUrl: string;
// i18n
// current effective language in that order:
// - backend saved user selected lanugage
// - backend saved user selected language
// - language in local app storage
// - browser language
// - english (en)
language: string;
// local stored language, keep that name for backward compability
// local stored language, keep that name for backward compatibility
selectedLanguage: string | null;
locale: FrontendTranslationData;
locale: FrontendLocaleData;
resources: Resources;
localize: LocalizeFunc;
translationMetadata: TranslationMetadata;

View File

@ -1,6 +1,6 @@
import {
fetchTranslationPreferences,
FrontendTranslationData,
FrontendLocaleData,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
@ -55,21 +55,24 @@ export function findAvailableLanguage(language: string) {
*/
export async function getUserLocale(
hass: HomeAssistant
): Promise<Partial<FrontendTranslationData>> {
): Promise<Partial<FrontendLocaleData>> {
const result = await fetchTranslationPreferences(hass);
const language = result?.language;
const number_format = result?.number_format;
const time_format = result?.time_format;
if (language) {
const availableLanguage = findAvailableLanguage(language);
if (availableLanguage) {
return {
language: availableLanguage,
number_format,
time_format,
};
}
}
return {
number_format,
time_format,
};
}

View File

@ -1,7 +1,7 @@
import { assert } from "chai";
import { formatDate } from "../../../src/common/datetime/format_date";
import { NumberFormat } from "../../../src/data/translation";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
describe("formatDate", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 1400);
@ -11,6 +11,7 @@ describe("formatDate", () => {
formatDate(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
}),
"November 18, 2017"
);

View File

@ -4,32 +4,50 @@ import {
formatDateTime,
formatDateTimeWithSeconds,
} from "../../../src/common/datetime/format_date_time";
import { NumberFormat } from "../../../src/data/translation";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
describe("formatDateTime", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 400);
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 400);
it("Formats English date times", () => {
assert.strictEqual(
formatDateTime(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
}),
"November 18, 2017, 11:12 AM"
"November 18, 2017, 11:12 PM"
);
assert.strictEqual(
formatDateTime(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
}),
"November 18, 2017, 23:12"
);
});
});
describe("formatDateTimeWithSeconds", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 400);
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 400);
it("Formats English date times with seconds", () => {
assert.strictEqual(
formatDateTimeWithSeconds(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
}),
"November 18, 2017, 11:12:13 AM"
"November 18, 2017, 11:12:13 PM"
);
assert.strictEqual(
formatDateTimeWithSeconds(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
}),
"November 18, 2017, 23:12:13"
);
});
});

View File

@ -4,32 +4,50 @@ import {
formatTime,
formatTimeWithSeconds,
} from "../../../src/common/datetime/format_time";
import { NumberFormat } from "../../../src/data/translation";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
describe("formatTime", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 1400);
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 1400);
it("Formats English times", () => {
assert.strictEqual(
formatTime(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
}),
"11:12 AM"
"11:12 PM"
);
assert.strictEqual(
formatTime(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
}),
"23:12"
);
});
});
describe("formatTimeWithSeconds", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 400);
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 400);
it("Formats English times with seconds", () => {
assert.strictEqual(
formatTimeWithSeconds(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
}),
"11:12:13 AM"
"11:12:13 PM"
);
assert.strictEqual(
formatTimeWithSeconds(dateObj, {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
}),
"23:12:13"
);
});
});

View File

@ -2,20 +2,26 @@ import { assert } from "chai";
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
import { UNKNOWN } from "../../../src/data/entity";
import {
FrontendTranslationData,
FrontendLocaleData,
NumberFormat,
TimeFormat,
} from "../../../src/data/translation";
const localeData: FrontendTranslationData = {
language: "en",
number_format: NumberFormat.comma_decimal,
};
let localeData: FrontendLocaleData;
describe("computeStateDisplay", () => {
// Mock Localize function for testing
const localize = (message, ...args) =>
message + (args.length ? ": " + args.join(",") : "");
beforeEach(() => {
localeData = {
language: "en",
number_format: NumberFormat.comma_decimal,
time_format: TimeFormat.am_pm,
};
});
it("Localizes binary sensor defaults", () => {
const stateObj: any = {
entity_id: "binary_sensor.test",
@ -148,7 +154,7 @@ describe("computeStateDisplay", () => {
);
});
it("Localizes input_datetime with full date time", () => {
describe("Localizes input_datetime with full date time", () => {
const stateObj: any = {
entity_id: "input_datetime.test",
state: "123",
@ -158,16 +164,25 @@ describe("computeStateDisplay", () => {
year: 2017,
month: 11,
day: 18,
hour: 11,
hour: 23,
minute: 12,
second: 13,
},
};
it("Uses am/pm time format", () => {
assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData),
"November 18, 2017, 11:12 AM"
"November 18, 2017, 11:12 PM"
);
});
it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData),
"November 18, 2017, 23:12"
);
});
});
it("Localizes input_datetime with date", () => {
const stateObj: any = {
@ -179,7 +194,7 @@ describe("computeStateDisplay", () => {
year: 2017,
month: 11,
day: 18,
hour: 11,
hour: 23,
minute: 12,
second: 13,
},
@ -190,7 +205,7 @@ describe("computeStateDisplay", () => {
);
});
it("Localizes input_datetime with time", () => {
describe("Localizes input_datetime with time", () => {
const stateObj: any = {
entity_id: "input_datetime.test",
state: "123",
@ -200,16 +215,26 @@ describe("computeStateDisplay", () => {
year: 2017,
month: 11,
day: 18,
hour: 11,
hour: 23,
minute: 12,
second: 13,
},
};
it("Uses am/pm time format", () => {
localeData.time_format = TimeFormat.am_pm;
assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData),
"11:12 AM"
"11:12 PM"
);
});
it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData),
"23:12"
);
});
});
it("Localizes unavailable", () => {
const altLocalize = (message, ...args) => {
@ -230,10 +255,9 @@ describe("computeStateDisplay", () => {
});
it("Localizes custom state", () => {
const altLocalize = () => {
const altLocalize = () =>
// No matches can be found
return "";
};
"";
const stateObj: any = {
entity_id: "sensor.test",
state: "My Custom State",

View File

@ -1,23 +1,29 @@
import { assert } from "chai";
import { formatNumber } from "../../../src/common/string/format_number";
import { NumberFormat } from "../../../src/data/translation";
import {
FrontendLocaleData,
NumberFormat,
TimeFormat,
} from "../../../src/data/translation";
describe("formatNumber", () => {
// Node only ships with English support for `Intl`, so we can not test for other number formats here.
it("Formats English numbers", () => {
assert.strictEqual(
formatNumber(1234.5, {
// Create default to not have to specify a not relevant TimeFormat over and over again.
const defaultLocale: FrontendLocaleData = {
language: "en",
number_format: NumberFormat.language,
}),
"1,234.5"
);
time_format: TimeFormat.language,
};
// Node only ships with English support for `Intl`, so we can not test for other number formats here.
it("Formats English numbers", () => {
assert.strictEqual(formatNumber(1234.5, defaultLocale), "1,234.5");
});
it("Test format 'none' (keep dot despite language 'de')", () => {
assert.strictEqual(
formatNumber(1.23, {
...defaultLocale,
language: "de",
number_format: NumberFormat.none,
}),
@ -26,57 +32,32 @@ describe("formatNumber", () => {
});
it("Ensure zero is kept for format 'language'", () => {
assert.strictEqual(
formatNumber(0, {
language: "en",
number_format: NumberFormat.language,
}),
"0"
);
assert.strictEqual(formatNumber(0, defaultLocale), "0");
});
it("Ensure zero is kept for format 'none'", () => {
assert.strictEqual(
formatNumber(0, {
language: "en",
number_format: NumberFormat.none,
}),
formatNumber(0, { ...defaultLocale, number_format: NumberFormat.none }),
"0"
);
});
it("Test empty string input for format 'none'", () => {
assert.strictEqual(
formatNumber("", {
language: "en",
number_format: NumberFormat.none,
}),
formatNumber("", { ...defaultLocale, number_format: NumberFormat.none }),
""
);
});
it("Test empty string input for format 'language'", () => {
assert.strictEqual(
formatNumber("", {
language: "en",
number_format: NumberFormat.language,
}),
"0"
);
assert.strictEqual(formatNumber("", defaultLocale), "0");
});
it("Formats number with options", () => {
assert.strictEqual(
formatNumber(
1234.5,
{
language: "en",
number_format: NumberFormat.language,
},
{
formatNumber(1234.5, defaultLocale, {
minimumFractionDigits: 2,
}
),
}),
"1,234.50"
);
});

View File

@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "commonjs"
"module": "commonjs",
"esModuleInterop": true
}
}