mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Create number formatting options on the profile panel (#7925)
This commit is contained in:
parent
0393970a80
commit
f43c420d59
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
|
@ -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() : "";
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)} %`;
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
)}
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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];
|
||||
|
@ -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:
|
||||
${(
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
);
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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)}
|
||||
`;
|
||||
};
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: () => "",
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
>`
|
||||
)}
|
||||
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -61,7 +61,7 @@ class MQTTMessages extends LitElement {
|
||||
Received
|
||||
${formatTimeWithSeconds(
|
||||
new Date(message.time),
|
||||
this.hass.language
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
${this._renderSingleMessage(message)}
|
||||
|
@ -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];
|
||||
|
@ -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">
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
`
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
>
|
||||
-
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>`
|
||||
: ""}
|
||||
|
@ -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`
|
||||
|
@ -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 ||
|
||||
|
@ -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>
|
||||
`
|
||||
|
@ -158,7 +158,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.language
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 = "";
|
||||
|
@ -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>
|
||||
`}
|
||||
|
@ -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,
|
||||
})}
|
||||
|
@ -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
|
||||
)} <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>
|
||||
`
|
||||
: ""}
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -76,7 +76,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow {
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.language
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
|
@ -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>
|
||||
|
@ -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) &&
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -64,7 +64,7 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow {
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.language
|
||||
this.hass!.locale
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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,
|
||||
|
@ -50,6 +50,7 @@ class AdvancedModeRow extends LitElement {
|
||||
|
||||
private async _advancedToggled(ev) {
|
||||
getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({
|
||||
...this.coreUserData,
|
||||
showAdvanced: ev.currentTarget.checked,
|
||||
});
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
src/panels/profile/ha-pick-number-format-row.ts
Normal file
81
src/panels/profile/ha-pick-number-format-row.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -56,7 +56,7 @@ export class StateCardDisplay extends LitElement {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
this.stateObj,
|
||||
this.hass.language
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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: () => "",
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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.",
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
33
test-mocha/common/string/format_number.ts
Normal file
33
test-mocha/common/string/format_number.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user