i18n-behavior
Version:
Instant and Modular I18N engine for lit-html and Polymer
262 lines (240 loc) • 8.09 kB
JavaScript
/**
@license https://github.com/t2ym/i18n-behavior/blob/master/LICENSE.md
Copyright (c) 2019, Tetsuya Mori <t2y3141592@gmail.com>. All rights reserved.
*/
/**
The singleton element `<i18n-preference>` maintains user preference for `i18n-behavior`.
The element is automatically attached at the end of `<body>` element when it is not
included in the root html document.
It just initializes `<html lang>` attribute with `navigator.language` value
unless `<i18n-preference persist>` attribute is specified.
It stores the value of `<html lang>` attribute into localstorage named `i18n-behavior-preference`
when `<i18n-preference persist>` attribute is specified.
The stored value is synchronized with that of `<html lang>` attribute on changes.
- - -
### TODO
- Per-user preference handling for application.
@group I18nBehavior
@element i18n-preference
*/
import { polyfill } from 'wc-putty/polyfill.js';
// html element of this document
export const html = document.querySelector('html');
// app global default language
export const defaultLang = html.hasAttribute('lang') ? html.getAttribute('lang') : '';
export class I18nPreference extends polyfill(HTMLElement) {
static get is() {
return 'i18n-preference';
}
static get observedAttributes() {
return [ 'persist' ];
}
constructor() {
super();
/**
* Key of localStorage
*/
this._storageKey = 'i18n-behavior-preference';
/**
* Persistence of preference
*/
this.persist = this.hasAttribute('persist');
}
/**
* attributeChangedCallback for custom elements
*
* Upates this.persist property on every persist attribute change
*/
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'persist':
this.persist = this.hasAttribute(name);
//console.log(`attributeChangedCallback name="${name}" oldValue="${oldValue}" newValue="${newValue}" this.persist=${this.persist}`);
break;
/* istanbul ignore next */
default:
/* istanbul ignore next */
break;
}
}
/**
* persist property for persistence of preference to localStorage
*
* Note: this._update() is called on every change
*/
get persist() {
return this._persist;
}
set persist(value) {
this._persist = value;
this._update();
}
/**
* value property for reading/storing language preference in localStorage
*
* Note: If the value is set as `undefined` or `null`, the language preference in localStorage is removed
*/
get value() {
return JSON.parse(window.localStorage.getItem(this._storageKey));
}
set value(_value) {
//console.log('save', _value, 'this.value', this.value);
this._lastSavedValue = _value;
if (_value === undefined || _value === null) {
window.localStorage.removeItem(this._storageKey);
}
else {
window.localStorage.setItem(this._storageKey, JSON.stringify(_value));
}
}
/**
* Connected callback to initialize html.lang and its observation
*/
connectedCallback() {
this._update();
this._observe();
}
/**
* Disconnected callback to disconnect html.lang observation
*/
disconnectedCallback() {
this._disconnect();
}
/**
* Updates the status based on `<html lang>`, `localStorage`, and `navigator.language`
*
* Note: Persistence is controlled by `persist` property
*/
_update() {
//console.log(`_update persist=${this.persist} <html lang=${html.getAttribute('lang')}> <html preferred=${html.hasAttribute('preferred')}> value="${this.value}"(type: ${typeof this.value})`);
if (this.persist) {
if (this.value === null) {
if (this.isInitialized) {
// store html.lang value
if (this.value !== html.getAttribute('lang')) {
this.value = html.getAttribute('lang');
}
}
else {
if (html.hasAttribute('preferred')) {
if (this.value !== html.getAttribute('lang')) {
this.value = html.getAttribute('lang');
}
}
else {
let value = navigator.language;
if (this.value !== value) {
this.value = value;
}
if (html.getAttribute('lang') !== value) {
html.setAttribute('lang', value);
}
}
this.isInitialized = true;
}
}
else {
// preferred attribute in html to put higher priority
// in the default html language than navigator.language
if (html.hasAttribute('preferred')) {
if (this.value !== defaultLang) {
// overwrite the storage by the app default language
this.value = defaultLang;
}
if (html.getAttribute('lang') !== defaultLang) {
// reset to the defaultLang if preferred
// Note: defaultLang does not change from the initial value at the loading
html.setAttribute('lang', defaultLang);
}
}
else {
// load the value from the storage
html.setAttribute('lang', this.value);
}
}
}
else {
if (this.value !== null) {
this.value = null;
}
// set html lang with navigator.language
if (!html.hasAttribute('preferred')) {
html.setAttribute('lang', navigator.language);
}
}
}
/**
* Handles attribute value changes on html
*
* @param {MutationRecord[]} mutations Array of MutationRecords for html.lang
*
* Note:
* - Bound to this element
*/
_htmlLangMutationObserverCallback(mutations) {
mutations.forEach(function(mutation) {
switch (mutation.type) {
case 'attributes':
if (mutation.attributeName === 'lang') {
if (this.persist) {
if (this.value !== mutation.target.getAttribute('lang')) {
this.value = mutation.target.getAttribute('lang');
}
}
else {
if (this.value !== null) {
this.value = null;
}
}
}
break;
/* istanbul ignore next: mutation.type is always attributes */
default:
/* istanbul ignore next: mutation.type is always attributes */
break;
}
}.bind(this));
}
/**
* "storage" event handler
*/
_onStorageEvent(event) {
//console.log(`_onStorageEvent: key="${event.key}" oldValue="${event.oldValue}"(type:${typeof event.oldValue}) newValue="${event.newValue}"(type:${typeof event.newValue}) url="${event.url}" storageArea="${JSON.stringify(event.storageArea)}"`);
// Note: IE11 dispatches unnecessary storage events even from the same window with obsolete event.newValue
if (event.key === this._storageKey && event.newValue === JSON.stringify(this.value) && event.newValue !== JSON.stringify(this._lastSavedValue)) {
this._update();
}
}
/**
* Sets up html.lang mutation observer
*/
_observe() {
// observe html lang mutations
if (!this._htmlLangMutationObserver) {
this._htmlLangMutationObserverCallbackBindThis =
this._htmlLangMutationObserverCallback.bind(this);
this._htmlLangMutationObserver =
new MutationObserver(this._htmlLangMutationObserverCallbackBindThis);
}
this._htmlLangMutationObserver.observe(html, { attributes: true });
// set up StorageEvent handler
if (!this._onStorageEventBindThis) {
this._onStorageEventBindThis = this._onStorageEvent.bind(this);
}
window.addEventListener('storage', this._onStorageEventBindThis);
}
/**
* Disconnects html.lang mutation observer
*/
_disconnect() {
if (this._htmlLangMutationObserver) {
this._htmlLangMutationObserver.disconnect();
}
// tear down StorageEvent handler, using _onStorageEventBindThis as a status indicator
if (this._onStorageEventBindThis) {
window.removeEventListener('storage', this._onStorageEventBindThis);
this._onStorageEventBindThis = null;
}
}
}
customElements.define(I18nPreference.is, I18nPreference);