Merge pull request #10190 from home-assistant/dev

This commit is contained in:
Bram Kragten 2021-10-07 21:20:25 +02:00 committed by GitHub
commit 80bbc9990a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 378 additions and 294 deletions

View File

@ -29,6 +29,7 @@
"__BUILD__": false,
"__VERSION__": false,
"__STATIC_PATH__": false,
"__SUPERVISOR__": false,
"Polymer": true
},
"env": {

View File

@ -35,6 +35,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
@ -194,6 +195,9 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
defineOverlay: {
__SUPERVISOR__: true,
},
};
},

View File

@ -4,6 +4,7 @@ import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import {
HassioAddonInfo,
@ -32,7 +33,7 @@ class HassioAddonRepositoryEl extends LitElement {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) =>
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
caseInsensitiveStringCompare(a.name, b.name)
);
}
);

View File

@ -297,10 +297,11 @@ class HassioAddonInfo extends LitElement {
})}
@click=${this._showMoreInfo}
id="rating"
.value=${this.addon.rating}
label="rating"
description=""
></ha-label-badge>
>
${this.addon.rating}
</ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge

View File

@ -1,7 +1,6 @@
import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-relative-time";
import "../../../src/components/ha-svg-icon";
import { HomeAssistant } from "../../../src/types";
@ -19,8 +18,6 @@ class HassioCardContent extends LitElement {
@property() public topbarClass?: string;
@property() public datetime?: string;
@property() public iconTitle?: string;
@property() public iconClass?: string;
@ -56,15 +53,6 @@ class HassioCardContent extends LitElement {
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
${this.datetime
? html`
<ha-relative-time
.hass=${this.hass}
class="addition"
.datetime=${this.datetime}
></ha-relative-time>
`
: undefined}
</div>
</div>
`;
@ -106,9 +94,6 @@ class HassioCardContent extends LitElement {
height: 2.4em;
line-height: 1.2em;
}
ha-relative-time {
display: block;
}
.icon_image img {
max-height: 40px;
max-width: 40px;

View File

@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { stringCompare } from "../../../src/common/string/compare";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
@ -33,7 +33,7 @@ class HassioAddons extends LitElement {
</ha-card>
`
: this.supervisor.supervisor.addons
.sort((a, b) => stringCompare(a.name, b.name))
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.map(
(addon) => html`
<ha-card .addon=${addon} @click=${this._addonTapped}>

View File

@ -9,6 +9,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
@ -57,7 +58,7 @@ class HassioRepositoriesDialog extends LitElement {
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => (a.name < b.name ? -1 : 1))
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
);
protected render(): TemplateResult {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211006.0",
version="20211007.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",

View File

@ -0,0 +1,2 @@
export const capitalizeFirstLetter = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1);

View File

@ -4,7 +4,7 @@ import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-rel
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
import { getLocalLanguage } from "../../util/hass-translation";
import { getLocalLanguage } from "../../util/common-translation";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
interface FormatType {

View File

@ -86,6 +86,7 @@ export default class HaChartBase extends LitElement {
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
.title=${dataset.label}
>
<div
class="bullet"

View File

@ -1,4 +1,5 @@
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
css,
@ -27,7 +28,7 @@ import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-icon";
import "../ha-svg-icon";
import { filterData, sortData } from "./sort-filter";
declare global {
@ -311,11 +312,11 @@ export class HaDataTable extends LitElement {
>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
<ha-svg-icon
.path=${sorted && this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: ""}
<span>${column.title}</span>
@ -863,14 +864,14 @@ export class HaDataTable extends LitElement {
:host([dir="rtl"]) .mdc-data-table__header-cell > * {
transition: right 0.2s ease;
}
.mdc-data-table__header-cell ha-icon {
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
position: absolute;
}
.mdc-data-table__header-cell.not-sorted ha-icon {
.mdc-data-table__header-cell.not-sorted ha-svg-icon {
left: -20px;
}
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-icon {
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-svg-icon {
right: -20px;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
@ -886,16 +887,16 @@ export class HaDataTable extends LitElement {
left: auto;
right: 24px;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-svg-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-svg-icon {
left: 12px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable:not(.not-sorted)
ha-icon,
ha-svg-icon,
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable:hover.not-sorted
ha-icon {
ha-svg-icon {
left: auto;
right: 12px;
}

View File

@ -1,3 +1,4 @@
import { mdiAlert } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@ -14,11 +15,12 @@ import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { stateIcon } from "../../common/entity/state_icon";
import { timerTimeRemaining } from "../../data/timer";
import { formatNumber } from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-icon";
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@ -58,16 +60,20 @@ export class HaStateLabelBadge extends LitElement {
<ha-label-badge
class="warning"
label=${this.hass!.localize("state_badge.default.error")}
icon="hass:alert"
description=${this.hass!.localize(
"state_badge.default.entity_not_found"
)}
></ha-label-badge>
>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</ha-label-badge>
`;
}
const domain = computeStateDomain(entityState);
const value = this._computeValue(domain, entityState);
const icon = this.icon ? this.icon : this._computeIcon(domain, entityState);
return html`
<ha-label-badge
class=${classMap({
@ -75,8 +81,6 @@ export class HaStateLabelBadge extends LitElement {
"has-unit_of_measurement":
"unit_of_measurement" in entityState.attributes,
})}
.value=${this._computeValue(domain, entityState)}
.icon=${this.icon ? this.icon : this._computeIcon(domain, entityState)}
.image=${this.icon
? ""
: this.image
@ -88,8 +92,15 @@ export class HaStateLabelBadge extends LitElement {
entityState,
this._timerTimeRemaining
)}
.description=${this.name ? this.name : computeStateName(entityState)}
></ha-label-badge>
.description=${this.name ?? computeStateName(entityState)}
>
${icon ? html`<ha-icon .icon=${icon}></ha-icon>` : ""}
${value && (this.icon || !this.image)
? html`<span class=${value && value.length > 4 ? "big" : ""}
>${value}</span
>`
: ""}
</ha-label-badge>
`;
}
@ -208,7 +219,9 @@ export class HaStateLabelBadge extends LitElement {
:host {
cursor: pointer;
}
.big {
font-size: 70%;
}
ha-label-badge {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}

View File

@ -24,13 +24,15 @@ class StateInfo extends LitElement {
return html``;
}
const name = computeStateName(this.stateObj);
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
></state-badge>
<div class="info">
<div class="name" .inDialog=${this.inDialog}>
${computeStateName(this.stateObj)}
<div class="name" .title=${name} .inDialog=${this.inDialog}>
${name}
</div>
${this.inDialog
? html`<div class="time-ago">
@ -38,6 +40,7 @@ class StateInfo extends LitElement {
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
<div>
@ -92,7 +95,6 @@ class StateInfo extends LitElement {
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}

View File

@ -9,7 +9,6 @@ import {
unsafeCSS,
} from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-icon";
declare global {
// for fire event

View File

@ -4,7 +4,8 @@ import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
export const createCloseHeading = (
hass: HomeAssistant,

View File

@ -1,3 +1,4 @@
import { mdiMenuDown } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
@ -7,7 +8,7 @@ import "@polymer/paper-ripple/paper-ripple";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-icon";
import "../ha-svg-icon";
import {
HaFormElement,
HaFormMultiSelectData,
@ -56,11 +57,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
input-aria-haspopup="listbox"
autocomplete="off"
>
<ha-icon
icon="paper-dropdown-menu:arrow-drop-down"
<ha-svg-icon
.path=${mdiMenuDown}
suffix
slot="suffix"
></ha-icon>
></ha-svg-icon>
</paper-input>
</div>
<paper-listbox

View File

@ -361,7 +361,10 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
const chunks: Chunks = {};
checkCacheVersion();
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
if (!__SUPERVISOR__) {
checkCacheVersion();
}
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);

View File

@ -8,13 +8,9 @@ import {
} from "lit";
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-icon";
import "./ha-svg-icon";
class HaLabelBadge extends LitElement {
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public description?: string;
@ -25,20 +21,8 @@ class HaLabelBadge extends LitElement {
return html`
<div class="badge-container">
<div class="label-badge" id="badge">
<div
class=${classMap({
value: true,
big: Boolean(this.value && this.value.length > 4),
})}
>
<slot>
${this.icon && !this.value && !this.image
? html`<ha-icon .icon=${this.icon}></ha-icon>`
: ""}
${this.value && !this.image
? html`<span>${this.value}</span>`
: ""}
</slot>
<div class="value">
<slot></slot>
</div>
${this.label
? html`
@ -54,7 +38,7 @@ class HaLabelBadge extends LitElement {
: ""}
</div>
${this.description
? html` <div class="title">${this.description}</div> `
? html`<div class="title">${this.description}</div>`
: ""}
</div>
`;
@ -87,14 +71,15 @@ class HaLabelBadge extends LitElement {
background-size: cover;
transition: border 0.3s ease-in-out;
}
.label-badge .label.big span {
font-size: 90%;
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
}
.label-badge .value {
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
}
.label-badge .value.big {
font-size: 70%;
}
.label-badge .label {
position: absolute;
bottom: -1em;
@ -119,10 +104,6 @@ class HaLabelBadge extends LitElement {
transition: background-color 0.3s ease-in-out;
text-transform: var(--ha-label-badge-label-text-transform, uppercase);
}
.label-badge .label.big span {
font-size: 90%;
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
}
.badge-container .title {
margin-top: 1em;
font-size: var(--ha-label-badge-title-font-size, 0.9em);

View File

@ -1,6 +1,7 @@
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
import type { HomeAssistant } from "../types";
@customElement("ha-relative-time")
@ -9,6 +10,8 @@ class HaRelativeTime extends ReactiveElement {
@property({ attribute: false }) public datetime?: string | Date;
@property({ type: Boolean }) public capitalize = false;
private _interval?: number;
public disconnectedCallback(): void {
@ -55,7 +58,10 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
this.innerHTML = relativeTime(new Date(this.datetime), this.hass.locale);
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
}
}
}

View File

@ -10,8 +10,6 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-tab")
export class HaTab extends LitElement {

View File

@ -10,7 +10,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { fetchUsers, User } from "../../data/user";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./ha-user-badge";
class HaUserPicker extends LitElement {

View File

@ -490,7 +490,7 @@ export const reduceSumStatisticsByDay = (
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfMonth(addDays(new Date(values[0].start), -1)).toISOString(),
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
});
}
let lastValue: StatisticValue;
@ -546,7 +546,7 @@ export const reduceSumStatisticsByMonth = (
prevMonth = month;
}
if (prevMonth !== month) {
// Last value of the day
// Last value of the month
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),

View File

@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import "../../common/search/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next";
import { domainToName } from "../../data/integration";
@ -59,7 +60,7 @@ class StepFlowPickHandler extends LitElement {
return fuse.search(filter).map((result) => result.item);
}
return handlers.sort((a, b) =>
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
caseInsensitiveStringCompare(a.name, b.name)
);
}
);

View File

@ -25,6 +25,7 @@ class MoreInfoAutomation extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
</div>

View File

@ -28,6 +28,7 @@ class MoreInfoScript extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize("ui.components.relative_time.never")}

View File

@ -73,9 +73,6 @@ class MoreInfoSun extends LitElement {
display: inline-block;
white-space: nowrap;
}
ha-relative-time::first-letter {
text-transform: lowercase;
}
hr {
border-color: var(--divider-color);
border-bottom: none;

View File

@ -94,12 +94,14 @@ export class MoreInfoDialog extends LitElement {
}
const entityId = this._entityId;
const stateObj = this.hass.states[entityId];
const domain = computeDomain(entityId);
if (!stateObj) {
return html``;
}
const domain = computeDomain(entityId);
const name = computeStateName(stateObj);
return html`
<ha-dialog
open
@ -119,8 +121,13 @@ export class MoreInfoDialog extends LitElement {
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<div slot="title" class="main-title" @click=${this._enlarge}>
${computeStateName(stateObj)}
<div
slot="title"
class="main-title"
.title=${name}
@click=${this._enlarge}
>
${name}
</div>
${this.hass.user!.is_admin
? html`

View File

@ -31,6 +31,7 @@ export class HuiPersistentNotificationItem extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.notification.created_at}
capitalize
></ha-relative-time>
<paper-tooltip animation-delay="0">
${this._computeTooltip(this.hass, this.notification)}

View File

@ -25,7 +25,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
import { domainIcon } from "../../common/entity/domain_icon";
import { navigate } from "../../common/navigate";
import "../../common/search/search-input";
import { stringCompare } from "../../common/string/compare";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import {
fuzzyFilterSort,
ScorableTextItem,
@ -399,7 +399,7 @@ export class QuickBar extends LitElement {
};
})
.sort((a, b) =>
stringCompare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
);
}
@ -409,10 +409,7 @@ export class QuickBar extends LitElement {
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) =>
stringCompare(
a.strings.join(" ").toLowerCase(),
b.strings.join(" ").toLowerCase()
)
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
);
}

