Merge pull request #153 from ing-bank/fix/htmlLangObserver
Observe <html lang> attribute
This commit is contained in:
commit
481012dc19
4 changed files with 155 additions and 53 deletions
|
|
@ -11,15 +11,22 @@ export class LocalizeManager extends LionSingleton {
|
|||
super(params);
|
||||
this._fakeExtendsEventTarget();
|
||||
|
||||
if (!this.locale) {
|
||||
this.locale = 'en-GB';
|
||||
if (!document.documentElement.lang) {
|
||||
document.documentElement.lang = 'en-GB';
|
||||
}
|
||||
|
||||
this._autoLoadOnLocaleChange = !!params.autoLoadOnLocaleChange;
|
||||
this.__storage = {};
|
||||
this.__namespacePatternsMap = new Map();
|
||||
this.__namespaceLoadersCache = {};
|
||||
this.__namespaceLoaderPromisesCache = {};
|
||||
this.formatNumberOptions = { returnIfNaN: '' };
|
||||
|
||||
this._setupHtmlLangAttributeObserver();
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this._teardownHtmlLangAttributeObserver();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
|
@ -29,7 +36,11 @@ export class LocalizeManager extends LionSingleton {
|
|||
|
||||
set locale(value) {
|
||||
const oldLocale = document.documentElement.lang;
|
||||
|
||||
this._teardownHtmlLangAttributeObserver();
|
||||
document.documentElement.lang = value;
|
||||
this._setupHtmlLangAttributeObserver();
|
||||
|
||||
this._onLocaleChanged(value, oldLocale);
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +101,25 @@ export class LocalizeManager extends LionSingleton {
|
|||
return formatter.format(vars);
|
||||
}
|
||||
|
||||
_setupHtmlLangAttributeObserver() {
|
||||
if (!this._htmlLangAttributeObserver) {
|
||||
this._htmlLangAttributeObserver = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
this._onLocaleChanged(document.documentElement.lang, mutation.oldValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
this._htmlLangAttributeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['lang'],
|
||||
attributeOldValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
_teardownHtmlLangAttributeObserver() {
|
||||
this._htmlLangAttributeObserver.disconnect();
|
||||
}
|
||||
|
||||
_isNamespaceInCache(locale, namespace) {
|
||||
return !!(this.__storage[locale] && this.__storage[locale][namespace]);
|
||||
}
|
||||
|
|
@ -181,6 +211,9 @@ export class LocalizeManager extends LionSingleton {
|
|||
}
|
||||
|
||||
_onLocaleChanged(newLocale, oldLocale) {
|
||||
if (newLocale === oldLocale) {
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('localeChanged', { detail: { newLocale, oldLocale } }));
|
||||
if (this._autoLoadOnLocaleChange) {
|
||||
this._loadAllMissing(newLocale, oldLocale);
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ export let localize = LocalizeManager.getInstance({
|
|||
});
|
||||
|
||||
export function setLocalize(newLocalize) {
|
||||
localize.teardown();
|
||||
localize = newLocalize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, oneEvent } from '@open-wc/testing';
|
||||
import { expect, oneEvent, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { fetchMock } from '@bundled-es-modules/fetch-mock';
|
||||
import { setupFakeImport, resetFakeImport, fakeImport } from './test-utils.js';
|
||||
|
||||
|
|
@ -10,47 +11,81 @@ function removeLtrRtl(str) {
|
|||
}
|
||||
|
||||
describe('LocalizeManager', () => {
|
||||
let manager;
|
||||
|
||||
beforeEach(() => {
|
||||
// makes sure that between tests the localization is reset to default state
|
||||
document.documentElement.lang = 'en-GB';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
manager.teardown();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
resetFakeImport();
|
||||
});
|
||||
|
||||
it('initializes locale from <html> by default', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
expect(manager.locale).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('syncs locale back to <html> if changed', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.locale = 'nl-NL';
|
||||
expect(document.documentElement.lang).to.equal('nl-NL');
|
||||
});
|
||||
|
||||
it('sets locale to "en-GB" by default if nothing is set on <html>', () => {
|
||||
document.documentElement.lang = '';
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
expect(manager.locale).to.equal('en-GB');
|
||||
expect(document.documentElement.lang).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('fires "localeChanged" event with detail.newLocale and detail.oldLocale if locale was changed', async () => {
|
||||
const manager = new LocalizeManager();
|
||||
setTimeout(() => {
|
||||
manager.locale = 'en-US';
|
||||
it('has teardown() method removing all side effects', () => {
|
||||
manager = new LocalizeManager();
|
||||
const disconnectObserverSpy = sinon.spy(manager._htmlLangAttributeObserver, 'disconnect');
|
||||
manager.teardown();
|
||||
expect(disconnectObserverSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('"localeChanged" event with detail.newLocale and detail.oldLocale', () => {
|
||||
it('fires "localeChanged" event if locale was changed via manager', async () => {
|
||||
manager = new LocalizeManager();
|
||||
setTimeout(() => {
|
||||
manager.locale = 'en-US';
|
||||
});
|
||||
const event = await oneEvent(manager, 'localeChanged');
|
||||
expect(event.detail.newLocale).to.equal('en-US');
|
||||
expect(event.detail.oldLocale).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('fires "localeChanged" event if locale was changed via <html lang> attribute', async () => {
|
||||
manager = new LocalizeManager();
|
||||
setTimeout(() => {
|
||||
document.documentElement.lang = 'en-US';
|
||||
});
|
||||
const event = await oneEvent(manager, 'localeChanged');
|
||||
expect(event.detail.newLocale).to.equal('en-US');
|
||||
expect(event.detail.oldLocale).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('does not fire "localeChanged" event if it was set to the same locale', () => {
|
||||
manager = new LocalizeManager();
|
||||
const eventSpy = sinon.spy();
|
||||
manager.addEventListener('localeChanged', eventSpy);
|
||||
manager.locale = 'en-US';
|
||||
manager.locale = 'en-US';
|
||||
expect(eventSpy.callCount).to.equal(1);
|
||||
});
|
||||
const event = await oneEvent(manager, 'localeChanged');
|
||||
expect(event.detail.newLocale).to.equal('en-US');
|
||||
expect(event.detail.oldLocale).to.equal('en-GB');
|
||||
});
|
||||
|
||||
describe('addData()', () => {
|
||||
it('allows to provide inline data', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
|
||||
|
||||
|
|
@ -85,7 +120,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('prevents mutating existing data for the same locale & namespace', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
|
||||
|
||||
|
|
@ -103,7 +138,7 @@ describe('LocalizeManager', () => {
|
|||
it('loads a namespace via loadNamespace()', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
await manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
|
|
@ -119,7 +154,7 @@ describe('LocalizeManager', () => {
|
|||
it('can load a namespace for a different locale', async () => {
|
||||
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.locale = 'en-US';
|
||||
|
||||
await manager.loadNamespace(
|
||||
|
|
@ -140,7 +175,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-defaults/en-GB.js', { default: { submit: 'Submit' } });
|
||||
setupFakeImport('./my-send-button/en-GB.js', { default: { submit: 'Send' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
await manager.loadNamespaces([
|
||||
{ 'my-defaults': locale => fakeImport(`./my-defaults/${locale}.js`) },
|
||||
|
|
@ -159,7 +194,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-defaults/nl-NL.js', { default: { submit: 'Submit' } });
|
||||
setupFakeImport('./my-send-button/nl-NL.js', { default: { submit: 'Send' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.locale = 'en-US';
|
||||
|
||||
await manager.loadNamespaces(
|
||||
|
|
@ -181,7 +216,7 @@ describe('LocalizeManager', () => {
|
|||
it('fallbacks to language file if locale file is not found', async () => {
|
||||
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
await manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
|
|
@ -195,7 +230,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('throws if both locale and language files could not be loaded', async () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
try {
|
||||
await manager.loadNamespace({
|
||||
|
|
@ -218,7 +253,7 @@ describe('LocalizeManager', () => {
|
|||
it('loads a namespace via loadNamespace() using string route', async () => {
|
||||
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader('my-component', async locale => {
|
||||
const response = await fetch(`./my-component/${locale}.json`);
|
||||
|
|
@ -238,7 +273,7 @@ describe('LocalizeManager', () => {
|
|||
fetchMock.get('./my-defaults/en-GB.json', { submit: 'Submit' });
|
||||
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader('my-defaults', async locale => {
|
||||
const response = await fetch(`./my-defaults/${locale}.json`);
|
||||
|
|
@ -262,7 +297,7 @@ describe('LocalizeManager', () => {
|
|||
it('loads a namespace via loadNamespace() using RegExp route', async () => {
|
||||
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
|
||||
const response = await fetch(`./${namespace}/${locale}.json`);
|
||||
|
|
@ -282,7 +317,7 @@ describe('LocalizeManager', () => {
|
|||
fetchMock.get('./my-defaults/en-GB.json', { submit: 'Submit' });
|
||||
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
|
||||
const response = await fetch(`./${namespace}/${locale}.json`);
|
||||
|
|
@ -300,26 +335,12 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('loading extra features', () => {
|
||||
it('has a Promise "loadingComplete" that resolved once all pending loading is done', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
const manager = new LocalizeManager();
|
||||
|
||||
manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
expect(manager.__storage).to.deep.equal({});
|
||||
await manager.loadingComplete;
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('supports auto loading of namespaces when locale has been changed', async () => {
|
||||
describe('{ autoLoadOnLocaleChange: true }', () => {
|
||||
it('loads namespaces automatically when locale is changed via manager', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
|
||||
|
||||
const manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
|
||||
manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
|
||||
|
||||
await manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
|
|
@ -338,6 +359,46 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('loads namespaces automatically when locale is changed via <html lang> attribute', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
|
||||
|
||||
manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
|
||||
|
||||
await manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
|
||||
document.documentElement.lang = 'nl-NL';
|
||||
await aTimeout(); // wait for mutation observer to be called
|
||||
await manager.loadingComplete;
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
'nl-NL': { 'my-component': { greeting: 'Hallo!' } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading extra features', () => {
|
||||
it('has a Promise "loadingComplete" that resolved once all pending loading is done', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
manager = new LocalizeManager();
|
||||
|
||||
manager.loadNamespace({
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
expect(manager.__storage).to.deep.equal({});
|
||||
await manager.loadingComplete;
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('loads namespace only once for the same locale', async () => {
|
||||
let called = 0;
|
||||
const myNamespace = {
|
||||
|
|
@ -346,7 +407,7 @@ describe('LocalizeManager', () => {
|
|||
return Promise.resolve({ default: {} });
|
||||
},
|
||||
};
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
|
||||
await Promise.all([
|
||||
manager.loadNamespace(myNamespace),
|
||||
|
|
@ -360,7 +421,7 @@ describe('LocalizeManager', () => {
|
|||
it('does not load inlined data', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Loaded hello!' } });
|
||||
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-component', { greeting: 'Hello!' });
|
||||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
|
@ -386,25 +447,25 @@ describe('LocalizeManager', () => {
|
|||
|
||||
describe('message()', () => {
|
||||
it('gets the message for the key in the format of "namespace:name"', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', { greeting: 'Hello!' });
|
||||
expect(manager.msg('my-ns:greeting')).to.equal('Hello!');
|
||||
});
|
||||
|
||||
it('supports nested names in the format of "namespace:path.to.deep.name"', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', { 'login-section': { greeting: 'Hello!' } });
|
||||
expect(manager.msg('my-ns:login-section.greeting')).to.equal('Hello!');
|
||||
});
|
||||
|
||||
it('supports variables', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', { greeting: 'Hello {name}!' });
|
||||
expect(manager.msg('my-ns:greeting', { name: 'John' })).to.equal('Hello John!');
|
||||
});
|
||||
|
||||
it('supports Intl MessageFormat proposal for messages', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', {
|
||||
date: 'I was written on {today, date}.',
|
||||
number: 'You have {n, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
|
||||
|
|
@ -419,7 +480,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('takes into account globally changed locale', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.locale = 'nl-NL';
|
||||
manager.addData('en-GB', 'my-ns', { greeting: 'Hi!' });
|
||||
manager.addData('nl-NL', 'my-ns', { greeting: 'Hey!' });
|
||||
|
|
@ -427,7 +488,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('allows to provide a different locale for specific call', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
manager.addData('en-GB', 'my-ns', { greeting: 'Hi!' });
|
||||
manager.addData('nl-NL', 'my-ns', { greeting: 'Hey!' });
|
||||
expect(manager.msg('my-ns:greeting', null, { locale: 'nl-NL' })).to.equal('Hey!');
|
||||
|
|
@ -440,7 +501,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('allows to provide an ordered list of keys where the first resolved is used', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
const keys = ['overridden-ns:greeting', 'default-ns:greeting'];
|
||||
expect(manager.msg(keys)).to.equal('');
|
||||
manager.addData('en-GB', 'default-ns', { greeting: 'Hi!' });
|
||||
|
|
@ -450,7 +511,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
it('throws a custom error when namespace prefix is missing', () => {
|
||||
const manager = new LocalizeManager();
|
||||
manager = new LocalizeManager();
|
||||
const msgKey = 'greeting';
|
||||
manager.addData('en-GB', 'my-ns', { [msgKey]: 'Hello!' });
|
||||
expect(() => manager.msg(msgKey)).to.throw(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { LionSingleton } from '@lion/core';
|
||||
import { LocalizeManager } from '../src/LocalizeManager.js';
|
||||
|
|
@ -22,10 +23,16 @@ describe('localize', () => {
|
|||
|
||||
it('is overridable globally', () => {
|
||||
const oldLocalize = localize;
|
||||
const newLocalize = {};
|
||||
const oldLocalizeTeardown = localize.teardown;
|
||||
localize.teardown = sinon.spy();
|
||||
|
||||
const newLocalize = { teardown: () => {} };
|
||||
setLocalize(newLocalize);
|
||||
expect(localize).to.equal(newLocalize);
|
||||
expect(oldLocalize.teardown.callCount).to.equal(1);
|
||||
|
||||
setLocalize(oldLocalize);
|
||||
localize.teardown = oldLocalizeTeardown;
|
||||
});
|
||||
|
||||
it('is configured to automatically load namespaces if locale is changed', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue