i18n-behavior
Version:
Instant and Modular I18N engine for lit-html and Polymer
504 lines (451 loc) • 16.3 kB
JavaScript
/**
@license https://github.com/t2ym/i18n-behavior/blob/master/LICENSE.md
Copyright (c) 2019, Tetsuya Mori <t2y3141592@gmail.com>. All rights reserved.
*/
import 'wc-putty/polyfill.js';
const template = document.createElement('template');
template.innerHTML = `<template id="i18n-attr-repo">
<template id="standard">
<!-- Standard HTML5 -->
<input placeholder="" value="type=button|submit">
<any-elements title="" aria-label="\$" aria-valuetext="\$"></any-elements>
<!-- Standard Polymer Elements -->
<paper-input label="" error-message="" placeholder=""></paper-input>
<paper-textarea label="" error-message="" placeholder=""></paper-textarea>
<paper-dropdown-menu label=""></paper-dropdown-menu>
<paper-toast text=""></paper-toast>
<paper-badge label=""></paper-badge>
<google-chart options="" cols="" rows="" data=""></google-chart>
<google-signin label-signin="" label-signout="" label-additional=""></google-signin>
<platinum-push-messaging title="" message=""></platinum-push-messaging>
<!-- Specific to i18n-behavior -->
<json-data any-attributes=""></json-data>
</template>
</template>`;
// shared data
const sharedData = {};
/*
`<i18n-attr-repo>` maintains a list of attributes targeted for UI localization.
It judges whether a specific attribute of an element requires localization or not.
var attrRepository =
document.createElement('i18n-attr-repo');
attrRepository.registerLocalizableAttributes(
'custom-element',
Polymer.DomModule.import('custom-element', 'template')
);
attrRepository.isLocalizableAttribute(inputElement, 'placeholder');
### Interactions with `BehaviorsStore.I18nBehavior`
The element is not meant for DOM attachment. The object is
a singleton object dedicated for `BehaviorsStore.I18nBehavior`.
`I18nBehavior` interacts with the localizable attributes repository in these 3 ways.
### 1) Construct the repository for the standard elements from its own static template at the object creation.
```
// i18n-behavior.html
var attrRepository =
document.createElement('i18n-attr-repo');
```
Pre-defined I18N-target attributes in the static template of `i18n-attr-repo`:
```
<dom-module id="i18n-attr-repo">
<template>
<template id="standard">
<input placeholder>
<any-elements title aria-label="$" aria-valuetext="$"></any-elements>
<paper-input label error-message placeholder></paper-input>
<paper-textarea label error-message placeholder></paper-textarea>
<paper-dropdown-menu label></paper-dropdown-menu>
<paper-toast text></paper-toast>
<google-chart options cols rows data></google-chart>
<google-signin label-signin label-signout label-additional></google-signin>
<platinum-push-messaging title message></platinum-push-messaging>
<json-data any-attributes></json-data>
</template>
</template>
</dom-module>
```
This static list is also referenced by [`gulp-i18n-preprocess`](https://github.com/t2ym/gulp-i18n-preprocess) filter for
build-time automatic I18N of hard-coded string attributes.
### 2) Register I18N-target attributes of custom elements from a template with id="custom" in its light DOM.
I18N-target attributes for custom elements without I18nBehavior can be registered to the respository by this method.
Example I18N-target attributes in a static template in the light DOM of `i18n-attr-repo`:
```
<i18n-attr-repo>
<template id="custom">
<shop-md-decorator error-message="$"></shop-md-decorator>
<input value="type=submit|button">
<my-element i18n-target-attr="attr=value,boolean-attr,!boolean-attr"></my-element>
<my-element i18n-target-attr="attr1=value1,attr2=value2,type-name"></my-element>
<my-element i18n-target-attr="boolean-attr="></my-element>
<my-element i18n-target-attr="type-name2"></my-element>
</template>
</i18n-attr-repo>
```
This list is also referenced by [`gulp-i18n-preprocess`](https://github.com/t2ym/gulp-i18n-preprocess) filter for
build-time automatic I18N of hard-coded string attributes.
Note: Type name feature is currently ineffective and reserved for further expansion of the attribute I18N features.
### 3) Register localizable attributes of the newly registered elements from the `text-attr` attribute of the element's template.
```
// i18n-behavior.html, scanning custom-element template
var id = 'custom-element';
attrRepository.registerLocalizableAttributes(
id,
Polymer.DomModule.import(id, 'template')
);
```
```
// custom-element.html
<dom-module id="custom-element">
<template text-attr="localizable-attr1 localizable-attr2">
<span>{{localizableAttr1}}</span>
<span>{{localizableAttr2}}</span>
</template>
<script>
Polymer({
is: 'custom-element',
behaviors: [ BehaviorsStore.I18nBehavior ],
properties: {
localizableAttr1: {
type: String
},
localizableAttr2: {
type: String
}
}
});
</ script>
</dom-module>
```
`text-attr` attributes are also traversed for build-time automatic I18N of
hard-coded UI string attributes by [`gulp-i18n-preprocess`](https://github.com/t2ym/gulp-i18n-preprocess) filter.
### 4) Judge localizability of attributes for the local DOM elements of the newly registered element.
```
// i18n-behavior.html, scanning custom-element-user template
var element; // target element
var attr;
if (attrRepository.isLocalizableAttribute(element, attr.name)) {
// make localizalbe-attr1 localizable
}
```
```
// custom-element-user.html
<dom-module id="custom-element-user">
<template>
<custom-element id="custom"
localizable-attr1="UI Text Label 1"
localizable-attr2="UI Text Label 2">
</custom-element>
</template>
<script>
Polymer({
is: 'custom-element-user',
behaviors: [ BehaviorsStore.I18nBehavior ]
});
</ script>
</dom-module>
```
```
// template for custom-element-user after localization binding
<template>
<custom-element id="custom"
localizable-attr1="{{model.custom.localizable-attr1}}"
localizable-attr2="{{model.custom.localizable-attr2}}">
</custom-element>
</template>
```
```
// extracted localizable texts in custom-element-user element
this.model = {
"custom": {
"localizable-attr1": "UI Text Label 1",
"localizable-attr2": "UI Text Label 2"
}
}
```
Since dependent elements should be registered prior to a custom element being registered,
the repository can always maintain the complete list of localizable attributes for registered custom elements.
- - -
### Note
The described processes above are for debug builds with runtime localization traversal of templates
by `I18nBehavior`.
For production builds, the build system can perform the same processes at build time so that
`I18nBehavior` at clients can skip runtime traversal of templates.
- - -
### TODO
Handle and judge JSON object attributes.
@group I18nBehavior
@element i18n-attr-repo
*/
export class I18nAttrRepo extends HTMLElement {
static get is() {
return 'i18n-attr-repo';
}
constructor() {
super();
this.data = sharedData;
let customAttributes = this.querySelector('template#custom');
// traverse custom attributes repository
if (customAttributes && !this.hasAttribute('processed')) {
this._traverseTemplateTree(customAttributes.content);
this.setAttribute('processed', '');
}
this._created();
}
/**
* Sets up repository by the standard template
*/
_created() {
this.data = sharedData;
if (this.data.__ready__) {
return; // traverse standard attributes only once
}
this.data.__ready__ = true;
// standard attributes
const standardTemplate = template.content.querySelector('template#i18n-attr-repo').content.querySelector('template#standard');
this._traverseTemplateTree(standardTemplate.content);
}
/**
* Judges if a specific attribute of an element requires localization.
*
* @param {HTMLElement} element Target element.
* @param {string} attr Target attribute name.
* @return {string or boolean} true - property, '$' - attribute, false - not targeted, 'type-name' - type name
*/
isLocalizableAttribute(element, attr) {
let tagName = element.tagName.toLowerCase();
if (!this.data) {
this._created();
this.data = sharedData;
}
attr = attr.replace(/\$$/, '');
if (this.data['any-elements'] &&
this.data['any-elements'][attr]) {
return this.data['any-elements'][attr];
}
else if (this.data[tagName]) {
return this.data[tagName]['any-attributes'] ||
this._getType(element, this.data[tagName][attr]);
}
else {
return false;
}
}
/**
* Gets the type name or '$' for a specific attribute of an element from the attributes repository
*
* @param {HTMLElement} element Target element.
* @param {object} value this.data[tagName][attr]
* @return {string or boolean} true - property, '$' - attribute, false - not targeted, 'type-name' - type name
*/
_getType(element, value) {
let selector;
let result;
if (typeof value === 'object') {
for (selector in value) {
if (selector) {
if (this._matchAttribute(element, selector)) {
result = this._getType(element, value[selector]);
if (result) {
return result;
}
}
}
}
if (value['']) {
if (this._matchAttribute(element, '')) {
result = this._getType(element, value['']);
if (result) {
return result;
}
}
}
return false;
}
else {
return value;
}
}
/**
* Gets the type name or '$' for a specific attribute of an element from the attributes repository
*
* Format for selectors:
* - `attr=value` - Value of `attr` matches Regex `^value$`
* - `!boolean-attr` - Boolean attribute does not exist
* - `boolean-attr` - Boolean attribute exists with empty value
* - empty string `''` - Always matches
*
* @param {HTMLElement} element Target element.
* @param {string} selector Matching condition for target attribute.
* @return {boolean} true - matching, false - not matching
*/
_matchAttribute(element, selector) {
let value;
let match;
// default ''
if (selector === '') {
return true;
}
// attr=value Regex ^value$
match = selector.match(/^([^!=]*)=(.*)$/);
if (match) {
if (element.hasAttribute(match[1])) {
value = element.getAttribute(match[1]);
return !!value.match(new RegExp('^' + match[2] + '$'));
}
else {
return false;
}
}
// !boolean-attr
match = selector.match(/^!([^!=]*)$/);
if (match) {
return !element.hasAttribute(match[1]);
}
// boolean-attr or empty-attr
match = selector.match(/^([^!=]*)$/);
if (match) {
if (element.hasAttribute(match[1])) {
value = element.getAttribute(match[1]);
return !value;
}
else {
return false;
}
}
// no matching
return false;
}
/**
* Comparator for attribute selectors
*
* @param {string} s1 selector 1
* @param {string} s2 selector 2
* @return {number} comparison result as -1, 0, or 1
*/
_compareSelectors(s1, s2) {
let name1 = s1.replace(/^!/, '').replace(/=.*$/, '').toLowerCase();
let name2 = s2.replace(/^!/, '').replace(/=.*$/, '').toLowerCase();
return name1.localeCompare(name2);
}
/**
* Adds a new localizable attribute of an element to the repository.
*
* Format for selector values for defining I18N-target attributes:
* - `attr1=value1,attr2=value2,boolean-attr,!boolean-attr` - Attribute value matching condition for property
* - `attr1=value1,attr2=value2,$` - Attribute value matching condition for attribute
* - `boolean-attr=` - Boolean attribute condition
* - `attr1=value1,type` - Attribute value condition with type name (type is currently ineffective)
*
* @param {string} element Target element name.
* @param {string} attr Target attribute name.
* @param {?*} value Selector value
*/
setLocalizableAttribute(element, attr, value) {
this.data[element] = this.data[element] || {};
let cursor = this.data[element];
let prev = attr;
let type = true;
let selectors = [];
if (typeof value === 'string' && value) {
selectors = value.split(',');
if (selectors[selectors.length - 1].match(/^[^!=][^=]*$/)) {
type = selectors.pop();
}
selectors = selectors.map(function (selector) {
return selector.replace(/=$/, '');
});
selectors.sort(this._compareSelectors);
while (selectors[0] === '') {
selectors.shift();
}
}
selectors.forEach(function (selector, index) {
if (typeof cursor[prev] !== 'object') {
cursor[prev] = cursor[prev] ? { '': cursor[prev] } : {};
}
cursor[prev][selector] = cursor[prev][selector] || {};
cursor = cursor[prev];
prev = selector;
});
if (typeof cursor[prev] === 'object' &&
cursor[prev] &&
Object.keys(cursor[prev]).length) {
cursor = cursor[prev];
prev = '';
}
cursor[prev] = type;
}
/**
* Picks up localizable attributes description for a custom element
* from `text-attr` attribute and register them to the repository.
* The `text-attr` attribute is used in the template of a custom
* element to declare localizable attributes of its own element.
*
* Format:
*
* Type 1: `<template text-attr="localizable-attr1 attr2">`
*
* Type 2: `<template text-attr localizable-attr1 attr2="value2">`
*
* @param {string} element Target element name.
* @param {HTMLTemplateElement} template Template of the element.
*/
registerLocalizableAttributes(element, template) {
if (!this.data) {
this._created();
this.data = sharedData;
}
if (!element) {
element = template.getAttribute('id');
}
if (element) {
let attrs = (template.getAttribute('text-attr') || '').split(' ');
let textAttr = false;
attrs.forEach(function (attr) {
if (attr) {
this.setLocalizableAttribute(element, attr, true);
}
}, this);
Array.prototype.forEach.call(template.attributes, function (attr) {
switch (attr.name) {
case 'id':
case 'lang':
case 'localizable-text':
case 'assetpath':
break;
case 'text-attr':
textAttr = true;
break;
default:
if (textAttr) {
this.setLocalizableAttribute(element, attr.name, attr.value);
}
break;
}
}.bind(this));
}
}
/**
* Traverses the template of `i18n-attr-repo` in the ready() callback
* and construct the localizable attributes repository object. The method calls itself
* recursively for traversal.
*
* @param {HTMLElement} node The target HTML node for traversing.
*/
_traverseTemplateTree(node) {
var name;
if (node.nodeType === node.ELEMENT_NODE) {
name = node.nodeName.toLowerCase();
Array.prototype.forEach.call(node.attributes, function (attribute) {
this.data[name] = this.data[name] || {};
this.setLocalizableAttribute(name, attribute.name, attribute.value);
}, this);
}
if (node.childNodes.length > 0) {
for (var i = 0; i < node.childNodes.length; i++) {
this._traverseTemplateTree(node.childNodes[i]);
}
}
}
}
customElements.define(I18nAttrRepo.is, I18nAttrRepo);
export const attributesRepository = document.createElement(I18nAttrRepo.is);