View File

@ -8,7 +8,7 @@ import { DEFAULT_PANEL } from "../data/panel";
import { NumberFormat, TimeFormat } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { getLocalLanguage, getTranslation } from "../util/common-translation";
import { demoConfig } from "./demo_config";
import { demoPanels } from "./demo_panels";
import { demoServices } from "./demo_services";

View File

@ -279,7 +279,7 @@ export class HaTabsSubpageDataTable extends LitElement {
margin-left: 4px;
font-size: 14px;
}
.active-filters ha-icon {
.active-filters ha-svg-icon {
color: var(--primary-color);
}
.active-filters mwc-button {

View File

@ -14,7 +14,6 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { LocalizeFunc } from "../common/translations/localize";
import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
@ -28,7 +27,6 @@ export interface PageNavigation {
name?: string;
core?: boolean;
advancedOnly?: boolean;
icon?: string;
iconPath?: string;
info?: any;
}
@ -98,7 +96,7 @@ class HassTabsSubpage extends LitElement {
slot="icon"
.path=${page.iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="icon" .icon=${page.icon}></ha-icon>`}
: ""}
</ha-tab>
</a>
`

View File

@ -2,7 +2,7 @@ import { LitElement, PropertyValues } from "lit";
import { property } from "lit/decorators";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { getLocalLanguage, getTranslation } from "../util/common-translation";
const empty = () => "";

View File

@ -34,6 +34,8 @@ export class DialogEnergyGasSettings
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
@state() private _unit?: string;
@state() private _error?: string;
public async showDialog(
@ -55,6 +57,7 @@ export class DialogEnergyGasSettings
public closeDialog(): void {
this._params = undefined;
this._source = undefined;
this._unit = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -64,6 +67,14 @@ export class DialogEnergyGasSettings
return html``;
}
const unit =
this._unit ||
(this._params.unit === undefined
? "m³ or kWh"
: this._params.unit === "energy"
? "kWh"
: "m³");
return html`
<ha-dialog
open
@ -154,7 +165,8 @@ export class DialogEnergyGasSettings
include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
`ui.panel.config.energy.gas.dialog.cost_entity_input`
`ui.panel.config.energy.gas.dialog.cost_entity_input`,
{ unit }
)}
@value-changed=${this._priceEntityChanged}
></ha-entity-picker>`
@ -174,7 +186,8 @@ export class DialogEnergyGasSettings
${this._costs === "number"
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.energy.gas.dialog.cost_number_input`
`ui.panel.config.energy.gas.dialog.cost_number_input`,
{ unit }
)}
no-label-float
class="price-options"
@ -183,12 +196,7 @@ export class DialogEnergyGasSettings
.value=${this._source.number_energy_price}
@value-changed=${this._numberPriceChanged}
>
<span slot="suffix"
>${this.hass.localize(
`ui.panel.config.energy.gas.dialog.cost_number_suffix`,
{ currency: this.hass.config.currency }
)}</span
>
<span slot="suffix">${this.hass.config.currency}/${unit}</span>
</paper-input>`
: ""}
@ -239,6 +247,18 @@ export class DialogEnergyGasSettings
}
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) {
const entity = this.hass.states[ev.detail.value];
if (entity?.attributes.unit_of_measurement) {
// Wh is normalized to kWh by stats generation
this._unit =
entity.attributes.unit_of_measurement === "Wh"
? "kWh"
: entity.attributes.unit_of_measurement;
}
} else {
this._unit = undefined;
}
this._source = {
...this._source!,
stat_energy_from: ev.detail.value,

View File

@ -16,6 +16,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
import { configTabs } from "./zwave_js-config-router";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
@customElement("zwave_js-logs")
class ZWaveJSLogs extends SubscribeMixin(LitElement) {
@ -149,7 +150,7 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
setZWaveJSLogLevel(this.hass!, this.configEntryId, selected);
this._textarea!.value += `${this.hass.localize(
"ui.panel.config.zwave_js.logs.log_level_changed",
{ level: selected.charAt(0).toUpperCase() + selected.slice(1) }
{ level: capitalizeFirstLetter(selected) }
)}\n`;
}

View File

@ -79,6 +79,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
? html`<ha-relative-time
.hass=${this.hass}
.datetime=${tag.last_scanned_datetime}
capitalize
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tag.never_scanned")}
</div>`
@ -96,6 +97,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
? html`<ha-relative-time
.hass=${this.hass}
.datetime=${last_scanned_datetime}
capitalize
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tag.never_scanned")}
`,

View File

@ -81,14 +81,16 @@ class HaPanelDevStatistics extends LitElement {
issue.data
) || issue.type
)
: "No issues"}`,
: localize("ui.panel.developer-tools.tabs.statistics.no_issue")}`,
},
fix: {
title: "",
template: (_, data: any) =>
html`${data.issues
? html`<mwc-button @click=${this._fixIssue} .data=${data.issues}>
Fix issue
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
)}
</mwc-button>`
: ""}`,
width: "113px",

