diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index b48609894b..be23073207 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -39,7 +39,7 @@ class HcLovelace extends LitElement { urlPath: this.urlPath!, enableFullEditMode: () => undefined, mode: "storage", - language: "en", + locale: this.hass.locale, saveConfig: async () => undefined, deleteConfig: async () => undefined, setEditMode: () => undefined, diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts index 050006fe7d..c7c02d38fe 100644 --- a/src/common/datetime/format_date.ts +++ b/src/common/datetime/format_date.ts @@ -1,9 +1,10 @@ import { format } from "fecha"; +import { FrontendTranslationData } from "../../data/translation"; import { toLocaleDateStringSupportsOptions } from "./check_options_support"; export const formatDate = toLocaleDateStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleDateString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleDateString(locales.language, { year: "numeric", month: "long", day: "numeric", @@ -11,8 +12,8 @@ export const formatDate = toLocaleDateStringSupportsOptions : (dateObj: Date) => format(dateObj, "longDate"); export const formatDateWeekday = toLocaleDateStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleDateString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleDateString(locales.language, { weekday: "long", month: "short", day: "numeric", diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index 3a20491ed5..d4b76a1da6 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -1,9 +1,10 @@ import { format } from "fecha"; +import { FrontendTranslationData } from "../../data/translation"; import { toLocaleStringSupportsOptions } from "./check_options_support"; export const formatDateTime = toLocaleStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleString(locales.language, { year: "numeric", month: "long", day: "numeric", @@ -13,8 +14,8 @@ export const formatDateTime = toLocaleStringSupportsOptions : (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm"); export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleString(locales.language, { year: "numeric", month: "long", day: "numeric", diff --git a/src/common/datetime/format_time.ts b/src/common/datetime/format_time.ts index 6b79f0c173..d2068f7638 100644 --- a/src/common/datetime/format_time.ts +++ b/src/common/datetime/format_time.ts @@ -1,17 +1,18 @@ import { format } from "fecha"; +import { FrontendTranslationData } from "../../data/translation"; import { toLocaleTimeStringSupportsOptions } from "./check_options_support"; export const formatTime = toLocaleTimeStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleTimeString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleTimeString(locales.language, { hour: "numeric", minute: "2-digit", }) : (dateObj: Date) => format(dateObj, "shortTime"); export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleTimeString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleTimeString(locales.language, { hour: "numeric", minute: "2-digit", second: "2-digit", @@ -19,8 +20,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions : (dateObj: Date) => format(dateObj, "mediumTime"); export const formatTimeWeekday = toLocaleTimeStringSupportsOptions - ? (dateObj: Date, locales: string) => - dateObj.toLocaleTimeString(locales, { + ? (dateObj: Date, locales: FrontendTranslationData) => + dateObj.toLocaleTimeString(locales.language, { weekday: "long", hour: "numeric", minute: "2-digit", diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 91b7a606dc..0ba0c36cb2 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,5 +1,6 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; +import { FrontendTranslationData } from "../../data/translation"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; @@ -10,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain"; export const computeStateDisplay = ( localize: LocalizeFunc, stateObj: HassEntity, - language: string, + locale: FrontendTranslationData, state?: string ): string => { const compareState = state !== undefined ? state : stateObj.state; @@ -20,7 +21,7 @@ export const computeStateDisplay = ( } if (stateObj.attributes.unit_of_measurement) { - return `${formatNumber(compareState, language)} ${ + return `${formatNumber(compareState, locale)} ${ stateObj.attributes.unit_of_measurement }`; } @@ -35,7 +36,7 @@ export const computeStateDisplay = ( stateObj.attributes.month - 1, stateObj.attributes.day ); - return formatDate(date, language); + return formatDate(date, locale); } if (!stateObj.attributes.has_date) { const now = new Date(); @@ -48,7 +49,7 @@ export const computeStateDisplay = ( stateObj.attributes.hour, stateObj.attributes.minute ); - return formatTime(date, language); + return formatTime(date, locale); } date = new Date( @@ -58,7 +59,7 @@ export const computeStateDisplay = ( stateObj.attributes.hour, stateObj.attributes.minute ); - return formatDateTime(date, language); + return formatDateTime(date, locale); } if (domain === "humidifier") { @@ -67,8 +68,9 @@ export const computeStateDisplay = ( } } - if (domain === "counter") { - return formatNumber(compareState, language); + // `counter` and `number` domains do not have a unit of measurement but should still use `formatNumber` + if (domain === "counter" || domain === "number") { + return formatNumber(compareState, locale); } return ( diff --git a/src/common/string/format_number.ts b/src/common/string/format_number.ts index 3dc345fbbc..0f171bd0ea 100644 --- a/src/common/string/format_number.ts +++ b/src/common/string/format_number.ts @@ -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 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 = ( num: string | number, - language: string, + locale?: FrontendTranslationData, options?: Intl.NumberFormatOptions ): 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() Number.isNaN = Number.isNaN || @@ -16,13 +38,27 @@ export const formatNumber = ( return typeof input === "number" && isNaN(input); }; - if (!Number.isNaN(Number(num)) && Intl) { - return new Intl.NumberFormat( - language, - getDefaultFormatOptions(num, options) - ).format(Number(num)); + if ( + !Number.isNaN(Number(num)) && + Intl && + locale?.number_format !== NumberFormat.none + ) { + 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() : ""; }; /** diff --git a/src/components/entity/ha-chart-base.js b/src/components/entity/ha-chart-base.js index a6ed78fe48..2ae4d7cf26 100644 --- a/src/components/entity/ha-chart-base.js +++ b/src/components/entity/ha-chart-base.js @@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors( return value; } const date = new Date(values[index].value); - return formatTime(date, this.hass.language); + return formatTime(date, this.hass.locale); } drawChart() { diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 86e4d899b1..340677ff41 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -116,12 +116,8 @@ export class HaStateLabelBadge extends LitElement { : state.state === UNKNOWN ? "-" : state.attributes.unit_of_measurement - ? formatNumber(state.state, this.hass!.language) - : computeStateDisplay( - this.hass!.localize, - state, - this.hass!.language - ); + ? formatNumber(state.state, this.hass!.locale) + : computeStateDisplay(this.hass!.localize, state, this.hass!.locale); } } diff --git a/src/components/entity/state-info.ts b/src/components/entity/state-info.ts index d493fb625d..0bb035cc7f 100644 --- a/src/components/entity/state-info.ts +++ b/src/components/entity/state-info.ts @@ -84,7 +84,7 @@ class StateInfo extends LitElement { } 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); } } diff --git a/src/components/ha-climate-state.ts b/src/components/ha-climate-state.ts index 19a8bc0b68..aac651d6af 100644 --- a/src/components/ha-climate-state.ts +++ b/src/components/ha-climate-state.ts @@ -53,14 +53,14 @@ class HaClimateState extends LitElement { if (this.stateObj.attributes.current_temperature != null) { return `${formatNumber( this.stateObj.attributes.current_temperature, - this.hass!.language + this.hass.locale )} ${this.hass.config.unit_system.temperature}`; } if (this.stateObj.attributes.current_humidity != null) { return `${formatNumber( this.stateObj.attributes.current_humidity, - this.hass!.language + this.hass.locale )} %`; } @@ -78,17 +78,17 @@ class HaClimateState extends LitElement { ) { return `${formatNumber( this.stateObj.attributes.target_temp_low, - this.hass!.language + this.hass.locale )}-${formatNumber( this.stateObj.attributes.target_temp_high, - this.hass!.language + this.hass.locale )} ${this.hass.config.unit_system.temperature}`; } if (this.stateObj.attributes.temperature != null) { return `${formatNumber( this.stateObj.attributes.temperature, - this.hass!.language + this.hass.locale )} ${this.hass.config.unit_system.temperature}`; } if ( @@ -97,17 +97,17 @@ class HaClimateState extends LitElement { ) { return `${formatNumber( this.stateObj.attributes.target_humidity_low, - this.hass!.language + this.hass.locale )}-${formatNumber( this.stateObj.attributes.target_humidity_high, - this.hass!.language + this.hass.locale )} %`; } if (this.stateObj.attributes.humidity != null) { return `${formatNumber( this.stateObj.attributes.humidity, - this.hass!.language + this.hass.locale )} %`; } diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index 91db9cab23..c3780244cd 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement { protected updated(changedProps: PropertyValues) { if (changedProps.has("hass")) { 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._rtlDirection = computeRTLDirection(this.hass); } @@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
{ @@ -29,7 +30,7 @@ export class Gauge extends LitElement { @property({ type: Number }) public value = 0; - @property({ type: String }) public language = ""; + @property() public locale!: FrontendTranslationData; @property() public label = ""; @@ -90,7 +91,7 @@ export class Gauge extends LitElement { - ${formatNumber(this.value, this.language)} ${this.label} + ${formatNumber(this.value, this.locale)} ${this.label} `; } diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index ffc5d57090..83502db730 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -245,7 +245,7 @@ class HaSidebar extends LitElement { hass.panelUrl !== oldHass.panelUrl || hass.user !== oldHass.user || hass.localize !== oldHass.localize || - hass.language !== oldHass.language || + hass.locale !== oldHass.locale || hass.states !== oldHass.states || hass.defaultPanel !== oldHass.defaultPanel ); @@ -281,7 +281,7 @@ class HaSidebar extends LitElement { } 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); } diff --git a/src/components/ha-water_heater-state.js b/src/components/ha-water_heater-state.js index 9a6a8c7610..f2047c8a6f 100644 --- a/src/components/ha-water_heater-state.js +++ b/src/components/ha-water_heater-state.js @@ -2,6 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import { computeStateDisplay } from "../common/entity/compute_state_display"; +import { formatNumber } from "../common/string/format_number"; import LocalizeMixin from "../mixins/localize-mixin"; /* @@ -55,21 +56,31 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) { computeTarget(hass, stateObj) { if (!hass || !stateObj) return null; // We're using "!= null" on purpose so that we match both null and undefined. + if ( stateObj.attributes.target_temp_low != 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) { - return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`; + return `${formatNumber( + stateObj.attributes.temperature, + this.hass.locale + )} ${hass.config.unit_system.temperature}`; } return ""; } _localizeState(stateObj) { - return computeStateDisplay(this.hass.localize, stateObj); + return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale); } } customElements.define("ha-water_heater-state", HaWaterHeaterState); diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js index 1b97a69b2c..b74f3feebd 100644 --- a/src/components/state-history-chart-line.js +++ b/src/components/state-history-chart-line.js @@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { const item = items[0]; const date = data.datasets[item.datasetIndex].data[item.index].x; - return formatDateTimeWithSeconds(date, this.hass.language); + return formatDateTimeWithSeconds(date, this.hass.locale); }; const chartOptions = { diff --git a/src/components/state-history-chart-timeline.js b/src/components/state-history-chart-timeline.js index 727e5af83d..12fe3ca416 100644 --- a/src/components/state-history-chart-timeline.js +++ b/src/components/state-history-chart-timeline.js @@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) { const formatTooltipLabel = (item, data) => { const values = data.datasets[item.datasetIndex].data[item.index]; - const start = formatDateTimeWithSeconds(values[0], this.hass.language); - const end = formatDateTimeWithSeconds(values[1], this.hass.language); + const start = formatDateTimeWithSeconds(values[0], this.hass.locale); + const end = formatDateTimeWithSeconds(values[1], this.hass.locale); const state = values[2]; return [state, start, end]; diff --git a/src/components/trace/hat-trace.ts b/src/components/trace/hat-trace.ts index 1e8b3aed88..da6ae4f372 100644 --- a/src/components/trace/hat-trace.ts +++ b/src/components/trace/hat-trace.ts @@ -366,7 +366,7 @@ export class HaAutomationTracer extends LitElement { Triggered by the ${this.trace.variables.trigger.description} at ${formatDateTimeWithSeconds( new Date(this.trace.timestamp.start), - this.hass.language + this.hass.locale )} `, @@ -433,7 +433,7 @@ export class HaAutomationTracer extends LitElement { ? html`Finished at ${formatDateTimeWithSeconds( new Date(this.trace.timestamp.finish), - this.hass.language + this.hass.locale )} (runtime: ${( diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts index 6c97c20baa..8ef204408e 100644 --- a/src/data/cached-history.ts +++ b/src/data/cached-history.ts @@ -54,7 +54,7 @@ export const getRecent = ( } const prom = fetchRecent(hass, entityId, startTime, endTime).then( - (stateHistory) => computeHistory(hass, stateHistory, localize, language), + (stateHistory) => computeHistory(hass, stateHistory, localize), (err) => { delete RECENT_CACHE[entityId]; throw err; @@ -140,12 +140,7 @@ export const getRecentWithCache = ( delete stateHistoryCache[cacheKey]; throw err; } - const stateHistory = computeHistory( - hass, - fetchedHistory, - localize, - language - ); + const stateHistory = computeHistory(hass, fetchedHistory, localize); if (appendingToCache) { mergeLine(stateHistory.line, cache.data.line); mergeTimeline(stateHistory.timeline, cache.data.timeline); diff --git a/src/data/history.ts b/src/data/history.ts index 7bab985c1c..c2fb363ebf 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -4,6 +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"; const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; const LINE_ATTRIBUTES_TO_KEEP = [ @@ -109,7 +110,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) => const processTimelineEntity = ( localize: LocalizeFunc, - language: string, + language: FrontendTranslationData, states: HassEntity[] ): TimelineEntity => { const data: TimelineState[] = []; @@ -203,8 +204,7 @@ const processLineChartEntities = ( export const computeHistory = ( hass: HomeAssistant, stateHistory: HassEntity[][], - localize: LocalizeFunc, - language: string + localize: LocalizeFunc ): HistoryResult => { const lineChartDevices: { [unit: string]: HassEntity[][] } = {}; const timelineDevices: TimelineEntity[] = []; @@ -235,7 +235,7 @@ export const computeHistory = ( if (!unit) { timelineDevices.push( - processTimelineEntity(localize, language, stateInfo) + processTimelineEntity(localize, hass.locale, stateInfo) ); } else if (unit in lineChartDevices) { lineChartDevices[unit].push(stateInfo); diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 27dd865df9..cc887d803e 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -314,7 +314,7 @@ export const getLogbookMessage = ( `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, "state", stateObj - ? computeStateDisplay(hass.localize, stateObj, hass.language, state) + ? computeStateDisplay(hass.localize, stateObj, hass.locale, state) : state ); }; diff --git a/src/data/translation.ts b/src/data/translation.ts index 4316242262..bc6603c5c2 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -1,8 +1,18 @@ import { HomeAssistant } from "../types"; 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 { language: string; + number_format: NumberFormat; } declare global { diff --git a/src/data/weather.ts b/src/data/weather.ts index a2eef98845..0014deb261 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -133,7 +133,7 @@ export const getWind = ( speed: string, bearing: string ): string => { - const speedText = `${formatNumber(speed, hass!.language)} ${getWeatherUnit( + const speedText = `${formatNumber(speed, hass.locale)} ${getWeatherUnit( hass!, "wind_speed" )}`; @@ -206,7 +206,7 @@ export const getSecondaryWeatherAttribute = ( ` : hass!.localize(`ui.card.weather.attributes.${attribute}`)} - ${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })} + ${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })} ${getWeatherUnit(hass!, attribute)} `; }; diff --git a/src/dialogs/more-info/controls/more-info-sun.ts b/src/dialogs/more-info/controls/more-info-sun.ts index b480045643..4819c9b486 100644 --- a/src/dialogs/more-info/controls/more-info-sun.ts +++ b/src/dialogs/more-info/controls/more-info-sun.ts @@ -50,7 +50,7 @@ class MoreInfoSun extends LitElement {
${formatTime( item === "ris" ? risingDate : settingDate, - this.hass.language + this.hass.locale )}
@@ -61,10 +61,7 @@ class MoreInfoSun extends LitElement { ${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
- ${formatNumber( - this.stateObj.attributes.elevation, - this.hass!.language - )} + ${formatNumber(this.stateObj.attributes.elevation, this.hass.locale)}
`; diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts index d750957fbc..323f327fd0 100644 --- a/src/dialogs/more-info/controls/more-info-weather.ts +++ b/src/dialogs/more-info/controls/more-info-weather.ts @@ -68,7 +68,7 @@ class MoreInfoWeather extends LitElement { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if ( !oldHass || - oldHass.language !== this.hass.language || + oldHass.locale !== this.hass.locale || oldHass.config.unit_system !== this.hass.config.unit_system ) { return true; @@ -91,7 +91,7 @@ class MoreInfoWeather extends LitElement {
${formatNumber( this.stateObj.attributes.temperature, - this.hass!.language + this.hass.locale )} ${getWeatherUnit(this.hass, "temperature")}
@@ -106,7 +106,7 @@ class MoreInfoWeather extends LitElement {
${formatNumber( this.stateObj.attributes.pressure, - this.hass!.language + this.hass.locale )} ${getWeatherUnit(this.hass, "air_pressure")}
@@ -123,7 +123,7 @@ class MoreInfoWeather extends LitElement {
${formatNumber( this.stateObj.attributes.humidity, - this.hass!.language + this.hass.locale )} %
@@ -157,7 +157,7 @@ class MoreInfoWeather extends LitElement {
${formatNumber( this.stateObj.attributes.visibility, - this.hass!.language + this.hass.locale )} ${getWeatherUnit(this.hass, "length")}
@@ -184,7 +184,7 @@ class MoreInfoWeather extends LitElement {
${formatTimeWeekday( new Date(item.datetime), - this.hass.language + this.hass.locale )}
` @@ -194,17 +194,17 @@ class MoreInfoWeather extends LitElement {
${formatDateWeekday( new Date(item.datetime), - this.hass.language + this.hass.locale )}
- ${formatNumber(item.templow, this.hass!.language)} + ${formatNumber(item.templow, this.hass.locale)} ${getWeatherUnit(this.hass, "temperature")}
` : ""}
- ${formatNumber(item.temperature, this.hass!.language)} + ${formatNumber(item.temperature, this.hass.locale)} ${getWeatherUnit(this.hass, "temperature")}
diff --git a/src/dialogs/notifications/configurator-notification-item.ts b/src/dialogs/notifications/configurator-notification-item.ts index a29f1db335..eb998f76be 100644 --- a/src/dialogs/notifications/configurator-notification-item.ts +++ b/src/dialogs/notifications/configurator-notification-item.ts @@ -42,7 +42,7 @@ export class HuiConfiguratorNotificationItem extends LitElement { >${computeStateDisplay( this.hass.localize, this.notification, - this.hass.language + this.hass.locale )} diff --git a/src/dialogs/notifications/persistent-notification-item.ts b/src/dialogs/notifications/persistent-notification-item.ts index cc5359f306..c70748ce30 100644 --- a/src/dialogs/notifications/persistent-notification-item.ts +++ b/src/dialogs/notifications/persistent-notification-item.ts @@ -9,6 +9,7 @@ import { property, TemplateResult, } from "lit-element"; +import { formatDateTime } from "../../common/datetime/format_date_time"; import "../../components/ha-markdown"; import "../../components/ha-relative-time"; import { PersistentNotification } from "../../data/persistent_notification"; @@ -92,13 +93,7 @@ export class HuiPersistentNotificationItem extends LitElement { } const d = new Date(notification.created_at!); - return d.toLocaleDateString(hass.language, { - year: "numeric", - month: "short", - day: "numeric", - minute: "numeric", - hour: "numeric", - }); + return formatDateTime(d, hass.locale); } } diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index dc1d29593a..dbbe930314 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -5,6 +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 { translationMetadata } from "../resources/translations-metadata"; import { HomeAssistant } from "../types"; import { getTranslation, getLocalLanguage } from "../util/hass-translation"; @@ -198,9 +199,12 @@ export const provideHass = ( }, panelUrl: "lovelace", defaultPanel: DEFAULT_PANEL, - language: localLanguage, selectedLanguage: localLanguage, + locale: { + language: localLanguage, + number_format: NumberFormat.language, + }, resources: null as any, localize: () => "", diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 8bce620971..3beebba9cd 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -55,7 +55,7 @@ class HaAutomationPicker extends LitElement { }); private _columns = memoizeOne( - (narrow: boolean, _language): DataTableColumnContainer => { + (narrow: boolean, _locale): DataTableColumnContainer => { const columns: DataTableColumnContainer = { toggle: { title: "", @@ -83,7 +83,7 @@ class HaAutomationPicker extends LitElement { ${automation.attributes.last_triggered ? formatDateTime( new Date(automation.attributes.last_triggered), - this.hass.language + this.hass.locale ) : this.hass.localize("ui.components.relative_time.never")} @@ -192,7 +192,7 @@ class HaAutomationPicker extends LitElement { back-path="/config" .route=${this.route} .tabs=${configSections.automation} - .columns=${this._columns(this.narrow, this.hass.language)} + .columns=${this._columns(this.narrow, this.hass.locale)} .data=${this._automations(this.automations)} id="entity_id" .noDataText=${this.hass.localize( diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts index 5f936c3e8d..b54a773009 100644 --- a/src/panels/config/automation/trace/ha-automation-trace.ts +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -85,7 +85,7 @@ export class HaAutomationTrace extends LitElement { html`` )} diff --git a/src/panels/config/cloud/account/cloud-account.js b/src/panels/config/cloud/account/cloud-account.js index 139921206d..cbe717df16 100644 --- a/src/panels/config/cloud/account/cloud-account.js +++ b/src/panels/config/cloud/account/cloud-account.js @@ -221,7 +221,7 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) { "{periodEnd}", formatDateTime( new Date(subInfo.plan_renewal_date * 1000), - this.hass.language + this.hass.locale ) ); } diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index 92ca9af54c..3a5e6811b2 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -48,7 +48,7 @@ class DialogCloudCertificate extends LitElement { )} ${formatDateTime( new Date(certificateInfo.expire_date), - this.hass!.language + this.hass!.locale )}
(${this.hass!.localize( "ui.panel.config.cloud.dialog_certificate.will_be_auto_renewed" diff --git a/src/panels/config/devices/device-detail/ha-device-automation-card.ts b/src/panels/config/devices/device-detail/ha-device-automation-card.ts index 7cb25de0c6..179d0a387d 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-card.ts @@ -49,8 +49,8 @@ export abstract class HaDeviceAutomationCard< if (changedProps.has("deviceId") || changedProps.has("automations")) { return true; } - const oldHass = changedProps.get("hass"); - if (!oldHass || this.hass.language !== oldHass.language) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.language !== this.hass.language) { return true; } return false; diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-messages.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-messages.ts index de55b224bd..549b4cfe00 100644 --- a/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-messages.ts +++ b/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-messages.ts @@ -61,7 +61,7 @@ class MQTTMessages extends LitElement { Received ${formatTimeWithSeconds( new Date(message.time), - this.hass.language + this.hass.locale )} ${this._renderSingleMessage(message)} diff --git a/src/panels/config/info/system-health-card.ts b/src/panels/config/info/system-health-card.ts index bda8d409fb..032467c31a 100644 --- a/src/panels/config/info/system-health-card.ts +++ b/src/panels/config/info/system-health-card.ts @@ -101,7 +101,7 @@ class SystemHealthCard extends LitElement { `} `; } else if (info.type === "date") { - value = formatDateTime(new Date(info.value), this.hass.language); + value = formatDateTime(new Date(info.value), this.hass.locale); } } else { value = domainInfo.info[key]; @@ -228,7 +228,7 @@ class SystemHealthCard extends LitElement { } else if (info.type === "failed") { value = `failed to load: ${info.error}`; } else if (info.type === "date") { - value = formatDateTime(new Date(info.value), this.hass.language); + value = formatDateTime(new Date(info.value), this.hass.locale); } } else { value = domainInfo.info[key]; diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts index 8818169cfb..532b8f7873 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts @@ -77,7 +77,7 @@ class MqttSubscribeCard extends LitElement { "topic", msg.message.topic, "time", - formatTime(msg.time, this.hass!.language) + formatTime(msg.time, this.hass!.locale) )}
${msg.payload}
diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts index c76e0894b7..4744303163 100644 --- a/src/panels/config/logs/dialog-system-log-detail.ts +++ b/src/panels/config/logs/dialog-system-log-detail.ts @@ -116,15 +116,12 @@ class DialogSystemLogDetail extends LitElement { ${item.count > 0 ? html` First occurred: - ${formatSystemLogTime( - item.first_occurred, - this.hass!.language - )} + ${formatSystemLogTime(item.first_occurred, this.hass!.locale)} (${item.count} occurrences)
` : ""} Last logged: - ${formatSystemLogTime(item.timestamp, this.hass!.language)} + ${formatSystemLogTime(item.timestamp, this.hass!.locale)}

${item.message.length > 1 ? html` diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts index e7e55c0087..b4a020014e 100644 --- a/src/panels/config/logs/system-log-card.ts +++ b/src/panels/config/logs/system-log-card.ts @@ -68,7 +68,7 @@ export class SystemLogCard extends LitElement {
${formatSystemLogTime( item.timestamp, - this.hass!.language + this.hass!.locale )} – ${html`( { +export const formatSystemLogTime = (date, locale: FrontendTranslationData) => { 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); return dateTimeDay < today - ? formatDateTimeWithSeconds(dateTime, language) - : formatTimeWithSeconds(dateTime, language); + ? formatDateTimeWithSeconds(dateTime, locale) + : formatTimeWithSeconds(dateTime, locale); }; diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 81a3d71f4d..88f3574fad 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -89,7 +89,7 @@ class HaScriptPicker extends LitElement { ${script.attributes.last_triggered ? formatDateTime( new Date(script.attributes.last_triggered), - this.hass.language + this.hass.locale ) : this.hass.localize("ui.components.relative_time.never")}
diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 7d8350d0de..5324b31acf 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -82,10 +82,7 @@ class EventSubscribeCard extends LitElement { "name", ev.id )} - ${formatTime( - new Date(ev.event.time_fired), - this.hass!.language - )}: + ${formatTime(new Date(ev.event.time_fired), this.hass!.locale)}:
${JSON.stringify(ev.event, null, 4)}
` diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js index 6ead7a103b..6eb56c8656 100644 --- a/src/panels/developer-tools/state/developer-tools-state.js +++ b/src/panels/developer-tools/state/developer-tools-state.js @@ -462,14 +462,14 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { lastChangedString(entity) { return formatDateTimeWithSeconds( new Date(entity.last_changed), - this.hass.language + this.hass.locale ); } lastUpdatedString(entity) { return formatDateTimeWithSeconds( new Date(entity.last_updated), - this.hass.language + this.hass.locale ); } diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index 80f9591547..ba0fc03517 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -181,8 +181,7 @@ class HaPanelHistory extends LitElement { this._stateHistory = computeHistory( this.hass, dateHistory, - this.hass.localize, - this.hass.language + this.hass.localize ); this._isLoading = false; } diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 514b838f0e..1fed1769b3 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -62,7 +62,7 @@ class HaLogbook extends LitElement { protected shouldUpdate(changedProps: PropertyValues) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const languageChanged = - oldHass === undefined || oldHass.language !== this.hass.language; + oldHass === undefined || oldHass.locale !== this.hass.locale; return ( changedProps.has("entries") || @@ -139,7 +139,7 @@ class HaLogbook extends LitElement { new Date(previous.when).toDateString()) ? html`

- ${formatDate(new Date(item.when), this.hass.language)} + ${formatDate(new Date(item.when), this.hass.locale)}

` : html``} @@ -204,7 +204,7 @@ class HaLogbook extends LitElement { ${formatTimeWithSeconds( new Date(item.when), - this.hass.language + this.hass.locale )} - diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 66b53d16a5..f63edb98cc 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -132,7 +132,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { if ( !oldHass || oldHass.themes !== this.hass!.themes || - oldHass.language !== this.hass!.language + oldHass.locale !== this.hass!.locale ) { return true; } diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index 8b36381c0e..2a0fc4dcc6 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -113,7 +113,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { if ( !oldHass || oldHass.themes !== this.hass!.themes || - oldHass.language !== this.hass!.language + oldHass.locale !== this.hass!.locale ) { return true; } @@ -196,7 +196,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { ${computeStateDisplay( this.hass.localize, stateObj, - this.hass.language + this.hass.locale )} ` : ""} diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 735aee9c90..7c8c1b9a36 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -129,11 +129,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { ? stateObj.attributes[this._config.attribute!] ?? this.hass.localize("state.default.unknown") : stateObj.attributes.unit_of_measurement - ? formatNumber(stateObj.state, this.hass!.language) + ? formatNumber(stateObj.state, this.hass.locale) : computeStateDisplay( this.hass.localize, stateObj, - this.hass.language + this.hass.locale )}${showUnit ? html` diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts index 092b48f294..a87bb5a9ba 100644 --- a/src/panels/lovelace/cards/hui-gauge-card.ts +++ b/src/panels/lovelace/cards/hui-gauge-card.ts @@ -130,7 +130,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { .min=${this._config.min!} .max=${this._config.max!} .value=${stateObj.state} - .language=${this.hass!.language} + .locale=${this.hass!.locale} .label=${this._config!.unit || this.hass?.states[this._config!.entity].attributes .unit_of_measurement || diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index daf60ca629..180cb8514b 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -134,7 +134,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { !this._configEntities || !oldHass || oldHass.themes !== this.hass!.themes || - oldHass.language !== this.hass!.language + oldHass.locale !== this.hass!.locale ) { return true; } @@ -301,7 +301,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { : computeStateDisplay( this.hass!.localize, stateObj, - this.hass!.language + this.hass!.locale )} ` diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index a1b49d9378..521cc5c7b8 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -158,7 +158,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { ${computeStateDisplay( this.hass.localize, stateObj, - this.hass.language + this.hass.locale )} ` diff --git a/src/panels/lovelace/cards/hui-logbook-card.ts b/src/panels/lovelace/cards/hui-logbook-card.ts index 78fa5026ff..29e7adbee8 100644 --- a/src/panels/lovelace/cards/hui-logbook-card.ts +++ b/src/panels/lovelace/cards/hui-logbook-card.ts @@ -101,7 +101,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { !this._configEntities || !oldHass || oldHass.themes !== this.hass!.themes || - oldHass.language !== this.hass!.language + oldHass.locale !== this.hass!.locale ) { return true; } diff --git a/src/panels/lovelace/cards/hui-picture-entity-card.ts b/src/panels/lovelace/cards/hui-picture-entity-card.ts index 49818e0b8a..0329de6ba8 100644 --- a/src/panels/lovelace/cards/hui-picture-entity-card.ts +++ b/src/panels/lovelace/cards/hui-picture-entity-card.ts @@ -125,7 +125,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { const state = computeStateDisplay( this.hass!.localize, stateObj, - this.hass.language + this.hass.locale ); let footer: TemplateResult | string = ""; diff --git a/src/panels/lovelace/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts index 3f9a9930bf..bc4b62419b 100644 --- a/src/panels/lovelace/cards/hui-picture-glance-card.ts +++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts @@ -118,7 +118,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { if ( !oldHass || oldHass.themes !== this.hass!.themes || - oldHass.language !== this.hass!.language + oldHass.locale !== this.hass!.locale ) { return true; } @@ -259,7 +259,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { ${computeStateName(stateObj)} : ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass!.language + this.hass!.locale )} `} > @@ -276,7 +276,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { : computeStateDisplay( this.hass!.localize, stateObj, - this.hass!.language + this.hass!.locale )} `} diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index c12cfc9a61..95843602ef 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -146,7 +146,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { !isNaN(stateObj.attributes.current_temperature) ? svg`${formatNumber( stateObj.attributes.current_temperature, - this.hass!.language + this.hass.locale )} ${this.hass.config.unit_system.temperature} @@ -169,31 +169,31 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { : Array.isArray(this._setTemp) ? this._stepSize === 1 ? svg` - ${formatNumber(this._setTemp[0], this.hass!.language, { + ${formatNumber(this._setTemp[0], this.hass.locale, { maximumFractionDigits: 0, })} - - ${formatNumber(this._setTemp[1], this.hass!.language, { + ${formatNumber(this._setTemp[1], this.hass.locale, { maximumFractionDigits: 0, })} ` : svg` - ${formatNumber(this._setTemp[0], this.hass!.language, { + ${formatNumber(this._setTemp[0], this.hass.locale, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} - - ${formatNumber(this._setTemp[1], this.hass!.language, { + ${formatNumber(this._setTemp[1], this.hass.locale, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} ` : this._stepSize === 1 ? svg` - ${formatNumber(this._setTemp, this.hass!.language, { + ${formatNumber(this._setTemp, this.hass.locale, { maximumFractionDigits: 0, })} ` : svg` - ${formatNumber(this._setTemp, this.hass!.language, { + ${formatNumber(this._setTemp, this.hass.locale, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index aca161b379..9d1a325759 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -219,7 +219,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ${computeStateDisplay( this.hass.localize, stateObj, - this.hass.language + this.hass.locale )}
@@ -230,7 +230,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${formatNumber( stateObj.attributes.temperature, - this.hass!.language + this.hass.locale )} ${getWeatherUnit(this.hass, "temperature")}
@@ -260,7 +260,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { stateObj.attributes[ this._config.secondary_info_attribute ], - this.hass!.language + this.hass.locale )} ${getWeatherUnit( this.hass, @@ -298,7 +298,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ? html` ${formatTime( new Date(item.datetime), - this.hass!.language + this.hass!.locale )} ` : html` @@ -325,7 +325,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${formatNumber( item.temperature, - this.hass!.language + this.hass!.locale )}°
` @@ -333,10 +333,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ${item.templow !== undefined && item.templow !== null ? html`
- ${formatNumber( - item.templow, - this.hass!.language - )}° + ${formatNumber(item.templow, this.hass!.locale)}°
` : ""} diff --git a/src/panels/lovelace/common/has-changed.ts b/src/panels/lovelace/common/has-changed.ts index db4b9de518..f054ece6ba 100644 --- a/src/panels/lovelace/common/has-changed.ts +++ b/src/panels/lovelace/common/has-changed.ts @@ -15,7 +15,7 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean { if ( oldHass.connected !== element.hass!.connected || oldHass.themes !== element.hass!.themes || - oldHass.language !== element.hass!.language || + oldHass.locale !== element.hass!.locale || oldHass.localize !== element.hass.localize || oldHass.config.state !== element.hass.config.state ) { diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts index 08bcfe8770..04663d12ae 100644 --- a/src/panels/lovelace/components/hui-timestamp-display.ts +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -11,10 +11,13 @@ 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 { HomeAssistant } 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, datetime: formatDateTime, time: formatTime, @@ -64,7 +67,7 @@ class HuiTimestampDisplay extends LitElement { return html` ${this._relative} `; } 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( "ui.panel.lovelace.components.timestamp-display.invalid_format" diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 48fbdecdfa..75c9a95bc5 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -147,7 +147,7 @@ export class HuiCardPicker extends LitElement { return true; } - if (oldHass.language !== this.hass!.language) { + if (oldHass.locale !== this.hass!.locale) { return true; } diff --git a/src/panels/lovelace/elements/hui-state-label-element.ts b/src/panels/lovelace/elements/hui-state-label-element.ts index 5c16451496..be5a114d7a 100644 --- a/src/panels/lovelace/elements/hui-state-label-element.ts +++ b/src/panels/lovelace/elements/hui-state-label-element.ts @@ -85,11 +85,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement { )} > ${this._config.prefix}${!this._config.attribute - ? computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.language - ) + ? computeStateDisplay(this.hass.localize, stateObj, this.hass.locale) : stateObj.attributes[this._config.attribute]}${this._config.suffix}
`; diff --git a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts index 3b256f629d..89e9813c3a 100644 --- a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts @@ -76,7 +76,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow { ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass.language + this.hass.locale )}
`} diff --git a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts index 8780106c10..344d5d4d06 100644 --- a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts @@ -10,6 +10,7 @@ import { PropertyValues, TemplateResult, } from "lit-element"; +import { formatNumber } from "../../../common/string/format_number"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import "../../../components/ha-slider"; import { UNAVAILABLE_STATES } from "../../../data/entity"; @@ -88,7 +89,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow { id="input" > - ${Number(stateObj.state)} + ${formatNumber(Number(stateObj.state), this.hass.locale)} ${stateObj.attributes.unit_of_measurement} diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts index 03a79c8bd5..f252ea2b88 100644 --- a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts @@ -136,7 +136,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { .hass=${this.hass} .config=${this._config} .secondaryText=${mediaDescription || - computeStateDisplay(this.hass.localize, stateObj, this.hass.language)} + computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)} >
${supportsFeature(stateObj, SUPPORT_TURN_ON) && diff --git a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts index d91e841d09..758c8570e3 100644 --- a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts @@ -84,7 +84,7 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow { : computeStateDisplay( this.hass!.localize, stateObj, - this.hass.language + this.hass.locale )}
diff --git a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts index 2296fa163b..1fb732ca5c 100644 --- a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts @@ -76,7 +76,7 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow { ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass.language + this.hass.locale )} diff --git a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts index a2b52c9115..08fb08f86f 100644 --- a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts @@ -64,7 +64,7 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow { ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass!.language + this.hass!.locale )} `} diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index dae998fb9f..1d4a23f185 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -16,6 +16,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateIcon } from "../../../common/entity/state_icon"; +import { formatNumber } from "../../../common/string/format_number"; import "../../../components/entity/state-badge"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import { ActionHandlerEvent } from "../../../data/lovelace"; @@ -111,10 +112,13 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { ? computeStateDisplay( this.hass.localize, stateObj, - this.hass.language + this.hass.locale ) : html` - ${stateObj.attributes.temperature} + ${formatNumber( + stateObj.attributes.temperature, + this.hass.locale + )} ${getWeatherUnit(this.hass, "temperature")} `} diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index c1cefca507..0d6b38716a 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -68,7 +68,7 @@ class LovelacePanel extends LitElement { if ( this.lovelace && this.hass && - this.lovelace.language !== this.hass.language + this.lovelace.locale !== this.hass.locale ) { // language has been changed, rebuild UI this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode); @@ -285,7 +285,7 @@ class LovelacePanel extends LitElement { mode, urlPath: this.urlPath, editMode: this.lovelace ? this.lovelace.editMode : false, - language: this.hass!.language, + locale: this.hass!.locale, enableFullEditMode: () => { if (!editorLoaded) { editorLoaded = true; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 292d8e00e7..17bd8bc465 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -3,6 +3,7 @@ import { LovelaceCardConfig, LovelaceConfig, } from "../../data/lovelace"; +import { FrontendTranslationData } from "../../data/translation"; import { Constructor, HomeAssistant } from "../../types"; import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types"; import { LovelaceHeaderFooterConfig } from "./header-footer/types"; @@ -20,7 +21,7 @@ export interface Lovelace { editMode: boolean; urlPath: string | null; mode: "generated" | "yaml" | "storage"; - language: string; + locale: FrontendTranslationData; enableFullEditMode: () => void; setEditMode: (editMode: boolean) => void; saveConfig: (newConfig: LovelaceConfig) => Promise; diff --git a/src/panels/mailbox/ha-panel-mailbox.js b/src/panels/mailbox/ha-panel-mailbox.js index 98e3e5aba6..1ebf9e4a88 100644 --- a/src/panels/mailbox/ha-panel-mailbox.js +++ b/src/panels/mailbox/ha-panel-mailbox.js @@ -214,7 +214,7 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) { for (let i = 0; i < arrayLength; i++) { const datetime = formatDateTime( new Date(values[i].info.origtime * 1000), - this.hass.language + this.hass.locale ); platformItems.push({ timestamp: datetime, diff --git a/src/panels/profile/ha-advanced-mode-row.ts b/src/panels/profile/ha-advanced-mode-row.ts index ed9a2a0222..4f5744e52a 100644 --- a/src/panels/profile/ha-advanced-mode-row.ts +++ b/src/panels/profile/ha-advanced-mode-row.ts @@ -50,6 +50,7 @@ class AdvancedModeRow extends LitElement { private async _advancedToggled(ev) { getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({ + ...this.coreUserData, showAdvanced: ev.currentTarget.checked, }); } diff --git a/src/panels/profile/ha-panel-profile.ts b/src/panels/profile/ha-panel-profile.ts index 7c007c8a3b..cca363678c 100644 --- a/src/panels/profile/ha-panel-profile.ts +++ b/src/panels/profile/ha-panel-profile.ts @@ -34,6 +34,7 @@ import "./ha-long-lived-access-tokens-card"; import "./ha-mfa-modules-card"; import "./ha-pick-dashboard-row"; import "./ha-pick-language-row"; +import "./ha-pick-number-format-row"; import "./ha-pick-theme-row"; import "./ha-push-notifications-row"; import "./ha-refresh-tokens-card"; @@ -100,6 +101,10 @@ class HaPanelProfile extends LitElement { .narrow=${this.narrow} .hass=${this.hass} > + + + ${this.hass.localize("ui.panel.profile.number_format.header")} + + + ${this.hass.localize("ui.panel.profile.number_format.description")} + + + + ${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` + + +
${value}
+ ${twoLine + ? html`
${formattedNumber}
` + : ""} +
+
+ `; + })} +
+
+ + `; + } + + 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; + } +} diff --git a/src/state-summary/state-card-configurator.js b/src/state-summary/state-card-configurator.js index 38ced37982..6240406ba4 100644 --- a/src/state-summary/state-card-configurator.js +++ b/src/state-summary/state-card-configurator.js @@ -58,11 +58,7 @@ class StateCardConfigurator extends LocalizeMixin(PolymerElement) { } _localizeState(stateObj) { - return computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.language - ); + return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale); } } customElements.define("state-card-configurator", StateCardConfigurator); diff --git a/src/state-summary/state-card-display.ts b/src/state-summary/state-card-display.ts index 23b31b8d6b..c89668cfe2 100755 --- a/src/state-summary/state-card-display.ts +++ b/src/state-summary/state-card-display.ts @@ -56,7 +56,7 @@ export class StateCardDisplay extends LitElement { : computeStateDisplay( this.hass!.localize, this.stateObj, - this.hass.language + this.hass.locale )} diff --git a/src/state-summary/state-card-media_player.js b/src/state-summary/state-card-media_player.js index 164e1b52cb..aae6990dda 100644 --- a/src/state-summary/state-card-media_player.js +++ b/src/state-summary/state-card-media_player.js @@ -86,7 +86,7 @@ class StateCardMediaPlayer extends LocalizeMixin(PolymerElement) { computePrimaryText(localize, playerObj) { return ( playerObj.primaryTitle || - computeStateDisplay(localize, playerObj.stateObj, this.hass.language) + computeStateDisplay(localize, playerObj.stateObj, this.hass.locale) ); } } diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index b8a03f756d..52164b59a0 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -13,6 +13,7 @@ import { broadcastConnectionStatus } from "../data/connection-status"; import { subscribeFrontendUserData } from "../data/frontend"; import { forwardHaptic } from "../data/haptics"; import { DEFAULT_PANEL } from "../data/panel"; +import { NumberFormat } from "../data/translation"; import { subscribePanels } from "../data/ws-panels"; import { translationMetadata } from "../resources/translations-metadata"; import { Constructor, ServiceCallResponse } from "../types"; @@ -27,6 +28,8 @@ export const connectionMixin = >( ) => class extends superClass { protected initializeHass(auth: Auth, conn: Connection) { + const language = getLocalLanguage(); + this.hass = { auth, connection: conn, @@ -39,8 +42,12 @@ export const connectionMixin = >( user: null as any, panelUrl: (this as any)._panelUrl, defaultPanel: DEFAULT_PANEL, - language: getLocalLanguage(), + language, selectedLanguage: null, + locale: { + language, + number_format: NumberFormat.language, + }, resources: null as any, localize: () => "", diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index 2258833b72..6c5bf64302 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -5,6 +5,7 @@ import { debounce } from "../common/util/debounce"; import { getHassTranslations, getHassTranslationsPre109, + NumberFormat, saveTranslationPreferences, TranslationCategory, } from "../data/translation"; @@ -14,10 +15,22 @@ import { storeState } from "../util/ha-pref-storage"; import { getTranslation, getLocalLanguage, - getUserLanguage, + getUserLocale, } from "../util/hass-translation"; 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 { // individual integrations loaded for this category integrations: string[]; @@ -45,9 +58,12 @@ export default >(superClass: T) => protected firstUpdated(changedProps) { super.firstUpdated(changedProps); - this.addEventListener("hass-language-select", (e) => - this._selectLanguage((e as CustomEvent).detail.language, true) - ); + this.addEventListener("hass-language-select", (e) => { + this._selectLanguage((e as CustomEvent).detail, true); + }); + this.addEventListener("hass-number-format-select", (e) => { + this._selectNumberFormat((e as CustomEvent).detail, true); + }); this._loadCoreTranslations(getLocalLanguage()); } @@ -56,20 +72,31 @@ export default >(superClass: T) => if (!changedProps.has("hass")) { return; } - const oldHass = changedProps.get("hass"); - if (this.hass?.panels && oldHass.panels !== this.hass.panels) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if ( + this.hass?.panels && + (!oldHass || oldHass.panels !== this.hass.panels) + ) { this._loadFragmentTranslations(this.hass.language, this.hass.panelUrl); } } protected hassConnected() { super.hassConnected(); - getUserLanguage(this.hass!).then((language) => { - if (language && this.hass!.language !== language) { - // We just get language from backend, no need to save back - this._selectLanguage(language, false); + getUserLocale(this.hass!).then((locale) => { + if (locale?.language && this.hass!.language !== locale.language) { + // We just got language from backend, no need to save back + 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( debounce(() => { this._refetchCachedHassTranslations(false, false); @@ -94,6 +121,18 @@ export default >(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) { if (!this.hass) { // should not happen, do it to avoid use this.hass! @@ -101,10 +140,14 @@ export default >(superClass: T) => } // 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); if (saveToBackend) { - saveTranslationPreferences(this.hass, { language }); + saveTranslationPreferences(this.hass, this.hass.locale); } this._applyTranslations(this.hass); this._refetchCachedHassTranslations(true, true); diff --git a/src/translations/en.json b/src/translations/en.json index 8433cebfbf..fd2fb79fba 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3110,6 +3110,19 @@ "link_promo": "Help translating", "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": "1 234 567,89", + "none": "None" + } + }, "themes": { "header": "Theme", "error_no_theme": "No themes available.", diff --git a/src/types.ts b/src/types.ts index 3c9e872e43..5b9b6ed980 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,10 @@ import { } from "home-assistant-js-websocket"; import { LocalizeFunc } from "./common/translations/localize"; import { CoreFrontendUserData } from "./data/frontend"; -import { getHassTranslations } from "./data/translation"; +import { + FrontendTranslationData, + getHassTranslations, +} from "./data/translation"; import { Themes } from "./data/ws-themes"; import { ExternalMessaging } from "./external_app/external_messaging"; @@ -193,9 +196,8 @@ export interface HomeAssistant { selectedTheme?: ThemeSettings | null; panels: Panels; panelUrl: string; - // i18n - // current effective language, in that order: + // current effective language in that order: // - backend saved user selected lanugage // - language in local appstorage // - browser language @@ -203,6 +205,7 @@ export interface HomeAssistant { language: string; // local stored language, keep that name for backward compability selectedLanguage: string | null; + locale: FrontendTranslationData; resources: Resources; localize: LocalizeFunc; translationMetadata: TranslationMetadata; diff --git a/src/util/hass-translation.ts b/src/util/hass-translation.ts index e0be32bd0a..694af2ee4e 100644 --- a/src/util/hass-translation.ts +++ b/src/util/hass-translation.ts @@ -1,4 +1,7 @@ -import { fetchTranslationPreferences } from "../data/translation"; +import { + fetchTranslationPreferences, + FrontendTranslationData, +} from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; import { HomeAssistant } from "../types"; import { getTranslation as commonGetTranslation } from "./common-translation"; @@ -19,7 +22,7 @@ const LOCALE_LOOKUP = { /** * 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 if (language in translationMetadata.translations) { 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> { const result = await fetchTranslationPreferences(hass); - const language = result ? result.language : null; + const language = result?.language; + const number_format = result?.number_format; if (language) { const availableLanguage = findAvailableLanguage(language); if (availableLanguage) { - return availableLanguage; + return { + language: availableLanguage, + number_format, + }; } } - return null; + return { + number_format, + }; } /** diff --git a/test-mocha/common/datetime/format_date.ts b/test-mocha/common/datetime/format_date.ts index dab6dd5188..851c18f99d 100644 --- a/test-mocha/common/datetime/format_date.ts +++ b/test-mocha/common/datetime/format_date.ts @@ -1,11 +1,18 @@ import { assert } from "chai"; import { formatDate } from "../../../src/common/datetime/format_date"; +import { NumberFormat } from "../../../src/data/translation"; describe("formatDate", () => { const dateObj = new Date(2017, 10, 18, 11, 12, 13, 1400); 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" + ); }); }); diff --git a/test-mocha/common/datetime/format_date_time.ts b/test-mocha/common/datetime/format_date_time.ts index 3cc331251e..beead0d442 100644 --- a/test-mocha/common/datetime/format_date_time.ts +++ b/test-mocha/common/datetime/format_date_time.ts @@ -4,13 +4,17 @@ import { formatDateTime, formatDateTimeWithSeconds, } from "../../../src/common/datetime/format_date_time"; +import { NumberFormat } from "../../../src/data/translation"; describe("formatDateTime", () => { const dateObj = new Date(2017, 10, 18, 11, 12, 13, 400); it("Formats English date times", () => { assert.strictEqual( - formatDateTime(dateObj, "en"), + formatDateTime(dateObj, { + language: "en", + number_format: NumberFormat.language, + }), "November 18, 2017, 11:12 AM" ); }); @@ -21,7 +25,10 @@ describe("formatDateTimeWithSeconds", () => { it("Formats English date times with seconds", () => { assert.strictEqual( - formatDateTimeWithSeconds(dateObj, "en"), + formatDateTimeWithSeconds(dateObj, { + language: "en", + number_format: NumberFormat.language, + }), "November 18, 2017, 11:12:13 AM" ); }); diff --git a/test-mocha/common/datetime/format_time.ts b/test-mocha/common/datetime/format_time.ts index 98dfbb0568..2b1ac03aba 100644 --- a/test-mocha/common/datetime/format_time.ts +++ b/test-mocha/common/datetime/format_time.ts @@ -4,12 +4,19 @@ import { formatTime, formatTimeWithSeconds, } from "../../../src/common/datetime/format_time"; +import { NumberFormat } from "../../../src/data/translation"; describe("formatTime", () => { const dateObj = new Date(2017, 10, 18, 11, 12, 13, 1400); 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); 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" + ); }); }); diff --git a/test-mocha/common/entity/compute_state_display.ts b/test-mocha/common/entity/compute_state_display.ts index b4822774f4..eb0157956f 100644 --- a/test-mocha/common/entity/compute_state_display.ts +++ b/test-mocha/common/entity/compute_state_display.ts @@ -1,6 +1,15 @@ import { assert } from "chai"; import { computeStateDisplay } from "../../../src/common/entity/compute_state_display"; 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", () => { // Mock Localize function for testing @@ -14,7 +23,7 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "component.binary_sensor.state._.off" ); }); @@ -28,7 +37,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "component.binary_sensor.state.moisture.off" ); }); @@ -48,7 +57,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "component.binary_sensor.state.invalid_device_class.off" ); }); @@ -61,7 +70,10 @@ describe("computeStateDisplay", () => { 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", () => { @@ -73,7 +85,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "1,234.5 m" ); }); @@ -93,7 +105,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "state.default.unknown" ); }); @@ -113,7 +125,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "state.default.unavailable" ); }); @@ -131,7 +143,7 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "component.sensor.state._.custom_state" ); }); @@ -152,7 +164,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "November 18, 2017, 11:12 AM" ); }); @@ -173,7 +185,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "November 18, 2017" ); }); @@ -194,7 +206,7 @@ describe("computeStateDisplay", () => { }, }; assert.strictEqual( - computeStateDisplay(localize, stateObj, "en"), + computeStateDisplay(localize, stateObj, localeData), "11:12 AM" ); }); @@ -212,7 +224,7 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "state.default.unavailable" ); }); @@ -228,7 +240,7 @@ describe("computeStateDisplay", () => { attributes: {}, }; assert.strictEqual( - computeStateDisplay(altLocalize, stateObj, "en"), + computeStateDisplay(altLocalize, stateObj, localeData), "My Custom State" ); }); diff --git a/test-mocha/common/string/format_number.ts b/test-mocha/common/string/format_number.ts new file mode 100644 index 0000000000..efd1461bc8 --- /dev/null +++ b/test-mocha/common/string/format_number.ts @@ -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" + ); + }); +});