mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 00:36:34 +00:00
Allow users to select time format for UI rendering (#9042)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
87e4c209f4
commit
70a1edd1dd
@ -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";
|
||||
|
||||
const formatDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDate = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateMem(locale).format(dateObj)
|
||||
: (dateObj: Date) => format(dateObj, "longDate");
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateWeekdayMem(locale).format(dateObj)
|
||||
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
|
||||
|
@ -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";
|
||||
|
||||
const formatDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTime = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
|
||||
? (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),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateTimeWithSecondsMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
|
||||
|
@ -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";
|
||||
|
||||
const formatTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "shortTime");
|
||||
? (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),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "mediumTime");
|
||||
? (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),
|
||||
})
|
||||
);
|
||||
|
||||
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "dddd, HH:mm");
|
||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeWeekdayMem(locale).format(dateObj)
|
||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||
|
15
src/common/datetime/use_am_pm.ts
Normal file
15
src/common/datetime/use_am_pm.ts
Normal 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;
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 = "";
|
||||
|
||||
|
@ -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") {
|
||||
|
@ -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[] = [];
|
||||
|
@ -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 (
|
||||
|
@ -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: () => "",
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
72
src/panels/profile/ha-pick-time-format-row.ts
Normal file
72
src/panels/profile/ha-pick-time-format-row.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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: () => "",
|
||||
|
@ -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!
|
||||
|
@ -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.",
|
||||
|
13
src/types.ts
13
src/types.ts
@ -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
|
||||
// - language in local appstorage
|
||||
// - 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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
);
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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,15 +164,24 @@ describe("computeStateDisplay", () => {
|
||||
year: 2017,
|
||||
month: 11,
|
||||
day: 18,
|
||||
hour: 11,
|
||||
hour: 23,
|
||||
minute: 12,
|
||||
second: 13,
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
"November 18, 2017, 11:12 AM"
|
||||
);
|
||||
it("Uses am/pm time format", () => {
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
"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", () => {
|
||||
@ -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,15 +215,25 @@ describe("computeStateDisplay", () => {
|
||||
year: 2017,
|
||||
month: 11,
|
||||
day: 18,
|
||||
hour: 11,
|
||||
hour: 23,
|
||||
minute: 12,
|
||||
second: 13,
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
"11:12 AM"
|
||||
);
|
||||
it("Uses am/pm time format", () => {
|
||||
localeData.time_format = TimeFormat.am_pm;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
"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", () => {
|
||||
@ -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",
|
||||
|
@ -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", () => {
|
||||
// Create default to not have to specify a not relevant TimeFormat over and over again.
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
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, {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
}),
|
||||
"1,234.5"
|
||||
);
|
||||
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,
|
||||
},
|
||||
{
|
||||
minimumFractionDigits: 2,
|
||||
}
|
||||
),
|
||||
formatNumber(1234.5, defaultLocale, {
|
||||
minimumFractionDigits: 2,
|
||||
}),
|
||||
"1,234.50"
|
||||
);
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user