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!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",
locale: this.hass.locale,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,

View File

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

View File

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

View File

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

View File

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

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 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() : "";
};
/**

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -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 {
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.language)}
.value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize(
"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 { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate";
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: String }) public language = "";
@property() public locale!: FrontendTranslationData;
@property() public label = "";
@ -90,7 +91,7 @@ export class Gauge extends LitElement {
</svg>
<svg class="text">
<text class="value-text">
${formatNumber(this.value, this.language)} ${this.label}
${formatNumber(this.value, this.locale)} ${this.label}
</text>
</svg>`;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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
)}
</ha-timeline>
`,
@ -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:
${(

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = (
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${formatNumber(value, hass!.language, { maximumFractionDigits: 1 })}
${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, attribute)}
`;
};

View File

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

View File

@ -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 {
<div>
${formatNumber(
this.stateObj.attributes.temperature,
this.hass!.language
this.hass.locale
)}
${getWeatherUnit(this.hass, "temperature")}
</div>
@ -106,7 +106,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.pressure,
this.hass!.language
this.hass.locale
)}
${getWeatherUnit(this.hass, "air_pressure")}
</div>
@ -123,7 +123,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.humidity,
this.hass!.language
this.hass.locale
)}
%
</div>
@ -157,7 +157,7 @@ class MoreInfoWeather extends LitElement {
<div>
${formatNumber(
this.stateObj.attributes.visibility,
this.hass!.language
this.hass.locale
)}
${getWeatherUnit(this.hass, "length")}
</div>
@ -184,7 +184,7 @@ class MoreInfoWeather extends LitElement {
<div class="main">
${formatTimeWeekday(
new Date(item.datetime),
this.hass.language
this.hass.locale
)}
</div>
`
@ -194,17 +194,17 @@ class MoreInfoWeather extends LitElement {
<div class="main">
${formatDateWeekday(
new Date(item.datetime),
this.hass.language
this.hass.locale
)}
</div>
<div class="templow">
${formatNumber(item.templow, this.hass!.language)}
${formatNumber(item.templow, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")}
</div>
`
: ""}
<div class="temp">
${formatNumber(item.temperature, this.hass!.language)}
${formatNumber(item.temperature, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")}
</div>
</div>

View File

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

View File

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

View File

@ -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: () => "",

View File

@ -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")}
</div>
@ -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(

View File

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

View File

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

View File

@ -48,7 +48,7 @@ class DialogCloudCertificate extends LitElement {
)}
${formatDateTime(
new Date(certificateInfo.expire_date),
this.hass!.language
this.hass!.locale
)}<br />
(${this.hass!.localize(
"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")) {
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;

View File

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

View File

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

View File

@ -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)
)}
<pre>${msg.payload}</pre>
<div class="bottom">

View File

@ -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) <br />
`
: ""}
Last logged:
${formatSystemLogTime(item.timestamp, this.hass!.language)}
${formatSystemLogTime(item.timestamp, this.hass!.locale)}
</p>
${item.message.length > 1
? html`

View File

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

View File

@ -1,12 +1,13 @@
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_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 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);
};

View File

@ -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")}
</div>

View File

@ -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)}:
<pre>${JSON.stringify(ev.event, null, 4)}</pre>
</div>
`

View File

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

View File

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

View File

@ -62,7 +62,7 @@ class HaLogbook extends LitElement {
protected shouldUpdate(changedProps: PropertyValues<this>) {
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`
<h4 class="date">
${formatDate(new Date(item.when), this.hass.language)}
${formatDate(new Date(item.when), this.hass.locale)}
</h4>
`
: html``}
@ -204,7 +204,7 @@ class HaLogbook extends LitElement {
<span
>${formatTimeWithSeconds(
new Date(item.when),
this.hass.language
this.hass.locale
)}</span
>
-

View File

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

View File

@ -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
)}
</span>`
: ""}

View File

@ -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
)}</span
>${showUnit
? html`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
)}
`}
></ha-icon-button>
@ -276,7 +276,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
: computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.language
this.hass!.locale
)}
</div>
`}

View File

@ -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
)}
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
${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,
})}

View File

@ -219,7 +219,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.language
this.hass.locale
)}
</div>
<div class="name">
@ -230,7 +230,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
<div class="temp">
${formatNumber(
stateObj.attributes.temperature,
this.hass!.language
this.hass.locale
)}&nbsp;<span>${getWeatherUnit(this.hass, "temperature")}</span>
</div>
<div class="attribute">
@ -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 {
<div class="temp">
${formatNumber(
item.temperature,
this.hass!.language
this.hass!.locale
)}°
</div>
`
@ -333,10 +333,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${item.templow !== undefined && item.templow !== null
? html`
<div class="templow">
${formatNumber(
item.templow,
this.hass!.language
)}°
${formatNumber(item.templow, this.hass!.locale)}°
</div>
`
: ""}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
></ha-slider>
<span class="state">
${Number(stateObj.state)}
${formatNumber(Number(stateObj.state), this.hass.locale)}
${stateObj.attributes.unit_of_measurement}
</span>
</div>

View File

@ -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)}
>
<div class="controls">
${supportsFeature(stateObj, SUPPORT_TURN_ON) &&

View File

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

View File

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

View File

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

View File

@ -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")}
`}
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
></ha-pick-language-row>
<ha-pick-number-format-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-number-format-row>
<ha-pick-theme-row
.narrow=${this.narrow}
.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
// responding to hass changes.
if (newVal !== this.hass.language) {
this.fire("hass-language-select", { language: newVal });
this.fire("hass-language-select", newVal);
}
}
ready() {
super.ready();
if (this.hass && this.hass.language) {
this.setLanguageSelection(this.hass.language);
if (this.hass && this.hass.locale && this.hass.locale.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) {
return computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.language
);
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
}
}
customElements.define("state-card-configurator", StateCardConfigurator);

View File

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

View File

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

View File

@ -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 = <T extends Constructor<HassBaseEl>>(
) =>
class extends superClass {
protected initializeHass(auth: Auth, conn: Connection) {
const language = getLocalLanguage();
this.hass = {
auth,
connection: conn,
@ -39,8 +42,12 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
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: () => "",

View File

@ -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 <T extends Constructor<HassBaseEl>>(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 <T extends Constructor<HassBaseEl>>(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 <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) {
if (!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
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);

View File

@ -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": "1234567,89",
"none": "None"
}
},
"themes": {
"header": "Theme",
"error_no_theme": "No themes available.",

View File

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

View File

@ -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<Partial<FrontendTranslationData>> {
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,
};
}
/**

View File

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

View File

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

View File

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

View File

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

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"
);
});
});