Create number formatting options on the profile panel (#7925)

This commit is contained in:
Josh McCarty 2021-03-28 09:32:48 -07:00 committed by GitHub
parent 0393970a80
commit f43c420d59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 513 additions and 235 deletions

View File

@ -39,7 +39,7 @@ class HcLovelace extends LitElement {
urlPath: this.urlPath!, urlPath: this.urlPath!,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
language: "en", locale: this.hass.locale,
saveConfig: async () => undefined, saveConfig: async () => undefined,
deleteConfig: async () => undefined, deleteConfig: async () => undefined,
setEditMode: () => undefined, setEditMode: () => undefined,

View File

@ -1,9 +1,10 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support"; import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales, { dateObj.toLocaleDateString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@ -11,8 +12,8 @@ export const formatDate = toLocaleDateStringSupportsOptions
: (dateObj: Date) => format(dateObj, "longDate"); : (dateObj: Date) => format(dateObj, "longDate");
export const formatDateWeekday = toLocaleDateStringSupportsOptions export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales, { dateObj.toLocaleDateString(locales.language, {
weekday: "long", weekday: "long",
month: "short", month: "short",
day: "numeric", day: "numeric",

View File

@ -1,9 +1,10 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support"; import { toLocaleStringSupportsOptions } from "./check_options_support";
export const formatDateTime = toLocaleStringSupportsOptions export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales, { dateObj.toLocaleString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@ -13,8 +14,8 @@ export const formatDateTime = toLocaleStringSupportsOptions
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm"); : (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales, { dateObj.toLocaleString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",

View File

@ -1,17 +1,18 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support"; import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
export const formatTime = toLocaleTimeStringSupportsOptions export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
}) })
: (dateObj: Date) => format(dateObj, "shortTime"); : (dateObj: Date) => format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
@ -19,8 +20,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
: (dateObj: Date) => format(dateObj, "mediumTime"); : (dateObj: Date) => format(dateObj, "mediumTime");
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
weekday: "long", weekday: "long",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",

View File

@ -1,5 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendTranslationData } from "../../data/translation";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
@ -10,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
language: string, locale: FrontendTranslationData,
state?: string state?: string
): string => { ): string => {
const compareState = state !== undefined ? state : stateObj.state; const compareState = state !== undefined ? state : stateObj.state;
@ -20,7 +21,7 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
return `${formatNumber(compareState, language)} ${ return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
} }
@ -35,7 +36,7 @@ export const computeStateDisplay = (
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
stateObj.attributes.day stateObj.attributes.day
); );
return formatDate(date, language); return formatDate(date, locale);
} }
if (!stateObj.attributes.has_date) { if (!stateObj.attributes.has_date) {
const now = new Date(); const now = new Date();
@ -48,7 +49,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour, stateObj.attributes.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatTime(date, language); return formatTime(date, locale);
} }
date = new Date( date = new Date(
@ -58,7 +59,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour, stateObj.attributes.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, language); return formatDateTime(date, locale);
} }
if (domain === "humidifier") { if (domain === "humidifier") {
@ -67,8 +68,9 @@ export const computeStateDisplay = (
} }
} }
if (domain === "counter") { // `counter` and `number` domains do not have a unit of measurement but should still use `formatNumber`
return formatNumber(compareState, language); if (domain === "counter" || domain === "number") {
return formatNumber(compareState, locale);
} }
return ( return (

View File

@ -1,14 +1,36 @@
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
/** /**
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility. * Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
* *
* @param num The number to format * @param num The number to format
* @param language The language to use when formatting the number * @param locale The user-selected language and number format, from `hass.locale`
* @param options Intl.NumberFormatOptions to use
*/ */
export const formatNumber = ( export const formatNumber = (
num: string | number, num: string | number,
language: string, locale?: FrontendTranslationData,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): string => { ): string => {
let format: string | string[] | undefined;
switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN() // Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN = Number.isNaN =
Number.isNaN || Number.isNaN ||
@ -16,13 +38,27 @@ export const formatNumber = (
return typeof input === "number" && isNaN(input); return typeof input === "number" && isNaN(input);
}; };
if (!Number.isNaN(Number(num)) && Intl) { if (
return new Intl.NumberFormat( !Number.isNaN(Number(num)) &&
language, Intl &&
getDefaultFormatOptions(num, options) locale?.number_format !== NumberFormat.none
).format(Number(num)); ) {
try {
return new Intl.NumberFormat(
format,
getDefaultFormatOptions(num, options)
).format(Number(num));
} catch (error) {
// Don't fail when using "TEST" language
// eslint-disable-next-line no-console
console.error(error);
return new Intl.NumberFormat(
undefined,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
} }
return num.toString(); return num ? num.toString() : "";
}; };
/** /**

View File

@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors(
return value; return value;
} }
const date = new Date(values[index].value); const date = new Date(values[index].value);
return formatTime(date, this.hass.language); return formatTime(date, this.hass.locale);
} }
drawChart() { drawChart() {

View File

@ -116,12 +116,8 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN : state.state === UNKNOWN
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? formatNumber(state.state, this.hass!.language) ? formatNumber(state.state, this.hass!.locale)
: computeStateDisplay( : computeStateDisplay(this.hass!.localize, state, this.hass!.locale);
this.hass!.localize,
state,
this.hass!.language
);
} }
} }

View File

@ -84,7 +84,7 @@ class StateInfo extends LitElement {
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) { if (!oldHass || oldHass.locale !== this.hass.locale) {
this.rtl = computeRTL(this.hass); this.rtl = computeRTL(this.hass);
} }
} }

View File

@ -53,14 +53,14 @@ class HaClimateState extends LitElement {
if (this.stateObj.attributes.current_temperature != null) { if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.current_temperature, this.stateObj.attributes.current_temperature,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.current_humidity != null) { if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.current_humidity, this.stateObj.attributes.current_humidity,
this.hass!.language this.hass.locale
)} %`; )} %`;
} }
@ -78,17 +78,17 @@ class HaClimateState extends LitElement {
) { ) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.target_temp_low, this.stateObj.attributes.target_temp_low,
this.hass!.language this.hass.locale
)}-${formatNumber( )}-${formatNumber(
this.stateObj.attributes.target_temp_high, this.stateObj.attributes.target_temp_high,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.temperature != null) { if (this.stateObj.attributes.temperature != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.temperature, this.stateObj.attributes.temperature,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if ( if (
@ -97,17 +97,17 @@ class HaClimateState extends LitElement {
) { ) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.target_humidity_low, this.stateObj.attributes.target_humidity_low,
this.hass!.language this.hass.locale
)}-${formatNumber( )}-${formatNumber(
this.stateObj.attributes.target_humidity_high, this.stateObj.attributes.target_humidity_high,
this.hass!.language this.hass.locale
)} %`; )} %`;
} }
if (this.stateObj.attributes.humidity != null) { if (this.stateObj.attributes.humidity != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.humidity, this.stateObj.attributes.humidity,
this.hass!.language this.hass.locale
)} %`; )} %`;
} }

View File

@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) { if (!oldHass || oldHass.locale !== this.hass.locale) {
this._hour24format = this._compute24hourFormat(); this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass); this._rtlDirection = computeRTLDirection(this.hass);
} }
@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
<div slot="input" class="date-range-inputs"> <div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input <paper-input
.value=${formatDateTime(this.startDate, this.hass.language)} .value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.date-range-picker.start_date" "ui.components.date-range-picker.start_date"
)} )}
@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
readonly readonly
></paper-input> ></paper-input>
<paper-input <paper-input
.value=${formatDateTime(this.endDate, this.hass.language)} .value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize( label=${this.hass.localize(
"ui.components.date-range-picker.end_date" "ui.components.date-range-picker.end_date"
)} )}

View File

@ -11,6 +11,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { formatNumber } from "../common/string/format_number"; import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status"; import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
@ -29,7 +30,7 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0; @property({ type: Number }) public value = 0;
@property({ type: String }) public language = ""; @property() public locale!: FrontendTranslationData;
@property() public label = ""; @property() public label = "";
@ -90,7 +91,7 @@ export class Gauge extends LitElement {
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
${formatNumber(this.value, this.language)} ${this.label} ${formatNumber(this.value, this.locale)} ${this.label}
</text> </text>
</svg>`; </svg>`;
} }

View File

@ -245,7 +245,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl || hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user || hass.user !== oldHass.user ||
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.language !== oldHass.language || hass.locale !== oldHass.locale ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel hass.defaultPanel !== oldHass.defaultPanel
); );
@ -281,7 +281,7 @@ class HaSidebar extends LitElement {
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) { if (!oldHass || oldHass.locale !== this.hass.locale) {
this.rtl = computeRTL(this.hass); this.rtl = computeRTL(this.hass);
} }

View File

@ -2,6 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/string/format_number";
import LocalizeMixin from "../mixins/localize-mixin"; import LocalizeMixin from "../mixins/localize-mixin";
/* /*
@ -55,21 +56,31 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
computeTarget(hass, stateObj) { computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null; if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined. // We're using "!= null" on purpose so that we match both null and undefined.
if ( if (
stateObj.attributes.target_temp_low != null && stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null stateObj.attributes.target_temp_high != null
) { ) {
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`; return `${formatNumber(
stateObj.attributes.target_temp_low,
this.hass.locale
)} - ${formatNumber(
stateObj.attributes.target_temp_high,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
} }
if (stateObj.attributes.temperature != null) { if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`; return `${formatNumber(
stateObj.attributes.temperature,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
} }
return ""; return "";
} }
_localizeState(stateObj) { _localizeState(stateObj) {
return computeStateDisplay(this.hass.localize, stateObj); return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
} }
} }
customElements.define("ha-water_heater-state", HaWaterHeaterState); customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
const item = items[0]; const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x; const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTimeWithSeconds(date, this.hass.language); return formatDateTimeWithSeconds(date, this.hass.locale);
}; };
const chartOptions = { const chartOptions = {

View File

@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const formatTooltipLabel = (item, data) => { const formatTooltipLabel = (item, data) => {
const values = data.datasets[item.datasetIndex].data[item.index]; const values = data.datasets[item.datasetIndex].data[item.index];
const start = formatDateTimeWithSeconds(values[0], this.hass.language); const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
const end = formatDateTimeWithSeconds(values[1], this.hass.language); const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
const state = values[2]; const state = values[2];
return [state, start, end]; return [state, start, end];

View File

@ -366,7 +366,7 @@ export class HaAutomationTracer extends LitElement {
Triggered by the ${this.trace.variables.trigger.description} at Triggered by the ${this.trace.variables.trigger.description} at
${formatDateTimeWithSeconds( ${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.start), new Date(this.trace.timestamp.start),
this.hass.language this.hass.locale
)} )}
</ha-timeline> </ha-timeline>
`, `,
@ -433,7 +433,7 @@ export class HaAutomationTracer extends LitElement {
? html`Finished at ? html`Finished at
${formatDateTimeWithSeconds( ${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.finish), new Date(this.trace.timestamp.finish),
this.hass.language this.hass.locale
)} )}
(runtime: (runtime:
${( ${(

View File

@ -54,7 +54,7 @@ export const getRecent = (
} }
const prom = fetchRecent(hass, entityId, startTime, endTime).then( const prom = fetchRecent(hass, entityId, startTime, endTime).then(
(stateHistory) => computeHistory(hass, stateHistory, localize, language), (stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => { (err) => {
delete RECENT_CACHE[entityId]; delete RECENT_CACHE[entityId];
throw err; throw err;
@ -140,12 +140,7 @@ export const getRecentWithCache = (
delete stateHistoryCache[cacheKey]; delete stateHistoryCache[cacheKey];
throw err; throw err;
} }
const stateHistory = computeHistory( const stateHistory = computeHistory(hass, fetchedHistory, localize);
hass,
fetchedHistory,
localize,
language
);
if (appendingToCache) { if (appendingToCache) {
mergeLine(stateHistory.line, cache.data.line); mergeLine(stateHistory.line, cache.data.line);
mergeTimeline(stateHistory.timeline, cache.data.timeline); mergeTimeline(stateHistory.timeline, cache.data.timeline);

View File

@ -4,6 +4,7 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { FrontendTranslationData } from "./translation";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const LINE_ATTRIBUTES_TO_KEEP = [ const LINE_ATTRIBUTES_TO_KEEP = [
@ -109,7 +110,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = ( const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: string, language: FrontendTranslationData,
states: HassEntity[] states: HassEntity[]
): TimelineEntity => { ): TimelineEntity => {
const data: TimelineState[] = []; const data: TimelineState[] = [];
@ -203,8 +204,7 @@ const processLineChartEntities = (
export const computeHistory = ( export const computeHistory = (
hass: HomeAssistant, hass: HomeAssistant,
stateHistory: HassEntity[][], stateHistory: HassEntity[][],
localize: LocalizeFunc, localize: LocalizeFunc
language: string
): HistoryResult => { ): HistoryResult => {
const lineChartDevices: { [unit: string]: HassEntity[][] } = {}; const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
const timelineDevices: TimelineEntity[] = []; const timelineDevices: TimelineEntity[] = [];
@ -235,7 +235,7 @@ export const computeHistory = (
if (!unit) { if (!unit) {
timelineDevices.push( timelineDevices.push(
processTimelineEntity(localize, language, stateInfo) processTimelineEntity(localize, hass.locale, stateInfo)
); );
} else if (unit in lineChartDevices) { } else if (unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo); lineChartDevices[unit].push(stateInfo);

View File

@ -314,7 +314,7 @@ export const getLogbookMessage = (
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state", "state",
stateObj stateObj
? computeStateDisplay(hass.localize, stateObj, hass.language, state) ? computeStateDisplay(hass.localize, stateObj, hass.locale, state)
: state : state
); );
}; };

View File

@ -1,8 +1,18 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { fetchFrontendUserData, saveFrontendUserData } from "./frontend"; import { fetchFrontendUserData, saveFrontendUserData } from "./frontend";
export enum NumberFormat {
language = "language",
system = "system",
comma_decimal = "comma_decimal",
decimal_comma = "decimal_comma",
space_comma = "space_comma",
none = "none",
}
export interface FrontendTranslationData { export interface FrontendTranslationData {
language: string; language: string;
number_format: NumberFormat;
} }
declare global { declare global {

View File

@ -133,7 +133,7 @@ export const getWind = (
speed: string, speed: string,
bearing: string bearing: string
): string => { ): string => {
const speedText = `${formatNumber(speed, hass!.language)} ${getWeatherUnit( const speedText = `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
hass!, hass!,
"wind_speed" "wind_speed"
)}`; )}`;
@ -206,7 +206,7 @@ export const getSecondaryWeatherAttribute = (
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon> <ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
` `
: hass!.localize(`ui.card.weather.attributes.${attribute}`)} : hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })} ${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, attribute)} ${getWeatherUnit(hass!, attribute)}
`; `;
}; };

View File

@ -50,7 +50,7 @@ class MoreInfoSun extends LitElement {
<div class="value"> <div class="value">
${formatTime( ${formatTime(
item === "ris" ? risingDate : settingDate, item === "ris" ? risingDate : settingDate,
this.hass.language this.hass.locale
)} )}
</div> </div>
</div> </div>
@ -61,10 +61,7 @@ class MoreInfoSun extends LitElement {
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")} ${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div> </div>
<div class="value"> <div class="value">
${formatNumber( ${formatNumber(this.stateObj.attributes.elevation, this.hass.locale)}
this.stateObj.attributes.elevation,
this.hass!.language
)}
</div> </div>
</div> </div>
`; `;

View File

@ -68,7 +68,7 @@ class MoreInfoWeather extends LitElement {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if ( if (
!oldHass || !oldHass ||
oldHass.language !== this.hass.language || oldHass.locale !== this.hass.locale ||
oldHass.config.unit_system !== this.hass.config.unit_system oldHass.config.unit_system !== this.hass.config.unit_system
) { ) {
return true; return true;
@ -91,7 +91,7 @@ class MoreInfoWeather extends LitElement {
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.temperature, this.stateObj.attributes.temperature,
this.hass!.language this.hass.locale
)} )}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
@ -106,7 +106,7 @@ class MoreInfoWeather extends LitElement {
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.pressure, this.stateObj.attributes.pressure,
this.hass!.language this.hass.locale
)} )}
${getWeatherUnit(this.hass, "air_pressure")} ${getWeatherUnit(this.hass, "air_pressure")}
</div> </div>
@ -123,7 +123,7 @@ class MoreInfoWeather extends LitElement {
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.humidity, this.stateObj.attributes.humidity,
this.hass!.language this.hass.locale
)} )}
% %
</div> </div>
@ -157,7 +157,7 @@ class MoreInfoWeather extends LitElement {
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.visibility, this.stateObj.attributes.visibility,
this.hass!.language this.hass.locale
)} )}
${getWeatherUnit(this.hass, "length")} ${getWeatherUnit(this.hass, "length")}
</div> </div>
@ -184,7 +184,7 @@ class MoreInfoWeather extends LitElement {
<div class="main"> <div class="main">
${formatTimeWeekday( ${formatTimeWeekday(
new Date(item.datetime), new Date(item.datetime),
this.hass.language this.hass.locale
)} )}
</div> </div>
` `
@ -194,17 +194,17 @@ class MoreInfoWeather extends LitElement {
<div class="main"> <div class="main">
${formatDateWeekday( ${formatDateWeekday(
new Date(item.datetime), new Date(item.datetime),
this.hass.language this.hass.locale
)} )}
</div> </div>
<div class="templow"> <div class="templow">
${formatNumber(item.templow, this.hass!.language)} ${formatNumber(item.templow, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
` `
: ""} : ""}
<div class="temp"> <div class="temp">
${formatNumber(item.temperature, this.hass!.language)} ${formatNumber(item.temperature, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
>${computeStateDisplay( >${computeStateDisplay(
this.hass.localize, this.hass.localize,
this.notification, this.notification,
this.hass.language this.hass.locale
)}</mwc-button )}</mwc-button
> >
</notification-item-template> </notification-item-template>

View File

@ -9,6 +9,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { formatDateTime } from "../../common/datetime/format_date_time";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import "../../components/ha-relative-time"; import "../../components/ha-relative-time";
import { PersistentNotification } from "../../data/persistent_notification"; import { PersistentNotification } from "../../data/persistent_notification";
@ -92,13 +93,7 @@ export class HuiPersistentNotificationItem extends LitElement {
} }
const d = new Date(notification.created_at!); const d = new Date(notification.created_at!);
return d.toLocaleDateString(hass.language, { return formatDateTime(d, hass.locale);
year: "numeric",
month: "short",
day: "numeric",
minute: "numeric",
hour: "numeric",
});
} }
} }

View File

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

View File

@ -55,7 +55,7 @@ class HaAutomationPicker extends LitElement {
}); });
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean, _language): DataTableColumnContainer => { (narrow: boolean, _locale): DataTableColumnContainer => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer = {
toggle: { toggle: {
title: "", title: "",
@ -83,7 +83,7 @@ class HaAutomationPicker extends LitElement {
${automation.attributes.last_triggered ${automation.attributes.last_triggered
? formatDateTime( ? formatDateTime(
new Date(automation.attributes.last_triggered), new Date(automation.attributes.last_triggered),
this.hass.language this.hass.locale
) )
: this.hass.localize("ui.components.relative_time.never")} : this.hass.localize("ui.components.relative_time.never")}
</div> </div>
@ -192,7 +192,7 @@ class HaAutomationPicker extends LitElement {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automation} .tabs=${configSections.automation}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations)} .data=${this._automations(this.automations)}
id="entity_id" id="entity_id"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(

View File

@ -85,7 +85,7 @@ export class HaAutomationTrace extends LitElement {
html`<option value=${trace.run_id} html`<option value=${trace.run_id}
>${formatDateTimeWithSeconds( >${formatDateTimeWithSeconds(
new Date(trace.timestamp.start), new Date(trace.timestamp.start),
this.hass.language this.hass.locale
)}</option )}</option
>` >`
)} )}

View File

@ -221,7 +221,7 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
"{periodEnd}", "{periodEnd}",
formatDateTime( formatDateTime(
new Date(subInfo.plan_renewal_date * 1000), new Date(subInfo.plan_renewal_date * 1000),
this.hass.language this.hass.locale
) )
); );
} }

View File

@ -48,7 +48,7 @@ class DialogCloudCertificate extends LitElement {
)} )}
${formatDateTime( ${formatDateTime(
new Date(certificateInfo.expire_date), new Date(certificateInfo.expire_date),
this.hass!.language this.hass!.locale
)}<br /> )}<br />
(${this.hass!.localize( (${this.hass!.localize(
"ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed" "ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed"

View File

@ -49,8 +49,8 @@ export abstract class HaDeviceAutomationCard<
if (changedProps.has("deviceId") || changedProps.has("automations")) { if (changedProps.has("deviceId") || changedProps.has("automations")) {
return true; return true;
} }
const oldHass = changedProps.get("hass"); const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || this.hass.language !== oldHass.language) { if (!oldHass || oldHass.language !== this.hass.language) {
return true; return true;
} }
return false; return false;

View File

@ -61,7 +61,7 @@ class MQTTMessages extends LitElement {
Received Received
${formatTimeWithSeconds( ${formatTimeWithSeconds(
new Date(message.time), new Date(message.time),
this.hass.language this.hass.locale
)} )}
</div> </div>
${this._renderSingleMessage(message)} ${this._renderSingleMessage(message)}

View File

@ -101,7 +101,7 @@ class SystemHealthCard extends LitElement {
`} `}
`; `;
} else if (info.type === "date") { } else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.language); value = formatDateTime(new Date(info.value), this.hass.locale);
} }
} else { } else {
value = domainInfo.info[key]; value = domainInfo.info[key];
@ -228,7 +228,7 @@ class SystemHealthCard extends LitElement {
} else if (info.type === "failed") { } else if (info.type === "failed") {
value = `failed to load: ${info.error}`; value = `failed to load: ${info.error}`;
} else if (info.type === "date") { } else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.language); value = formatDateTime(new Date(info.value), this.hass.locale);
} }
} else { } else {
value = domainInfo.info[key]; value = domainInfo.info[key];

View File

@ -77,7 +77,7 @@ class MqttSubscribeCard extends LitElement {
"topic", "topic",
msg.message.topic, msg.message.topic,
"time", "time",
formatTime(msg.time, this.hass!.language) formatTime(msg.time, this.hass!.locale)
)} )}
<pre>${msg.payload}</pre> <pre>${msg.payload}</pre>
<div class="bottom"> <div class="bottom">

View File

@ -116,15 +116,12 @@ class DialogSystemLogDetail extends LitElement {
${item.count > 0 ${item.count > 0
? html` ? html`
First occurred: First occurred:
${formatSystemLogTime( ${formatSystemLogTime(item.first_occurred, this.hass!.locale)}
item.first_occurred,
this.hass!.language
)}
(${item.count} occurrences) <br /> (${item.count} occurrences) <br />
` `
: ""} : ""}
Last logged: Last logged:
${formatSystemLogTime(item.timestamp, this.hass!.language)} ${formatSystemLogTime(item.timestamp, this.hass!.locale)}
</p> </p>
${item.message.length > 1 ${item.message.length > 1
? html` ? html`

View File

@ -68,7 +68,7 @@ export class SystemLogCard extends LitElement {
<div secondary> <div secondary>
${formatSystemLogTime( ${formatSystemLogTime(
item.timestamp, item.timestamp,
this.hass!.language this.hass!.locale
)} )}
${html`(<span class="${item.level.toLowerCase()}" ${html`(<span class="${item.level.toLowerCase()}"
@ -88,7 +88,7 @@ export class SystemLogCard extends LitElement {
"time", "time",
formatSystemLogTime( formatSystemLogTime(
item.first_occurred, item.first_occurred,
this.hass!.language this.hass!.locale
), ),
"counter", "counter",
item.count item.count

View File

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

View File

@ -89,7 +89,7 @@ class HaScriptPicker extends LitElement {
${script.attributes.last_triggered ${script.attributes.last_triggered
? formatDateTime( ? formatDateTime(
new Date(script.attributes.last_triggered), new Date(script.attributes.last_triggered),
this.hass.language this.hass.locale
) )
: this.hass.localize("ui.components.relative_time.never")} : this.hass.localize("ui.components.relative_time.never")}
</div> </div>

View File

@ -82,10 +82,7 @@ class EventSubscribeCard extends LitElement {
"name", "name",
ev.id ev.id
)} )}
${formatTime( ${formatTime(new Date(ev.event.time_fired), this.hass!.locale)}:
new Date(ev.event.time_fired),
this.hass!.language
)}:
<pre>${JSON.stringify(ev.event, null, 4)}</pre> <pre>${JSON.stringify(ev.event, null, 4)}</pre>
</div> </div>
` `

View File

@ -462,14 +462,14 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
lastChangedString(entity) { lastChangedString(entity) {
return formatDateTimeWithSeconds( return formatDateTimeWithSeconds(
new Date(entity.last_changed), new Date(entity.last_changed),
this.hass.language this.hass.locale
); );
} }
lastUpdatedString(entity) { lastUpdatedString(entity) {
return formatDateTimeWithSeconds( return formatDateTimeWithSeconds(
new Date(entity.last_updated), new Date(entity.last_updated),
this.hass.language this.hass.locale
); );
} }

View File

@ -181,8 +181,7 @@ class HaPanelHistory extends LitElement {
this._stateHistory = computeHistory( this._stateHistory = computeHistory(
this.hass, this.hass,
dateHistory, dateHistory,
this.hass.localize, this.hass.localize
this.hass.language
); );
this._isLoading = false; this._isLoading = false;
} }

View File

@ -62,7 +62,7 @@ class HaLogbook extends LitElement {
protected shouldUpdate(changedProps: PropertyValues<this>) { protected shouldUpdate(changedProps: PropertyValues<this>) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const languageChanged = const languageChanged =
oldHass === undefined || oldHass.language !== this.hass.language; oldHass === undefined || oldHass.locale !== this.hass.locale;
return ( return (
changedProps.has("entries") || changedProps.has("entries") ||
@ -139,7 +139,7 @@ class HaLogbook extends LitElement {
new Date(previous.when).toDateString()) new Date(previous.when).toDateString())
? html` ? html`
<h4 class="date"> <h4 class="date">
${formatDate(new Date(item.when), this.hass.language)} ${formatDate(new Date(item.when), this.hass.locale)}
</h4> </h4>
` `
: html``} : html``}
@ -204,7 +204,7 @@ class HaLogbook extends LitElement {
<span <span
>${formatTimeWithSeconds( >${formatTimeWithSeconds(
new Date(item.when), new Date(item.when),
this.hass.language this.hass.locale
)}</span )}</span
> >
- -

View File

@ -132,7 +132,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
if ( if (
!oldHass || !oldHass ||
oldHass.themes !== this.hass!.themes || oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language oldHass.locale !== this.hass!.locale
) { ) {
return true; return true;
} }

View File

@ -113,7 +113,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
if ( if (
!oldHass || !oldHass ||
oldHass.themes !== this.hass!.themes || oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language oldHass.locale !== this.hass!.locale
) { ) {
return true; return true;
} }
@ -196,7 +196,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</span>` </span>`
: ""} : ""}

View File

@ -129,11 +129,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? stateObj.attributes[this._config.attribute!] ?? ? stateObj.attributes[this._config.attribute!] ??
this.hass.localize("state.default.unknown") this.hass.localize("state.default.unknown")
: stateObj.attributes.unit_of_measurement : stateObj.attributes.unit_of_measurement
? formatNumber(stateObj.state, this.hass!.language) ? formatNumber(stateObj.state, this.hass.locale)
: computeStateDisplay( : computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)}</span )}</span
>${showUnit >${showUnit
? html` ? html`

View File

@ -130,7 +130,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
.min=${this._config.min!} .min=${this._config.min!}
.max=${this._config.max!} .max=${this._config.max!}
.value=${stateObj.state} .value=${stateObj.state}
.language=${this.hass!.language} .locale=${this.hass!.locale}
.label=${this._config!.unit || .label=${this._config!.unit ||
this.hass?.states[this._config!.entity].attributes this.hass?.states[this._config!.entity].attributes
.unit_of_measurement || .unit_of_measurement ||

View File

@ -134,7 +134,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
!this._configEntities || !this._configEntities ||
!oldHass || !oldHass ||
oldHass.themes !== this.hass!.themes || oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language oldHass.locale !== this.hass!.locale
) { ) {
return true; return true;
} }
@ -301,7 +301,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.language this.hass!.locale
)} )}
</div> </div>
` `

View File

@ -158,7 +158,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
` `

View File

@ -101,7 +101,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
!this._configEntities || !this._configEntities ||
!oldHass || !oldHass ||
oldHass.themes !== this.hass!.themes || oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language oldHass.locale !== this.hass!.locale
) { ) {
return true; return true;
} }

View File

@ -125,7 +125,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
const state = computeStateDisplay( const state = computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
); );
let footer: TemplateResult | string = ""; let footer: TemplateResult | string = "";

View File

@ -118,7 +118,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
if ( if (
!oldHass || !oldHass ||
oldHass.themes !== this.hass!.themes || oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language oldHass.locale !== this.hass!.locale
) { ) {
return true; return true;
} }
@ -259,7 +259,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
${computeStateName(stateObj)} : ${computeStateDisplay( ${computeStateName(stateObj)} : ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.language this.hass!.locale
)} )}
`} `}
></ha-icon-button> ></ha-icon-button>
@ -276,7 +276,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.language this.hass!.locale
)} )}
</div> </div>
`} `}

View File

@ -146,7 +146,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
!isNaN(stateObj.attributes.current_temperature) !isNaN(stateObj.attributes.current_temperature)
? svg`${formatNumber( ? svg`${formatNumber(
stateObj.attributes.current_temperature, stateObj.attributes.current_temperature,
this.hass!.language this.hass.locale
)} )}
<tspan dx="-3" dy="-6.5" style="font-size: 4px;"> <tspan dx="-3" dy="-6.5" style="font-size: 4px;">
${this.hass.config.unit_system.temperature} ${this.hass.config.unit_system.temperature}
@ -169,31 +169,31 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
: Array.isArray(this._setTemp) : Array.isArray(this._setTemp)
? this._stepSize === 1 ? this._stepSize === 1
? svg` ? svg`
${formatNumber(this._setTemp[0], this.hass!.language, { ${formatNumber(this._setTemp[0], this.hass.locale, {
maximumFractionDigits: 0, maximumFractionDigits: 0,
})} - })} -
${formatNumber(this._setTemp[1], this.hass!.language, { ${formatNumber(this._setTemp[1], this.hass.locale, {
maximumFractionDigits: 0, maximumFractionDigits: 0,
})} })}
` `
: svg` : svg`
${formatNumber(this._setTemp[0], this.hass!.language, { ${formatNumber(this._setTemp[0], this.hass.locale, {
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} - })} -
${formatNumber(this._setTemp[1], this.hass!.language, { ${formatNumber(this._setTemp[1], this.hass.locale, {
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
` `
: this._stepSize === 1 : this._stepSize === 1
? svg` ? svg`
${formatNumber(this._setTemp, this.hass!.language, { ${formatNumber(this._setTemp, this.hass.locale, {
maximumFractionDigits: 0, maximumFractionDigits: 0,
})} })}
` `
: svg` : svg`
${formatNumber(this._setTemp, this.hass!.language, { ${formatNumber(this._setTemp, this.hass.locale, {
minimumFractionDigits: 1, minimumFractionDigits: 1,
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}

View File

@ -219,7 +219,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
<div class="name"> <div class="name">
@ -230,7 +230,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
<div class="temp"> <div class="temp">
${formatNumber( ${formatNumber(
stateObj.attributes.temperature, stateObj.attributes.temperature,
this.hass!.language this.hass.locale
)}&nbsp;<span>${getWeatherUnit(this.hass, "temperature")}</span> )}&nbsp;<span>${getWeatherUnit(this.hass, "temperature")}</span>
</div> </div>
<div class="attribute"> <div class="attribute">
@ -260,7 +260,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
stateObj.attributes[ stateObj.attributes[
this._config.secondary_info_attribute this._config.secondary_info_attribute
], ],
this.hass!.language this.hass.locale
)} )}
${getWeatherUnit( ${getWeatherUnit(
this.hass, this.hass,
@ -298,7 +298,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
? html` ? html`
${formatTime( ${formatTime(
new Date(item.datetime), new Date(item.datetime),
this.hass!.language this.hass!.locale
)} )}
` `
: html` : html`
@ -325,7 +325,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
<div class="temp"> <div class="temp">
${formatNumber( ${formatNumber(
item.temperature, item.temperature,
this.hass!.language this.hass!.locale
)}° )}°
</div> </div>
` `
@ -333,10 +333,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${item.templow !== undefined && item.templow !== null ${item.templow !== undefined && item.templow !== null
? html` ? html`
<div class="templow"> <div class="templow">
${formatNumber( ${formatNumber(item.templow, this.hass!.locale)}°
item.templow,
this.hass!.language
)}°
</div> </div>
` `
: ""} : ""}

View File

@ -15,7 +15,7 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
if ( if (
oldHass.connected !== element.hass!.connected || oldHass.connected !== element.hass!.connected ||
oldHass.themes !== element.hass!.themes || oldHass.themes !== element.hass!.themes ||
oldHass.language !== element.hass!.language || oldHass.locale !== element.hass!.locale ||
oldHass.localize !== element.hass.localize || oldHass.localize !== element.hass.localize ||
oldHass.config.state !== element.hass.config.state oldHass.config.state !== element.hass.config.state
) { ) {

View File

@ -11,10 +11,13 @@ import { formatDate } from "../../../common/datetime/format_date";
import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatDateTime } from "../../../common/datetime/format_date_time";
import { formatTime } from "../../../common/datetime/format_time"; import { formatTime } from "../../../common/datetime/format_time";
import relativeTime from "../../../common/datetime/relative_time"; import relativeTime from "../../../common/datetime/relative_time";
import { FrontendTranslationData } from "../../../data/translation";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { TimestampRenderingFormats } from "./types"; import { TimestampRenderingFormats } from "./types";
const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = { const FORMATS: {
[key: string]: (ts: Date, lang: FrontendTranslationData) => string;
} = {
date: formatDate, date: formatDate,
datetime: formatDateTime, datetime: formatDateTime,
time: formatTime, time: formatTime,
@ -64,7 +67,7 @@ class HuiTimestampDisplay extends LitElement {
return html` ${this._relative} `; return html` ${this._relative} `;
} }
if (format in FORMATS) { if (format in FORMATS) {
return html` ${FORMATS[format](this.ts, this.hass.language)} `; return html` ${FORMATS[format](this.ts, this.hass.locale)} `;
} }
return html`${this.hass.localize( return html`${this.hass.localize(
"ui.panel.lovelace.components.timestamp-display.invalid_format" "ui.panel.lovelace.components.timestamp-display.invalid_format"

View File

@ -147,7 +147,7 @@ export class HuiCardPicker extends LitElement {
return true; return true;
} }
if (oldHass.language !== this.hass!.language) { if (oldHass.locale !== this.hass!.locale) {
return true; return true;
} }

View File

@ -85,11 +85,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
)} )}
> >
${this._config.prefix}${!this._config.attribute ${this._config.prefix}${!this._config.attribute
? computeStateDisplay( ? computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)
this.hass.localize,
stateObj,
this.hass.language
)
: stateObj.attributes[this._config.attribute]}${this._config.suffix} : stateObj.attributes[this._config.attribute]}${this._config.suffix}
</div> </div>
`; `;

View File

@ -76,7 +76,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow {
${computeStateDisplay( ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
`} `}

View File

@ -10,6 +10,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { formatNumber } from "../../../common/string/format_number";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-slider"; import "../../../components/ha-slider";
import { UNAVAILABLE_STATES } from "../../../data/entity"; import { UNAVAILABLE_STATES } from "../../../data/entity";
@ -88,7 +89,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
id="input" id="input"
></ha-slider> ></ha-slider>
<span class="state"> <span class="state">
${Number(stateObj.state)} ${formatNumber(Number(stateObj.state), this.hass.locale)}
${stateObj.attributes.unit_of_measurement} ${stateObj.attributes.unit_of_measurement}
</span> </span>
</div> </div>

View File

@ -136,7 +136,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
.hass=${this.hass} .hass=${this.hass}
.config=${this._config} .config=${this._config}
.secondaryText=${mediaDescription || .secondaryText=${mediaDescription ||
computeStateDisplay(this.hass.localize, stateObj, this.hass.language)} computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)}
> >
<div class="controls"> <div class="controls">
${supportsFeature(stateObj, SUPPORT_TURN_ON) && ${supportsFeature(stateObj, SUPPORT_TURN_ON) &&

View File

@ -84,7 +84,7 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
</hui-generic-entity-row> </hui-generic-entity-row>

View File

@ -76,7 +76,7 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
${computeStateDisplay( ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
</hui-generic-entity-row> </hui-generic-entity-row>

View File

@ -64,7 +64,7 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow {
${computeStateDisplay( ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.language this.hass!.locale
)} )}
</div> </div>
`} `}

View File

@ -16,6 +16,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon"; import { stateIcon } from "../../../common/entity/state_icon";
import { formatNumber } from "../../../common/string/format_number";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import { UNAVAILABLE_STATES } from "../../../data/entity"; import { UNAVAILABLE_STATES } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent } from "../../../data/lovelace";
@ -111,10 +112,13 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
? computeStateDisplay( ? computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.language this.hass.locale
) )
: html` : html`
${stateObj.attributes.temperature} ${formatNumber(
stateObj.attributes.temperature,
this.hass.locale
)}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, "temperature")}
`} `}
</div> </div>

View File

@ -68,7 +68,7 @@ class LovelacePanel extends LitElement {
if ( if (
this.lovelace && this.lovelace &&
this.hass && this.hass &&
this.lovelace.language !== this.hass.language this.lovelace.locale !== this.hass.locale
) { ) {
// language has been changed, rebuild UI // language has been changed, rebuild UI
this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode); this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode);
@ -285,7 +285,7 @@ class LovelacePanel extends LitElement {
mode, mode,
urlPath: this.urlPath, urlPath: this.urlPath,
editMode: this.lovelace ? this.lovelace.editMode : false, editMode: this.lovelace ? this.lovelace.editMode : false,
language: this.hass!.language, locale: this.hass!.locale,
enableFullEditMode: () => { enableFullEditMode: () => {
if (!editorLoaded) { if (!editorLoaded) {
editorLoaded = true; editorLoaded = true;

View File

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

View File

@ -214,7 +214,7 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
for (let i = 0; i < arrayLength; i++) { for (let i = 0; i < arrayLength; i++) {
const datetime = formatDateTime( const datetime = formatDateTime(
new Date(values[i].info.origtime * 1000), new Date(values[i].info.origtime * 1000),
this.hass.language this.hass.locale
); );
platformItems.push({ platformItems.push({
timestamp: datetime, timestamp: datetime,

View File

@ -50,6 +50,7 @@ class AdvancedModeRow extends LitElement {
private async _advancedToggled(ev) { private async _advancedToggled(ev) {
getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({ getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({
...this.coreUserData,
showAdvanced: ev.currentTarget.checked, showAdvanced: ev.currentTarget.checked,
}); });
} }

View File

@ -34,6 +34,7 @@ import "./ha-long-lived-access-tokens-card";
import "./ha-mfa-modules-card"; import "./ha-mfa-modules-card";
import "./ha-pick-dashboard-row"; import "./ha-pick-dashboard-row";
import "./ha-pick-language-row"; import "./ha-pick-language-row";
import "./ha-pick-number-format-row";
import "./ha-pick-theme-row"; import "./ha-pick-theme-row";
import "./ha-push-notifications-row"; import "./ha-push-notifications-row";
import "./ha-refresh-tokens-card"; import "./ha-refresh-tokens-card";
@ -100,6 +101,10 @@ class HaPanelProfile extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}
></ha-pick-language-row> ></ha-pick-language-row>
<ha-pick-number-format-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-number-format-row>
<ha-pick-theme-row <ha-pick-theme-row
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}

View File

@ -96,14 +96,14 @@ class HaPickLanguageRow extends LocalizeMixin(EventsMixin(PolymerElement)) {
// Only fire event if language was changed. This prevents select updates when // Only fire event if language was changed. This prevents select updates when
// responding to hass changes. // responding to hass changes.
if (newVal !== this.hass.language) { if (newVal !== this.hass.language) {
this.fire("hass-language-select", { language: newVal }); this.fire("hass-language-select", newVal);
} }
} }
ready() { ready() {
super.ready(); super.ready();
if (this.hass && this.hass.language) { if (this.hass && this.hass.locale && this.hass.locale.language) {
this.setLanguageSelection(this.hass.language); this.setLanguageSelection(this.hass.locale.language);
} }
} }
} }

View File

@ -0,0 +1,81 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../components/ha-card";
import "../../components/ha-paper-dropdown-menu";
import { HomeAssistant } from "../../types";
import "../../components/ha-settings-row";
import { formatNumber } from "../../common/string/format_number";
import { NumberFormat } from "../../data/translation";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-pick-number-format-row")
class NumberFormatRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
protected render(): TemplateResult {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize("ui.panel.profile.number_format.header")}
</span>
<span slot="description">
${this.hass.localize("ui.panel.profile.number_format.description")}
</span>
<ha-paper-dropdown-menu
label=${this.hass.localize(
"ui.panel.profile.number_format.dropdown_label"
)}
dynamic-align
.disabled=${this.hass.locale === undefined}
>
<paper-listbox
slot="dropdown-content"
.selected=${this.hass.locale.number_format}
@iron-select=${this._handleFormatSelection}
attr-for-selected="format"
>
${Object.values(NumberFormat).map((format) => {
const formattedNumber = formatNumber(1234567.89, {
language: this.hass.locale.language,
number_format: format,
});
const value = this.hass.localize(
`ui.panel.profile.number_format.formats.${format}`
);
const twoLine = value.slice(value.length - 2) !== "89"; // Display explicit number formats on one line
return html`
<paper-item .format=${format}>
<paper-item-body ?two-line=${twoLine}>
<div>${value}</div>
${twoLine
? html`<div secondary>${formattedNumber}</div>`
: ""}
</paper-item-body>
</paper-item>
`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row>
`;
}
private async _handleFormatSelection(ev: CustomEvent) {
fireEvent(this, "hass-number-format-select", ev.detail.item.format);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-pick-number-format-row": NumberFormatRow;
}
}

