Clean up localize mixin (#1771)

This commit is contained in:
Paulus Schoutsen 2018-10-14 22:40:43 +02:00 committed by GitHub
parent 906aaa15a3
commit fbccf23d36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 377 deletions

View File

@ -0,0 +1,52 @@
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js";
import { LocalizeBaseMixin } from "./localize-base-mixin";
export const HassLocalizeLitMixin = dedupingMixin(
(superClass) =>
class extends LocalizeBaseMixin(superClass) {
static get properties() {
return {
hass: {},
localize: {},
};
}
connectedCallback() {
super.connectedCallback();
let language;
let resources;
if (this.hass) {
language = this.hass.language;
resources = this.hass.resources;
}
this.localize = this.__computeLocalize(language, resources);
}
updated(changedProperties) {
super.updated(changedProperties);
if (!changedProperties.has("hass")) {
return;
}
let oldLanguage;
let oldResources;
if (changedProperties.hass) {
oldLanguage = changedProperties.hass.language;
oldResources = changedProperties.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);
}
}
}
);

View File

@ -0,0 +1,82 @@
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js";
import IntlMessageFormat from "intl-messageformat/src/main.js";
/**
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
*/
export const LocalizeBaseMixin = dedupingMixin(
(superClass) =>
class extends superClass {
/**
* Returns a computed `localize` method, based on the current `language`.
*/
__computeLocalize(language, resources, formats) {
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(
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];
}
return translatedMessage.format(argObject);
};
}
__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: {},
};
}
}
}
);

View File

