diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts index ef5a895df0..8eaafb4ad3 100644 --- a/src/common/datetime/relative_time.ts +++ b/src/common/datetime/relative_time.ts @@ -1,4 +1,4 @@ -import { LocalizeFunc } from "../../mixins/localize-base-mixin"; +import { LocalizeFunc } from "../translations/localize"; /** * Calculate a string representing a date object as relative time from now. diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 9becd6a82e..4ca6fbae7e 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -3,7 +3,7 @@ import computeStateDomain from "./compute_state_domain"; import formatDateTime from "../datetime/format_date_time"; import formatDate from "../datetime/format_date"; import formatTime from "../datetime/format_time"; -import { LocalizeFunc } from "../../mixins/localize-base-mixin"; +import { LocalizeFunc } from "../translations/localize"; export default ( localize: LocalizeFunc, diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts new file mode 100644 index 0000000000..433e7882f8 --- /dev/null +++ b/src/common/translations/localize.ts @@ -0,0 +1,81 @@ +import IntlMessageFormat from "intl-messageformat/src/main"; +import { Resources } from "../../types"; + +export type LocalizeFunc = (key: string, ...args: any[]) => string; + +interface FormatType { + [format: string]: any; +} +export interface FormatsType { + number: FormatType; + date: FormatType; + time: FormatType; +} + +/** + * Adapted from Polymer app-localize-behavior. + * + * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/** + * Optional dictionary of user defined formats, as explained here: + * http://formatjs.io/guides/message-syntax/#custom-formats + * + * For example, a valid dictionary of formats would be: + * this.formats = { + * number: { USD: { style: 'currency', currency: 'USD' } } + * } + */ + +export const computeLocalize = ( + cache: any, + language: string, + resources: Resources, + formats?: FormatsType +): LocalizeFunc => { + // Everytime any of the parameters change, invalidate the strings cache. + cache._localizationCache = {}; + + return (key, ...args) => { + if (!key || !resources || !language || !resources[language]) { + return ""; + } + + // Cache the key/value pairs for the same language, so that we don't + // do extra work if we're just reusing strings across an application. + const translatedValue = resources[language][key]; + + if (!translatedValue) { + return ""; + } + + const messageKey = key + translatedValue; + let translatedMessage = cache._localizationCache[messageKey]; + + if (!translatedMessage) { + translatedMessage = new (IntlMessageFormat as any)( + translatedValue, + language, + formats + ); + cache._localizationCache[messageKey] = translatedMessage; + } + + const argObject = {}; + for (let i = 0; i < args.length; i += 2) { + argObject[args[i]] = args[i + 1]; + } + + try { + return translatedMessage.format(argObject); + } catch (err) { + return "Translation " + err; + } + }; +}; diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 86a5f3ad28..b8cf388d5c 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -15,7 +15,7 @@ import stateIcon from "../../common/entity/state_icon"; import timerTimeRemaining from "../../common/entity/timer_time_remaining"; import secondsToDuration from "../../common/datetime/seconds_to_duration"; import { fireEvent } from "../../common/dom/fire_event"; -import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin"; +import { HomeAssistant } from "../../types"; import "../ha-label-badge"; @@ -23,7 +23,8 @@ import "../ha-label-badge"; * @appliesMixin LocalizeMixin * @appliesMixin EventsMixin */ -export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) { +export class HaStateLabelBadge extends LitElement { + public hass?: HomeAssistant; public state?: HassEntity; private _connected?: boolean; private _updateRemaining?: number; @@ -108,7 +109,7 @@ export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) { default: return state.state === "unknown" ? "-" - : this.localize(`component.${domain}.state.${state.state}`) || + : this.hass!.localize(`component.${domain}.state.${state.state}`) || state.state; } } @@ -163,8 +164,8 @@ export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) { // the state translations that are truncated to fit within the badge label. Translations // are only added for device_tracker and alarm_control_panel. return ( - this.localize(`state_badge.${domain}.${state.state}`) || - this.localize(`state_badge.default.${state.state}`) || + this.hass!.localize(`state_badge.${domain}.${state.state}`) || + this.hass!.localize(`state_badge.default.${state.state}`) || state.state ); } diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts index a1b79fedb3..b74e212071 100644 --- a/src/data/cached-history.ts +++ b/src/data/cached-history.ts @@ -6,8 +6,8 @@ import { LineChartUnit, } from "./history"; import { HomeAssistant } from "../types"; -import { LocalizeFunc } from "../mixins/localize-base-mixin"; import { HassEntity } from "home-assistant-js-websocket"; +import { LocalizeFunc } from "../common/translations/localize"; interface CacheConfig { refresh: number; diff --git a/src/data/history.ts b/src/data/history.ts index 7699094454..3f2556522d 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -2,8 +2,8 @@ import computeStateName from "../common/entity/compute_state_name"; import computeStateDomain from "../common/entity/compute_state_domain"; import computeStateDisplay from "../common/entity/compute_state_display"; import { HassEntity } from "home-assistant-js-websocket"; -import { LocalizeFunc } from "../mixins/localize-base-mixin"; import { HomeAssistant } from "../types"; +import { LocalizeFunc } from "../common/translations/localize"; const DOMAINS_USE_LAST_UPDATED = ["climate", "water_heater"]; const LINE_ATTRIBUTES_TO_KEEP = [ diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index de409524af..2e7ce665bd 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -136,6 +136,7 @@ export const provideHass = ( language: getActiveTranslation(), resources: null as any, + localize: () => "", translationMetadata: translationMetadata as any, dockedSidebar: false, diff --git a/src/layouts/app/connection-mixin.js b/src/layouts/app/connection-mixin.js index bef29cbbbe..b4b794fc17 100644 --- a/src/layouts/app/connection-mixin.js +++ b/src/layouts/app/connection-mixin.js @@ -53,6 +53,7 @@ export default (superClass) => language: getActiveTranslation(), // If resources are already loaded, don't discard them resources: (this.hass && this.hass.resources) || null, + localize: () => "", translationMetadata: translationMetadata, dockedSidebar: false, diff --git a/src/layouts/app/disconnect-toast-mixin.ts b/src/layouts/app/disconnect-toast-mixin.ts index 5cbfe6b60d..649a955c47 100644 --- a/src/layouts/app/disconnect-toast-mixin.ts +++ b/src/layouts/app/disconnect-toast-mixin.ts @@ -25,13 +25,7 @@ export default (superClass: Constructor) => if (!this._discToast) { const el = document.createElement("ha-toast"); el.duration = 0; - // Temp. Somehow the localize func is not getting recalculated for - // this class. Manually generating one. Will be fixed when we move - // the localize function to the hass object. - const { language, resources } = this.hass!; - el.text = (this as any).__computeLocalize(language, resources)( - "ui.notification_toast.connection_lost" - ); + el.text = this.hass!.localize("ui.notification_toast.connection_lost"); this._discToast = el; this.shadowRoot!.appendChild(el as any); } diff --git a/src/layouts/app/translations-mixin.js b/src/layouts/app/translations-mixin.ts similarity index 57% rename from src/layouts/app/translations-mixin.js rename to src/layouts/app/translations-mixin.ts index 41ace43e74..c069fea240 100644 --- a/src/layouts/app/translations-mixin.js +++ b/src/layouts/app/translations-mixin.ts @@ -1,14 +1,17 @@ import { translationMetadata } from "../../resources/translations-metadata"; import { getTranslation } from "../../util/hass-translation"; import { storeState } from "../../util/ha-pref-storage"; +import { Constructor, LitElement } from "lit-element"; +import { HassBaseEl } from "./hass-base-mixin"; +import { computeLocalize } from "../../common/translations/localize"; /* * superClass needs to contain `this.hass` and `this._updateHass`. */ -export default (superClass) => +export default (superClass: Constructor) => class extends superClass { - firstUpdated(changedProps) { + protected firstUpdated(changedProps) { super.firstUpdated(changedProps); this.addEventListener("hass-language-select", (e) => this._selectLanguage(e) @@ -16,71 +19,78 @@ export default (superClass) => this._loadResources(); } - hassConnected() { + protected hassConnected() { super.hassConnected(); this._loadBackendTranslations(); } - hassReconnected() { + protected hassReconnected() { super.hassReconnected(); this._loadBackendTranslations(); } - panelUrlChanged(newPanelUrl) { + protected panelUrlChanged(newPanelUrl) { super.panelUrlChanged(newPanelUrl); this._loadTranslationFragment(newPanelUrl); } - async _loadBackendTranslations() { - if (!this.hass.language) return; + private async _loadBackendTranslations() { + const hass = this.hass; + if (!hass || !hass.language) { + return; + } - const language = this.hass.selectedLanguage || this.hass.language; + const language = hass.selectedLanguage || hass.language; - const { resources } = await this.hass.callWS({ + const { resources } = await hass.callWS({ type: "frontend/get_translations", language, }); // If we've switched selected languages just ignore this response - if ((this.hass.selectedLanguage || this.hass.language) !== language) + if ((hass.selectedLanguage || hass.language) !== language) { return; + } this._updateResources(language, resources); } - _loadTranslationFragment(panelUrl) { + private _loadTranslationFragment(panelUrl) { if (translationMetadata.fragments.includes(panelUrl)) { this._loadResources(panelUrl); } } - async _loadResources(fragment) { + private async _loadResources(fragment?) { const result = await getTranslation(fragment); this._updateResources(result.language, result.data); } - _updateResources(language, data) { + private _updateResources(language, data) { // Update the language in hass, and update the resources with the newly // loaded resources. This merges the new data on top of the old data for // this language, so that the full translation set can be loaded across // multiple fragments. - this._updateHass({ - language: language, - resources: { - [language]: Object.assign( - {}, - this.hass && this.hass.resources && this.hass.resources[language], - data - ), + const resources = { + [language]: { + ...(this.hass && + this.hass.resources && + this.hass.resources[language]), + ...data, }, + }; + this._updateHass({ + language, + resources, + localize: computeLocalize(this, language, resources), }); } - _selectLanguage(event) { + private _selectLanguage(event) { this._updateHass({ selectedLanguage: event.detail.language }); storeState(this.hass); this._loadResources(); this._loadBackendTranslations(); - this._loadTranslationFragment(this.hass.panelUrl); + this._loadTranslationFragment(this.hass!.panelUrl); } }; diff --git a/src/mixins/lit-localize-lite-mixin.ts b/src/mixins/lit-localize-lite-mixin.ts index 393cdb875c..8aacd8089c 100644 --- a/src/mixins/lit-localize-lite-mixin.ts +++ b/src/mixins/lit-localize-lite-mixin.ts @@ -4,10 +4,10 @@ import { PropertyDeclarations, PropertyValues, } from "lit-element"; -import { HomeAssistant } from "../types"; import { getActiveTranslation } from "../util/hass-translation"; -import { LocalizeFunc, LocalizeMixin } from "./localize-base-mixin"; import { localizeLiteBaseMixin } from "./localize-lite-base-mixin"; +import { computeLocalize } from "../common/translations/localize"; +import { LocalizeMixin } from "../types"; const empty = () => ""; @@ -22,12 +22,8 @@ export const litLocalizeLiteMixin = ( ): Constructor => // @ts-ignore class extends localizeLiteBaseMixin(superClass) { - protected hass?: HomeAssistant; - protected localize!: LocalizeFunc; - static get properties(): PropertyDeclarations { return { - hass: {}, localize: {}, language: {}, resources: {}, @@ -45,7 +41,11 @@ export const litLocalizeLiteMixin = ( public connectedCallback(): void { super.connectedCallback(); this._initializeLocalizeLite(); - this.localize = this.__computeLocalize(this.language, this.resources); + this.localize = computeLocalize( + this.constructor.prototype, + this.language, + this.resources + ); } public updated(changedProperties: PropertyValues) { @@ -54,7 +54,11 @@ export const litLocalizeLiteMixin = ( changedProperties.has("language") || changedProperties.has("resources") ) { - this.localize = this.__computeLocalize(this.language, this.resources); + this.localize = computeLocalize( + this.constructor.prototype, + this.language, + this.resources + ); } } }; diff --git a/src/mixins/lit-localize-mixin.ts b/src/mixins/lit-localize-mixin.ts index 742a60b76d..74fe20441f 100644 --- a/src/mixins/lit-localize-mixin.ts +++ b/src/mixins/lit-localize-mixin.ts @@ -1,15 +1,5 @@ -import { - Constructor, - LitElement, - PropertyDeclarations, - PropertyValues, -} from "lit-element"; -import { HomeAssistant } from "../types"; -import { - localizeBaseMixin, - LocalizeFunc, - LocalizeMixin, -} from "./localize-base-mixin"; +import { Constructor, LitElement } from "lit-element"; +import { HomeAssistant, LocalizeMixin } from "../types"; const empty = () => ""; @@ -17,61 +7,10 @@ export const hassLocalizeLitMixin = ( superClass: Constructor ): Constructor => // @ts-ignore - class extends localizeBaseMixin(superClass) { - protected hass?: HomeAssistant; - protected localize!: LocalizeFunc; + class extends superClass { + public hass?: HomeAssistant; - static get properties(): PropertyDeclarations { - return { - hass: {}, - localize: {}, - }; - } - - constructor() { - super(); - // This will prevent undefined errors if called before connected to DOM. - this.localize = empty; - } - - public connectedCallback(): void { - super.connectedCallback(); - - if (this.localize === empty) { - let language; - let resources; - if (this.hass) { - language = this.hass.language; - resources = this.hass.resources; - } - this.localize = this.__computeLocalize(language, resources); - } - } - - public updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - - if (!changedProperties.has("hass")) { - return; - } - - let oldLanguage; - let oldResources; - const hass = changedProperties.get("hass") as HomeAssistant; - if (hass) { - oldLanguage = hass.language; - oldResources = hass.resources; - } - - let language; - let resources; - if (this.hass) { - language = this.hass.language; - resources = this.hass.resources; - } - - if (oldLanguage !== language || oldResources !== resources) { - this.localize = this.__computeLocalize(language, resources); - } + get localize() { + return this.hass ? this.hass.localize : empty; } }; diff --git a/src/mixins/localize-base-mixin.ts b/src/mixins/localize-base-mixin.ts deleted file mode 100644 index a70821e6be..0000000000 --- a/src/mixins/localize-base-mixin.ts +++ /dev/null @@ -1,114 +0,0 @@ -import IntlMessageFormat from "intl-messageformat/src/main"; -import { HomeAssistant } from "../types"; - -/** - * Adapted from Polymer app-localize-behavior. - * - * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt - */ - -/** - * Optional dictionary of user defined formats, as explained here: - * http://formatjs.io/guides/message-syntax/#custom-formats - * - * For example, a valid dictionary of formats would be: - * this.formats = { - * number: { USD: { style: 'currency', currency: 'USD' } } - * } - */ -interface FormatType { - [format: string]: any; -} -export interface FormatsType { - number: FormatType; - date: FormatType; - time: FormatType; -} - -export type LocalizeFunc = (key: string, ...args: any[]) => string; - -export interface LocalizeMixin { - hass?: HomeAssistant; - localize: LocalizeFunc; -} - -export const localizeBaseMixin = (superClass) => - class extends superClass { - /** - * Returns a computed `localize` method, based on the current `language`. - */ - public __computeLocalize( - language: string, - resources: string, - formats?: FormatsType - ): LocalizeFunc { - const proto = this.constructor.prototype; - - // Check if localCache exist just in case. - this.__checkLocalizationCache(proto); - - // Everytime any of the parameters change, invalidate the strings cache. - if (!proto.__localizationCache) { - proto.__localizationCache = { - messages: {}, - }; - } - proto.__localizationCache.messages = {}; - - return (key, ...args) => { - if (!key || !resources || !language || !resources[language]) { - return ""; - } - - // Cache the key/value pairs for the same language, so that we don't - // do extra work if we're just reusing strings across an application. - const translatedValue = resources[language][key]; - - if (!translatedValue) { - return this.useKeyIfMissing ? key : ""; - } - - const messageKey = key + translatedValue; - let translatedMessage = proto.__localizationCache.messages[messageKey]; - - if (!translatedMessage) { - translatedMessage = new (IntlMessageFormat as any)( - translatedValue, - language, - formats - ); - proto.__localizationCache.messages[messageKey] = translatedMessage; - } - - const argObject = {}; - for (let i = 0; i < args.length; i += 2) { - argObject[args[i]] = args[i + 1]; - } - - try { - return translatedMessage.format(argObject); - } catch (err) { - return "Translation " + err; - } - }; - } - - public __checkLocalizationCache(proto) { - // do nothing if proto is undefined. - if (proto === undefined) { - return; - } - - // In the event proto not have __localizationCache object, create it. - if (proto.__localizationCache === undefined) { - proto.__localizationCache = { - messages: {}, - }; - } - } - }; diff --git a/src/mixins/localize-lite-base-mixin.ts b/src/mixins/localize-lite-base-mixin.ts index d9fe1e2a58..e257eb4fef 100644 --- a/src/mixins/localize-lite-base-mixin.ts +++ b/src/mixins/localize-lite-base-mixin.ts @@ -1,14 +1,13 @@ /** * Lite base mixin to add localization without depending on the Hass object. */ -import { localizeBaseMixin } from "./localize-base-mixin"; import { getTranslation } from "../util/hass-translation"; /** * @polymerMixin */ export const localizeLiteBaseMixin = (superClass) => - class extends localizeBaseMixin(superClass) { + class extends superClass { protected _initializeLocalizeLite() { if (this.resources) { return; diff --git a/src/mixins/localize-lite-mixin.ts b/src/mixins/localize-lite-mixin.ts index 16ffa615a1..f406fab134 100644 --- a/src/mixins/localize-lite-mixin.ts +++ b/src/mixins/localize-lite-mixin.ts @@ -4,6 +4,7 @@ import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin"; import { getActiveTranslation } from "../util/hass-translation"; import { localizeLiteBaseMixin } from "./localize-lite-base-mixin"; +import { computeLocalize } from "../common/translations/localize"; /** * @polymerMixin @@ -36,5 +37,14 @@ export const localizeLiteMixin = dedupingMixin( super.ready(); this._initializeLocalizeLite(); } + + protected __computeLocalize(language, resources, formats?) { + return computeLocalize( + this.constructor.prototype, + language, + resources, + formats + ); + } } ); diff --git a/src/mixins/localize-mixin.js b/src/mixins/localize-mixin.js index 06884ddbcf..19a7081d99 100644 --- a/src/mixins/localize-mixin.js +++ b/src/mixins/localize-mixin.js @@ -1,5 +1,4 @@ import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin"; -import { localizeBaseMixin } from "./localize-base-mixin"; /** * Polymer Mixin to enable a localize function powered by language/resources from hass object. * @@ -7,7 +6,7 @@ import { localizeBaseMixin } from "./localize-base-mixin"; */ export default dedupingMixin( (superClass) => - class extends localizeBaseMixin(superClass) { + class extends superClass { static get properties() { return { hass: Object, @@ -19,10 +18,13 @@ export default dedupingMixin( */ localize: { type: Function, - computed: - "__computeLocalize(hass.language, hass.resources, formats)", + computed: "__computeLocalize(hass.localize)", }, }; } + + __computeLocalize(localize) { + return localize; + } } ); diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 9d04361828..72a2ccced5 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -11,10 +11,10 @@ import computeStateName from "../../../common/entity/compute_state_name"; import splitByGroups from "../../../common/entity/split_by_groups"; import computeObjectId from "../../../common/entity/compute_object_id"; import computeStateDomain from "../../../common/entity/compute_state_domain"; -import { LocalizeFunc } from "../../../mixins/localize-base-mixin"; import computeDomain from "../../../common/entity/compute_domain"; import { EntityRowConfig, WeblinkConfig } from "../entity-rows/types"; import { EntitiesCardConfig } from "../cards/hui-entities-card"; +import { LocalizeFunc } from "../../../common/translations/localize"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const DOMAINS_BADGES = [ diff --git a/src/types.ts b/src/types.ts index b0cdfe43d8..e72f40b124 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ import { HassEntityAttributeBase, HassServices, } from "home-assistant-js-websocket"; +import { LocalizeFunc } from "./common/translations/localize"; declare global { var __DEV__: boolean; @@ -92,6 +93,10 @@ export interface Notification { created_at: string; } +export interface Resources { + [language: string]: { [key: string]: string }; +} + export interface HomeAssistant { auth: Auth; connection: Connection; @@ -103,14 +108,19 @@ export interface HomeAssistant { selectedTheme?: string | null; panels: Panels; panelUrl: string; + + // i18n language: string; - resources: { [key: string]: any }; + selectedLanguage?: string; + resources: Resources; + localize: LocalizeFunc; translationMetadata: { fragments: string[]; translations: { [lang: string]: Translation; }; }; + dockedSidebar: boolean; moreInfoEntityId: string; user: User; @@ -196,3 +206,8 @@ export interface PanelElement extends HTMLElement { route?: Route | null; panel?: Panel; } + +export interface LocalizeMixin { + hass?: HomeAssistant; + localize: LocalizeFunc; +}