View File

@ -82,7 +82,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
<mwc-button slot="primaryAction" @click=${this._fixIssue}>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.fix"
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
)}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>

View File

@ -37,10 +37,10 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
heading="Unsupported unit in recorded statistics"
>
<p>
The unit of the statistics in your database for this entity is not a
supported unit for the device class of the entity,
${this._params.issue.data.device_class}. It should be
${this._params.issue.data.supported_unit}.
The unit ${this._params.issue.data.metadata_unit} of the statistics in
your database for this entity is not a supported unit for the device
class of the entity, ${this._params.issue.data.device_class}. It
should be ${this._params.issue.data.supported_unit}.
</p>
<p>
Do you want to update the unit of the history statistics to

View File

@ -210,6 +210,7 @@ class HaLogbook extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${item.when}
capitalize
></ha-relative-time>
${item.domain === "automation" &&
item.context_id! in this.traceContexts

View File

@ -1,6 +1,8 @@
import { mdiAlert } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../components/ha-label-badge";
import "../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../types";
import { LovelaceBadge } from "../types";
import { ErrorBadgeConfig } from "./types";
@ -32,11 +34,9 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
}
return html`
<ha-label-badge
label="Error"
icon="hass:alert"
description=${this._config.error}
></ha-label-badge>
<ha-label-badge label="Error" description=${this._config.error}>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</ha-label-badge>
`;
}