@ -1,9 +1,8 @@
/**
* Lite mixin to add localization without depending on the Hass object.
*/
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class.js";
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js";
import { AppLocalizeBehavior } from "../util/app-localize-behavior.js";
import { LocalizeBaseMixin } from "./localize-base-mixin";
import {
getActiveTranslation,
getTranslation,
@ -11,11 +10,10 @@ import {
/**
* @polymerMixin
* @appliesMixin AppLocalizeBehavior
*/
export default dedupingMixin(
(superClass) =>
class extends mixinBehaviors([AppLocalizeBehavior], superClass) {
class extends LocalizeBaseMixin(superClass) {
static get properties() {
return {
language: {
@ -25,10 +23,19 @@ export default dedupingMixin(
resources: Object,
// The fragment to load.
translationFragment: String,
/**
* Translates a string to the current `language`. Any parameters to the
* string should be passed in order, as follows:
* `localize(stringKey, param1Name, param1Value, param2Name, param2Value)`
*/
localize: {
type: Function,
computed: "__computeLocalize(language, resources, formats)",
},
};
}
async ready() {
ready() {
super.ready();
if (this.resources) {
@ -53,6 +60,10 @@ export default dedupingMixin(
return;
}
this._updateResources();
}
async _updateResources() {
const { language, data } = await getTranslation(
this.translationFragment
);

View File

@ -1,34 +1,28 @@
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class.js";
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin.js";
import { AppLocalizeBehavior } from "../util/app-localize-behavior.js";
import { LocalizeBaseMixin } from "./localize-base-mixin.js";
/**
* Polymer Mixin to enable a localize function powered by language/resources from hass object.
*
* @polymerMixin
* @appliesMixin AppLocalizeBehavior
*/
export default dedupingMixin(
(superClass) =>
class extends mixinBehaviors([AppLocalizeBehavior], superClass) {
class extends LocalizeBaseMixin(superClass) {
static get properties() {
return {
hass: Object,
language: {
type: String,
computed: "computeLanguage(hass)",
},
resources: {
type: Object,
computed: "computeResources(hass)",
/**
* Translates a string to the current `language`. Any parameters to the
* string should be passed in order, as follows:
* `localize(stringKey, param1Name, param1Value, param2Name, param2Value)`
*/
localize: {
type: Function,
computed:
"__computeLocalize(hass.language, hass.resources, formats)",
},
};
}
computeLanguage(hass) {
return hass && hass.language;
}
computeResources(hass) {
return hass && hass.resources;
}
}
);

View File

@ -13,13 +13,9 @@ import "../../../components/ha-card.js";
import "../../../components/ha-icon.js";
import EventsMixin from "../../../mixins/events-mixin.js";
import LocalizeMixin from "../../../mixins/localize-mixin.js";
import { HassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin.js";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class HuiGlanceCard extends LocalizeMixin(EventsMixin(LitElement)) {
class HuiGlanceCard extends HassLocalizeLitMixin(EventsMixin(LitElement)) {
renderStyle() {
return html`
<style>

View File

@ -64,6 +64,8 @@ class HaPanelProfile extends EventsMixin(LocalizeMixin(PolymerElement)) {
<template is='dom-if' if='[[hass.user.is_owner]]'>[[localize('ui.panel.profile.is_owner')]]</template>
</div>
<hello-world hass='[[hass]]'></hello-world>
<ha-pick-language-row
narrow="[[narrow]]"
hass="[[hass]]"

View File

@ -1,346 +0,0 @@
/* Forked to fix the import of IntlMessageFormat */
/* eslint-disable */
/**
@license
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
*/
import "@polymer/polymer/polymer-legacy.js";
import "@polymer/iron-ajax/iron-ajax.js";
import IntlMessageFormat from "intl-messageformat/src/main.js";
/**
* `Polymer.AppLocalizeBehavior` wraps the [format.js](http://formatjs.io/) library to
* help you internationalize your application. Note that if you're on a browser that
* does not natively support the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)
* object, you must load the polyfill yourself. An example polyfill can
* be found [here](https://github.com/andyearnshaw/Intl.js/).
*
* `Polymer.AppLocalizeBehavior` supports the same [message-syntax](http://formatjs.io/guides/message-syntax/)
* of format.js, in its entirety; use the library docs as reference for the
* available message formats and options.
*
* Sample application loading resources from an external file:
*
* <dom-module id="x-app">
* <template>
* <div>{{localize('hello', 'name', 'Batman')}}</div>
* </template>
* <script>
* Polymer({
* is: "x-app",
*
* behaviors: [
* Polymer.AppLocalizeBehavior
* ],
*
* properties: {
* language: {
* value: 'en'
* },
* }
*
* attached: function() {
* this.loadResources(this.resolveUrl('locales.json'));
* },
* });
* &lt;/script>
* </dom-module>
*
*
* If the resources stored in your external file are for a single language and
* so are not nested inside any language keys, you can pass an optional
* `language` parameter to store the fetched resources inside that key.
*
* This complements the optional third parameter, `merge`, nicely: If you pass
* `merge = true`, the fetched resources will be merged into any existing
* resources rather than clobbering them.
*
* This is also useful for storing resources for different parts of a page that
* the user might or might not see at the same time in different files, so that
* the user can fetch only the needed resources on-demand, and doesn't have to
* load any resources they'll never see anyway. For example, you could store
* your resources for your global nav, homepage, and FAQ page in 3 different
* files. When a user requests the homepage, both the global nav and the
* homepage resources are fetched and merged together, since they both appear
* on the page at the same time, but you spare the user from fetching the
* unneeded FAQ resources.
*
*
* Example:
*
* attached: function() {
* this.loadResources(
*
* // Only contains the flattened "es" translations:
* 'locales/es.json', // {"hi": "hola"}
*
* 'es', // unflatten -> {"es": {"hi": "hola"}}
*
* true // merge so existing resources won't be clobbered
* );
* }
*
*
* Alternatively, you can also inline your resources inside the app itself:
*
* <dom-module id="x-app">
* <template>
* <div>{{localize('hello', 'name', 'Batman')}}</div>
* </template>
* <script>
* Polymer({
* is: "x-app",
*
* behaviors: [
* Polymer.AppLocalizeBehavior
* ],
*
* properties: {
* language: {
* value: 'en'
* },
* resources: {
* value: function() {
* return {
* 'en': { 'hello': 'My name is {name}.' },
* 'fr': { 'hello': 'Je m\'appelle {name}.' }
* }
* }
* }
* });
* &lt;/script>
* </dom-module>
*
* @demo demo/index.html
* @polymerBehavior Polymer.AppLocalizeBehavior
*/
export const AppLocalizeBehavior = {
/**
* Internal singleton cache. This is the private implementation of the
* behaviour; don't interact with it directly.
*/
__localizationCache: {
requests: {} /* One iron-request per unique resources path. */,
messages: {} /* Unique localized strings. Invalidated when the language, formats or resources change. */,
ajax: null /* Global iron-ajax object used to request resource files. */,
},
/**
* Fired after the resources have been loaded.
*
* @event app-localize-resources-loaded
*/
/**
* Fired when the resources cannot be loaded due to an error.
*
* @event app-localize-resources-error
*/
properties: {
/**
* The language used for translation.
*/
language: {
type: String,
},
/**
* The dictionary of localized messages, for each of the languages that
* are going to be used. See http://formatjs.io/guides/message-syntax/ for
* more information on the message syntax.
*
* For example, a valid dictionary would be:
* this.resources = {
* 'en': { 'greeting': 'Hello!' }, 'fr' : { 'greeting': 'Bonjour!' }
* }
*/
resources: {
type: Object,
},
/**
* 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' } }
* }
*/
formats: {
type: Object,
value: function() {
return {};
},
},
/**
* If true, will use the provided key when
* the translation does not exist for that key.
*/
useKeyIfMissing: {
type: Boolean,
value: false,
},
/**
* Translates a string to the current `language`. Any parameters to the
* string should be passed in order, as follows:
* `localize(stringKey, param1Name, param1Value, param2Name, param2Value)`
*/
localize: {
type: Function,
computed: "__computeLocalize(language, resources, formats)",
},
/**
* If true, will bubble up the event to the parents
*/
bubbleEvent: {
type: Boolean,
value: false,
},
},
loadResources: function(path, language, merge) {
var proto = this.constructor.prototype;
// Check if localCache exist just in case.
this.__checkLocalizationCache(proto);
// If the global ajax object has not been initialized, initialize and cache it.
var ajax = proto.__localizationCache.ajax;
if (!ajax) {
ajax = proto.__localizationCache.ajax = document.createElement(
"iron-ajax"
);
}
var request = proto.__localizationCache.requests[path];
function onRequestResponse(event) {
this.__onRequestResponse(event, language, merge);
}
if (!request) {
ajax.url = path;
var request = ajax.generateRequest();
request.completes.then(
onRequestResponse.bind(this),
this.__onRequestError.bind(this)
);
// Cache the instance so that it can be reused if the same path is loaded.
proto.__localizationCache.requests[path] = request;
} else {
request.completes.then(
onRequestResponse.bind(this),
this.__onRequestError.bind(this)
);
}
},
/**
* Returns a computed `localize` method, based on the current `language`.
*/
__computeLocalize: function(language, resources, formats) {
var 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"] = { requests: {}, messages: {}, ajax: null };
}
proto.__localizationCache.messages = {};
return function() {
var key = arguments[0];
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.
var translatedValue = resources[language][key];
if (!translatedValue) {
return this.useKeyIfMissing ? key : "";
}
var messageKey = key + translatedValue;
var translatedMessage = proto.__localizationCache.messages[messageKey];
if (!translatedMessage) {
translatedMessage = new IntlMessageFormat(
translatedValue,
language,
formats
);
proto.__localizationCache.messages[messageKey] = translatedMessage;
}
var args = {};
for (var i = 1; i < arguments.length; i += 2) {
args[arguments[i]] = arguments[i + 1];
}
return translatedMessage.format(args);
}.bind(this);
},
__onRequestResponse: function(event, language, merge) {
var propertyUpdates = {};
var newResources = event.response;
if (merge) {
if (language) {
propertyUpdates.resources = Object.assign({}, this.resources || {});
propertyUpdates["resources." + language] = Object.assign(
propertyUpdates.resources[language] || {},
newResources
);
} else {
propertyUpdates.resources = Object.assign(this.resources, newResources);
}
} else {
if (language) {
propertyUpdates.resources = {};
propertyUpdates.resources[language] = newResources;
propertyUpdates["resources." + language] = newResources;
} else {
propertyUpdates.resources = newResources;
}
}
if (this.setProperties) {
this.setProperties(propertyUpdates);
} else {
for (var key in propertyUpdates) {
this.set(key, propertyUpdates[key]);
}
}
this.fire("app-localize-resources-loaded", event, {
bubbles: this.bubbleEvent,
});
},
__onRequestError: function(event) {
this.fire("app-localize-resources-error");
},
__checkLocalizationCache: function(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"] = { requests: {}, messages: {}, ajax: null };
}
},
};