View File

@ -58,11 +58,7 @@ class StateCardConfigurator extends LocalizeMixin(PolymerElement) {
} }
_localizeState(stateObj) { _localizeState(stateObj) {
return computeStateDisplay( return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
this.hass.localize,
stateObj,
this.hass.language
);
} }
} }
customElements.define("state-card-configurator", StateCardConfigurator); customElements.define("state-card-configurator", StateCardConfigurator);

View File

@ -56,7 +56,7 @@ export class StateCardDisplay extends LitElement {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
this.stateObj, this.stateObj,
this.hass.language this.hass.locale
)} )}
</div> </div>
</div> </div>

View File

@ -86,7 +86,7 @@ class StateCardMediaPlayer extends LocalizeMixin(PolymerElement) {
computePrimaryText(localize, playerObj) { computePrimaryText(localize, playerObj) {
return ( return (
playerObj.primaryTitle || playerObj.primaryTitle ||
computeStateDisplay(localize, playerObj.stateObj, this.hass.language) computeStateDisplay(localize, playerObj.stateObj, this.hass.locale)
); );
} }
} }

View File

@ -13,6 +13,7 @@ import { broadcastConnectionStatus } from "../data/connection-status";
import { subscribeFrontendUserData } from "../data/frontend"; import { subscribeFrontendUserData } from "../data/frontend";
import { forwardHaptic } from "../data/haptics"; import { forwardHaptic } from "../data/haptics";
import { DEFAULT_PANEL } from "../data/panel"; import { DEFAULT_PANEL } from "../data/panel";
import { NumberFormat } from "../data/translation";
import { subscribePanels } from "../data/ws-panels"; import { subscribePanels } from "../data/ws-panels";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, ServiceCallResponse } from "../types"; import { Constructor, ServiceCallResponse } from "../types";
@ -27,6 +28,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
) => ) =>
class extends superClass { class extends superClass {
protected initializeHass(auth: Auth, conn: Connection) { protected initializeHass(auth: Auth, conn: Connection) {
const language = getLocalLanguage();
this.hass = { this.hass = {
auth, auth,
connection: conn, connection: conn,
@ -39,8 +42,12 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
user: null as any, user: null as any,
panelUrl: (this as any)._panelUrl, panelUrl: (this as any)._panelUrl,
defaultPanel: DEFAULT_PANEL, defaultPanel: DEFAULT_PANEL,
language: getLocalLanguage(), language,
selectedLanguage: null, selectedLanguage: null,
locale: {
language,
number_format: NumberFormat.language,
},
resources: null as any, resources: null as any,
localize: () => "", localize: () => "",

View File

@ -5,6 +5,7 @@ import { debounce } from "../common/util/debounce";
import { import {
getHassTranslations, getHassTranslations,
getHassTranslationsPre109, getHassTranslationsPre109,
NumberFormat,
saveTranslationPreferences, saveTranslationPreferences,
TranslationCategory, TranslationCategory,
} from "../data/translation"; } from "../data/translation";
@ -14,10 +15,22 @@ import { storeState } from "../util/ha-pref-storage";
import { import {
getTranslation, getTranslation,
getLocalLanguage, getLocalLanguage,
getUserLanguage, getUserLocale,
} from "../util/hass-translation"; } from "../util/hass-translation";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
declare global {
// for fire event
interface HASSDomEvents {
"hass-language-select": {
language: string;
};
"hass-number-format-select": {
number_format: NumberFormat;
};
}
}
interface LoadedTranslationCategory { interface LoadedTranslationCategory {
// individual integrations loaded for this category // individual integrations loaded for this category
integrations: string[]; integrations: string[];
@ -45,9 +58,12 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("hass-language-select", (e) => this.addEventListener("hass-language-select", (e) => {
this._selectLanguage((e as CustomEvent).detail.language, true) this._selectLanguage((e as CustomEvent).detail, true);
); });
this.addEventListener("hass-number-format-select", (e) => {
this._selectNumberFormat((e as CustomEvent).detail, true);
});
this._loadCoreTranslations(getLocalLanguage()); this._loadCoreTranslations(getLocalLanguage());
} }
@ -56,20 +72,31 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
if (!changedProps.has("hass")) { if (!changedProps.has("hass")) {
return; return;
} }
const oldHass = changedProps.get("hass"); const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (this.hass?.panels && oldHass.panels !== this.hass.panels) { if (
this.hass?.panels &&
(!oldHass || oldHass.panels !== this.hass.panels)
) {
this._loadFragmentTranslations(this.hass.language, this.hass.panelUrl); this._loadFragmentTranslations(this.hass.language, this.hass.panelUrl);
} }
} }
protected hassConnected() { protected hassConnected() {
super.hassConnected(); super.hassConnected();
getUserLanguage(this.hass!).then((language) => { getUserLocale(this.hass!).then((locale) => {
if (language && this.hass!.language !== language) { if (locale?.language && this.hass!.language !== locale.language) {
// We just get language from backend, no need to save back // We just got language from backend, no need to save back
this._selectLanguage(language, false); this._selectLanguage(locale.language, false);
}
if (
locale?.number_format &&
this.hass!.locale.number_format !== locale.number_format
) {
// We just got number_format from backend, no need to save back
this._selectNumberFormat(locale.number_format, false);
} }
}); });
this.hass!.connection.subscribeEvents( this.hass!.connection.subscribeEvents(
debounce(() => { debounce(() => {
this._refetchCachedHassTranslations(false, false); this._refetchCachedHassTranslations(false, false);
@ -94,6 +121,18 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
); );
} }
private _selectNumberFormat(
number_format: NumberFormat,
saveToBackend: boolean
) {
this._updateHass({
locale: { ...this.hass!.locale, number_format: number_format },
});
if (saveToBackend) {
saveTranslationPreferences(this.hass!, this.hass!.locale);
}
}
private _selectLanguage(language: string, saveToBackend: boolean) { private _selectLanguage(language: string, saveToBackend: boolean) {
if (!this.hass) { if (!this.hass) {
// should not happen, do it to avoid use this.hass! // should not happen, do it to avoid use this.hass!
@ -101,10 +140,14 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
} }
// update selectedLanguage so that it can be saved to local storage // update selectedLanguage so that it can be saved to local storage
this._updateHass({ language, selectedLanguage: language }); this._updateHass({
locale: { ...this.hass!.locale, language: language },
language: language,
selectedLanguage: language,
});
storeState(this.hass); storeState(this.hass);
if (saveToBackend) { if (saveToBackend) {
saveTranslationPreferences(this.hass, { language }); saveTranslationPreferences(this.hass, this.hass.locale);
} }
this._applyTranslations(this.hass); this._applyTranslations(this.hass);
this._refetchCachedHassTranslations(true, true); this._refetchCachedHassTranslations(true, true);

View File

@ -3110,6 +3110,19 @@
"link_promo": "Help translating", "link_promo": "Help translating",
"dropdown_label": "Language" "dropdown_label": "Language"
}, },
"number_format": {
"header": "Number Format",
"dropdown_label": "Number format",
"description": "Choose how numbers are formatted.",
"formats": {
"language": "Auto (use language setting)",
"system": "Use system locale",
"comma_decimal": "1,234,567.89",
"decimal_comma": "1.234.567,89",
"space_comma": "1234567,89",
"none": "None"
}
},
"themes": { "themes": {
"header": "Theme", "header": "Theme",
"error_no_theme": "No themes available.", "error_no_theme": "No themes available.",

View File

@ -9,7 +9,10 @@ import {
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { LocalizeFunc } from "./common/translations/localize"; import { LocalizeFunc } from "./common/translations/localize";
import { CoreFrontendUserData } from "./data/frontend"; import { CoreFrontendUserData } from "./data/frontend";
import { getHassTranslations } from "./data/translation"; import {
FrontendTranslationData,
getHassTranslations,
} from "./data/translation";
import { Themes } from "./data/ws-themes"; import { Themes } from "./data/ws-themes";
import { ExternalMessaging } from "./external_app/external_messaging"; import { ExternalMessaging } from "./external_app/external_messaging";
@ -193,9 +196,8 @@ export interface HomeAssistant {
selectedTheme?: ThemeSettings | null; selectedTheme?: ThemeSettings | null;
panels: Panels; panels: Panels;
panelUrl: string; panelUrl: string;
// i18n // i18n
// current effective language, in that order: // current effective language in that order:
// - backend saved user selected lanugage // - backend saved user selected lanugage
// - language in local appstorage // - language in local appstorage
// - browser language // - browser language
@ -203,6 +205,7 @@ export interface HomeAssistant {
language: string; language: string;
// local stored language, keep that name for backward compability // local stored language, keep that name for backward compability
selectedLanguage: string | null; selectedLanguage: string | null;
locale: FrontendTranslationData;
resources: Resources; resources: Resources;
localize: LocalizeFunc; localize: LocalizeFunc;
translationMetadata: TranslationMetadata; translationMetadata: TranslationMetadata;

View File

@ -1,4 +1,7 @@
import { fetchTranslationPreferences } from "../data/translation"; import {
fetchTranslationPreferences,
FrontendTranslationData,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { getTranslation as commonGetTranslation } from "./common-translation"; import { getTranslation as commonGetTranslation } from "./common-translation";
@ -19,7 +22,7 @@ const LOCALE_LOOKUP = {
/** /**
* Search for a matching translation from most specific to general * Search for a matching translation from most specific to general
*/ */
function findAvailableLanguage(language: string) { export function findAvailableLanguage(language: string) {
// In most case, the language has the same format with our translation meta data // In most case, the language has the same format with our translation meta data
if (language in translationMetadata.translations) { if (language in translationMetadata.translations) {
return language; return language;
@ -39,18 +42,26 @@ function findAvailableLanguage(language: string) {
} }
/** /**
* Get user selected language from backend * Get user selected locale data from backend
*/ */
export async function getUserLanguage(hass: HomeAssistant) { export async function getUserLocale(
hass: HomeAssistant
): Promise<Partial<FrontendTranslationData>> {
const result = await fetchTranslationPreferences(hass); const result = await fetchTranslationPreferences(hass);
const language = result ? result.language : null; const language = result?.language;
const number_format = result?.number_format;
if (language) { if (language) {
const availableLanguage = findAvailableLanguage(language); const availableLanguage = findAvailableLanguage(language);
if (availableLanguage) { if (availableLanguage) {
return availableLanguage; return {
language: availableLanguage,
number_format,
};
} }
} }
return null; return {
number_format,
};
} }
/** /**

View File

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

View File

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

View File

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

View File

@ -1,6 +1,15 @@
import { assert } from "chai"; import { assert } from "chai";
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display"; import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
import { UNKNOWN } from "../../../src/data/entity"; import { UNKNOWN } from "../../../src/data/entity";
import {
FrontendTranslationData,
NumberFormat,
} from "../../../src/data/translation";
const localeData: FrontendTranslationData = {
language: "en",
number_format: NumberFormat.comma_decimal,
};
describe("computeStateDisplay", () => { describe("computeStateDisplay", () => {
// Mock Localize function for testing // Mock Localize function for testing
@ -14,7 +23,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"component.binary_sensor.state._.off" "component.binary_sensor.state._.off"
); );
}); });
@ -28,7 +37,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"component.binary_sensor.state.moisture.off" "component.binary_sensor.state.moisture.off"
); );
}); });
@ -48,7 +57,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"component.binary_sensor.state.invalid_device_class.off" "component.binary_sensor.state.invalid_device_class.off"
); );
}); });
@ -61,7 +70,10 @@ describe("computeStateDisplay", () => {
unit_of_measurement: "m", unit_of_measurement: "m",
}, },
}; };
assert.strictEqual(computeStateDisplay(localize, stateObj, "en"), "123 m"); assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData),
"123 m"
);
}); });
it("Localizes and formats numeric sensor value with units", () => { it("Localizes and formats numeric sensor value with units", () => {
@ -73,7 +85,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"1,234.5 m" "1,234.5 m"
); );
}); });
@ -93,7 +105,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"state.default.unknown" "state.default.unknown"
); );
}); });
@ -113,7 +125,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -131,7 +143,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"component.sensor.state._.custom_state" "component.sensor.state._.custom_state"
); );
}); });
@ -152,7 +164,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"November 18, 2017, 11:12 AM" "November 18, 2017, 11:12 AM"
); );
}); });
@ -173,7 +185,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"November 18, 2017" "November 18, 2017"
); );
}); });
@ -194,7 +206,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, "en"), computeStateDisplay(localize, stateObj, localeData),
"11:12 AM" "11:12 AM"
); );
}); });
@ -212,7 +224,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -228,7 +240,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, "en"), computeStateDisplay(altLocalize, stateObj, localeData),
"My Custom State" "My Custom State"
); );
}); });

View File

@ -0,0 +1,33 @@
import { assert } from "chai";
import { formatNumber } from "../../../src/common/string/format_number";
import { NumberFormat } 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, {
language: "en",
number_format: NumberFormat.language,
}),
"1,234.5"
);
});
it("Formats number with options", () => {
assert.strictEqual(
formatNumber(
1234.5,
{
language: "en",
number_format: NumberFormat.language,
},
{
minimumFractionDigits: 2,
}
),
"1,234.50"
);
});
});