View File

@ -763,6 +763,8 @@ class HuiEnergyDistrubutionCard
stroke-width: 4px;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.gas path,
.gas circle {

View File

@ -10,7 +10,13 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import { differenceInDays, endOfToday, isToday, startOfToday } from "date-fns";
import {
addHours,
differenceInDays,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyGasGraphCardConfig } from "../types";
@ -39,6 +45,7 @@ import {
reduceSumStatisticsByMonth,
reduceSumStatisticsByDay,
} from "../../../../data/history";
import { formatTime } from "../../../../common/datetime/format_time";
@customElement("hui-energy-gas-graph-card")
export class HuiEnergyGasGraphCard
@ -180,6 +187,16 @@ export class HuiEnergyGasGraphCard
tooltip: {
mode: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${formatTime(date, locale)} - ${formatTime(
addHours(date, 1),
locale
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,

View File

@ -10,7 +10,13 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import { differenceInDays, endOfToday, isToday, startOfToday } from "date-fns";
import {
addHours,
differenceInDays,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
@ -40,6 +46,7 @@ import {
reduceSumStatisticsByMonth,
reduceSumStatisticsByDay,
} from "../../../../data/history";
import { formatTime } from "../../../../common/datetime/format_time";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@ -173,6 +180,16 @@ export class HuiEnergySolarGraphCard
tooltip: {
mode: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${formatTime(date, locale)} - ${formatTime(
addHours(date, 1),
locale
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,

View File

@ -1,5 +1,11 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { startOfToday, endOfToday, isToday, differenceInDays } from "date-fns";
import {
startOfToday,
endOfToday,
isToday,
differenceInDays,
addHours,
} from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
@ -13,6 +19,7 @@ import {
} from "../../../../common/color/convert-color";
import { hexBlend } from "../../../../common/color/hex";
import { labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
formatNumber,
@ -168,6 +175,16 @@ export class HuiEnergyUsageGraphCard
position: "nearest",
filter: (val) => val.formattedValue !== "0",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${formatTime(date, locale)} - ${formatTime(
addHours(date, 1),
locale
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
Math.abs(context.parsed.y),

View File

@ -143,6 +143,10 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config.show_name
? this._config.name || (stateObj ? computeStateName(stateObj) : "")
: "";
return html`
<ha-card
@action=${this._handleAction}
@ -186,12 +190,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
`
: ""}
${this._config.show_name
? html`
<span tabindex="-1">
${this._config.name ||
(stateObj ? computeStateName(stateObj) : "")}
</span>
`
? html`<span tabindex="-1" .title=${name}>${name}</span>`
: ""}
${this._config.show_state && stateObj
? html`<span class="state">

View File

@ -115,12 +115,12 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? this._config.attribute in stateObj.attributes
: !UNAVAILABLE_STATES.includes(stateObj.state);
const name = this._config.name || computeStateName(stateObj);
return html`
<ha-card @click=${this._handleClick} tabindex="0">
<div class="header">
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="name" .title=${name}>${name}</div>
<div class="icon">
<ha-icon
.icon=${this._config.icon || stateIcon(stateObj)}

View File

@ -44,9 +44,8 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
return css`
pre {
font-family: var(--code-font-family, monospace);
text-overflow: ellipsis;
white-space: break-spaces;
user-select: text;
overflow: hidden;
}
`;
}

View File

@ -119,6 +119,8 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config.name ?? computeStateName(stateObj);
// Use `stateObj.state` as value to keep formatting (e.g trailing zeros)
// for consistent value display across gauge, entity, entity-row, etc.
return html`
@ -138,9 +140,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
.needle=${this._config!.needle}
.levels=${this._config!.needle ? this._severityLevels() : undefined}
></ha-gauge>
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="name" .title=${name}>${name}</div>
</ha-card>
`;
}

View File

@ -278,6 +278,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
</div>`;
}
const name = entityConf.name ?? computeStateName(stateObj);
return html`
<div
class="entity"
@ -292,13 +294,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
)}
>
${this._config!.show_name
? html`
<div class="name">
${"name" in entityConf
? entityConf.name
: computeStateName(stateObj)}
</div>
`
? html` <div class="name" .title=${name}>${name}</div> `
: ""}
${this._config!.show_icon
? html`

View File

@ -173,7 +173,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
</div>
</div>
</div>
<div id="info">${name}</div>
<div id="info" .title=${name}>${name}</div>
</div>
</ha-card>
`;

View File

@ -96,6 +96,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
const brightness =
Math.round((stateObj.attributes.brightness / 255) * 100) || 0;
const name = this._config.name ?? computeStateName(stateObj);
return html`
<ha-card>
<mwc-icon-button
@ -145,7 +147,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
</div>
</div>
<div id="info">
<div id="info" .title=${name}>
${UNAVAILABLE_STATES.includes(stateObj.state)
? html`
<div>
@ -157,7 +159,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
</div>
`
: html` <div class="brightness">%</div> `}
${this._config.name || computeStateName(stateObj)}
${name}
</div>
</div>
</ha-card>

View File

@ -251,7 +251,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
</div>
</div>
</div>
<div id="info">
<div id="info" .title=${name}>
<div id="modes">
${(stateObj.attributes.hvac_modes || [])
.concat()

View File

@ -190,6 +190,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
}
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
const name = this._config.name ?? computeStateName(stateObj);
return html`
<ha-card
@ -221,9 +222,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
this.hass.locale
)}
</div>
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="name" .title=${name}>${name}</div>
</div>
<div class="temp-attribute">
<div class="temp">

View File

@ -54,6 +54,7 @@ class HuiGenericEntityRow extends LitElement {
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
const hasSecondary = this.secondaryText || this.config.secondary_info;
const name = this.config.name ?? computeStateName(stateObj);
return html`
<state-badge
@ -82,8 +83,9 @@ class HuiGenericEntityRow extends LitElement {
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
.title=${name}
>
${this.config.name || computeStateName(stateObj)}
${name}
${hasSecondary
? html`
<div class="secondary">
@ -95,6 +97,7 @@ class HuiGenericEntityRow extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
@ -102,6 +105,7 @@ class HuiGenericEntityRow extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-triggered"
@ -110,6 +114,7 @@ class HuiGenericEntityRow extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize(

View File

@ -103,6 +103,7 @@ export class HuiEntityPickerTable extends LitElement {
<ha-relative-time
.hass=${this.hass!}
.datetime=${lastChanged}
capitalize
></ha-relative-time>
`,
};

View File

@ -63,7 +63,7 @@ export class HuiGenericEntityRowEditor
return html``;
}
const domain = computeDomain(this._config.entity);
const domain = computeDomain(this._entity);
return html`
<div class="card-config">

View File

@ -50,6 +50,9 @@ export class HuiButtonRow extends LitElement implements LovelaceRow {
? this.hass.states[this._config.entity]
: undefined;
const name =
this._config.name ?? (stateObj ? computeStateName(stateObj) : "");
return html`
<ha-icon
.icon=${this._config.icon ||
@ -57,9 +60,7 @@ export class HuiButtonRow extends LitElement implements LovelaceRow {
>
</ha-icon>
<div class="flex">
<div>
${this._config.name || (stateObj ? computeStateName(stateObj) : "")}
</div>
<div .title=${name}>${name}</div>
<mwc-button
@action=${this._handleAction}
.actionHandler=${actionHandler({

View File

@ -26,7 +26,11 @@ class HuiSectionRow extends LitElement implements LovelaceRow {
return html`
<div class="divider"></div>
${this._config.label
? html` <div class="label">${this._config.label}</div> `
? html`
<div class="label" .title=${this._config.label}>
${this._config.label}
</div>
`
: html``}
`;
}

View File

@ -22,8 +22,8 @@ class HuiTextRow extends LitElement implements LovelaceRow {
return html`
<ha-icon .icon=${this._config.icon}></ha-icon>
<div class="name">${this._config.name}</div>
<div class="text">${this._config.text}</div>
<div class="name" .title=${this._config.name}>${this._config.name}</div>
<div class="text" .title=${this._config.text}>${this._config.text}</div>
`;
}

View File

@ -36,7 +36,7 @@ class HuiWeblinkRow extends LitElement implements LovelaceRow {
?download=${this._config.download}
>
<ha-icon .icon=${this._config.icon}></ha-icon>
<div>${this._config.name}</div>
<div .title=${this._config.name}>${this._config.name}</div>
</a>
`;
}

View File

@ -22,7 +22,7 @@ import { Constructor, ServiceCallResponse } from "../types";
import { fetchWithAuth } from "../util/fetch-with-auth";
import { getState } from "../util/ha-pref-storage";
import hassCallApi from "../util/hass-call-api";
import { getLocalLanguage } from "../util/hass-translation";
import { getLocalLanguage } from "../util/common-translation";
import { HassBaseEl } from "./hass-base-mixin";
export const connectionMixin = <T extends Constructor<HassBaseEl>>(

View File

@ -17,7 +17,7 @@ import {
getLocalLanguage,
getTranslation,
getUserLocale,
} from "../util/hass-translation";
} from "../util/common-translation";
import { HassBaseEl } from "./hass-base-mixin";
declare global {
@ -376,3 +376,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
}
};
// Load selected translation into memory immediately so it is ready when Polymer
// initializes.
getTranslation(null, getLocalLanguage());

View File

@ -1045,10 +1045,9 @@
"cost_stat": "Use an entity tracking the total costs",
"cost_stat_input": "Total Costs Entity",
"cost_entity": "Use an entity with current price",
"cost_entity_input": "Entity with the current price",
"cost_entity_input": "Entity with the current price per {unit}",
"cost_number": "Use a static price",
"cost_number_input": "Price per m³",
"cost_number_suffix": "{currency}/m³"
"cost_number_input": "Price per {unit}"
}
},
"device_consumption": {
@ -3730,19 +3729,20 @@
"title": "Statistics",
"entity": "Entity",
"issue": "Issue",
"no_issue": "No issue",
"issues": {
"units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.",
"unsupported_unit_state": "The unit (''{state_unit}'') of this entity doesn't match a unit of device class ''{device_class}''.",
"unsupported_unit_metadata": "The unit (''{state_unit}'') of the recorded statistics doesn't match a unit of device class ''{device_class}''.",
"unsupported_unit_metadata": "The unit (''{metadata_unit}'') of the recorded statistics doesn't match the supported unit ''{supported_unit}'' of device class ''{device_class}''.",
"unsupported_state_class": "The state class ''{state_class}'' of this entity is not supported.",
"entity_not_recorded": "This entity is excluded from being recorded."
},
"fix_issue": {
"fix": "Fix issue",
"units_changed": {
"title": "The unit of this entity changed",
"update": "Update the historic statistic values from ''{metadata_unit}'' to ''{state_unit}''",
"clear": "Delete all old statistic data for this entity",
"fix": "Fix issue"
"clear": "Delete all old statistic data for this entity"
}
}
}

View File

@ -21,6 +21,7 @@ declare global {
var __VERSION__: string;
var __STATIC_PATH__: string;
var __BACKWARDS_COMPAT__: boolean;
var __SUPERVISOR__: boolean;
/* eslint-enable no-var, no-redeclare */
interface Window {

View File

@ -1,6 +1,12 @@
import {
fetchTranslationPreferences,
FrontendLocaleData,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
const DEFAULT_BASE_URL = "/static/translations";
const STORAGE = window.localStorage || {};
// Store loaded translations in memory so translations are available immediately
// when DOM is created in Polymer. Even a cache lookup creates noticeable latency.
@ -18,6 +24,108 @@ async function fetchTranslation(fingerprint: string, base_url: string) {
return response.json();
}
// Chinese locales need map to Simplified or Traditional Chinese
const LOCALE_LOOKUP = {
"zh-cn": "zh-Hans",
"zh-sg": "zh-Hans",
"zh-my": "zh-Hans",
"zh-tw": "zh-Hant",
"zh-hk": "zh-Hant",
"zh-mo": "zh-Hant",
zh: "zh-Hant", // all other Chinese locales map to Traditional Chinese
};
/**
* Search for a matching translation from most specific to general
*/
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;
}
// Perform case-insenstive comparison since browser isn't required to
// report languages with specific cases.
const langLower = language.toLowerCase();
if (langLower in LOCALE_LOOKUP) {
return LOCALE_LOOKUP[langLower];
}
const translation = Object.keys(translationMetadata.translations).find(
(lang) => lang.toLowerCase() === langLower
);
if (translation) {
return translation;
}
if (language.includes("-")) {
return findAvailableLanguage(language.split("-")[0]);
}
return undefined;
}
/**
* Get user selected locale data from backend
*/
export async function getUserLocale(
hass: HomeAssistant
): Promise<Partial<FrontendLocaleData>> {
const result = await fetchTranslationPreferences(hass);
const language = result?.language;
const number_format = result?.number_format;
const time_format = result?.time_format;
if (language) {
const availableLanguage = findAvailableLanguage(language);
if (availableLanguage) {
return {
language: availableLanguage,
number_format,
time_format,
};
}
}
return {
number_format,
time_format,
};
}
/**
* Get browser specific language
*/
export function getLocalLanguage() {
let language = null;
if (STORAGE.selectedLanguage) {
try {
const stored = JSON.parse(STORAGE.selectedLanguage);
if (stored) {
language = findAvailableLanguage(stored);
if (language) {
return language;
}
}
} catch (err: any) {
// Ignore parsing error.
}
}
if (navigator.languages) {
for (const locale of navigator.languages) {
language = findAvailableLanguage(locale);
if (language) {
return language;
}
}
}
language = findAvailableLanguage(navigator.language);
if (language) {
return language;
}
// Final fallback
return "en";
}
export async function getTranslation(
fragment: string | null,
language: string,

View File

@ -3,6 +3,7 @@ import { until } from "lit/directives/until";
import checkValidDate from "../common/datetime/check_valid_date";
import { formatDate } from "../common/datetime/format_date";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
import { isDate } from "../common/string/is_date";
import { isTimestamp } from "../common/string/is_timestamp";
import { HomeAssistant } from "../types";
@ -159,7 +160,7 @@ export function formatAttributeName(value: string): string {
.replace(/\bip\b/g, "IP")
.replace(/\bmac\b/g, "MAC")
.replace(/\bgps\b/g, "GPS");
return value.charAt(0).toUpperCase() + value.slice(1);
return capitalizeFirstLetter(value);
}
export function formatAttributeValue(

View File

@ -1,118 +0,0 @@
import {
fetchTranslationPreferences,
FrontendLocaleData,
} from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getTranslation as commonGetTranslation } from "./common-translation";
const STORAGE = window.localStorage || {};
// Chinese locales need map to Simplified or Traditional Chinese
const LOCALE_LOOKUP = {
"zh-cn": "zh-Hans",
"zh-sg": "zh-Hans",
"zh-my": "zh-Hans",
"zh-tw": "zh-Hant",
"zh-hk": "zh-Hant",
"zh-mo": "zh-Hant",
zh: "zh-Hant", // all other Chinese locales map to Traditional Chinese
};
/**
* Search for a matching translation from most specific to general
*/
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;
}
// Perform case-insenstive comparison since browser isn't required to
// report languages with specific cases.
const langLower = language.toLowerCase();
if (langLower in LOCALE_LOOKUP) {
return LOCALE_LOOKUP[langLower];
}
const translation = Object.keys(translationMetadata.translations).find(
(lang) => lang.toLowerCase() === langLower
);
if (translation) {
return translation;
}
if (language.includes("-")) {
return findAvailableLanguage(language.split("-")[0]);
}
return undefined;
}
/**
* Get user selected locale data from backend
*/
export async function getUserLocale(
hass: HomeAssistant
): Promise<Partial<FrontendLocaleData>> {
const result = await fetchTranslationPreferences(hass);
const language = result?.language;
const number_format = result?.number_format;
const time_format = result?.time_format;
if (language) {
const availableLanguage = findAvailableLanguage(language);
if (availableLanguage) {
return {
language: availableLanguage,
number_format,
time_format,
};
}
}
return {
number_format,
time_format,
};
}
/**
* Get browser specific language
*/
export function getLocalLanguage() {
let language = null;
if (STORAGE.selectedLanguage) {
try {
const stored = JSON.parse(STORAGE.selectedLanguage);
if (stored) {
language = findAvailableLanguage(stored);
if (language) {
return language;
}
}
} catch (err: any) {
// Ignore parsing error.
}
}
if (navigator.languages) {
for (const locale of navigator.languages) {
language = findAvailableLanguage(locale);
if (language) {
return language;
}
}
}
language = findAvailableLanguage(navigator.language);
if (language) {
return language;
}
// Final fallback
return "en";
}
export const getTranslation = (fragment: string | null, language: string) =>
commonGetTranslation(fragment, language);
// Load selected translation into memory immediately so it is ready when Polymer
// initializes.
commonGetTranslation(null, getLocalLanguage());