-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathi18n-preference.js
261 lines (240 loc) · 7.9 KB
/
i18n-preference.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
@license https://github.com/t2ym/i18n-behavior/blob/master/LICENSE.md
Copyright (c) 2019, Tetsuya Mori <[email protected]>. 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);