Merge pull request #211 from ing-bank/chore/improveLocalizeDocs

[localize] Improve documentation
This commit is contained in:
Thijs Louisse 2019-08-01 17:26:47 +02:00 committed by GitHub
commit 635b09743d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 78 additions and 60 deletions

View file

@ -4,11 +4,11 @@
The localization system helps to manage localization data split into locales and automate its loading. The localization system helps to manage localization data split into locales and automate its loading.
The loading of data tries to be as unobtrusive as possible for a typical workflow while providing a flexible and controllable mechanism for non-trivial use cases. The loading of data tries to be as unobtrusive as possible for a typical workflow while providing a flexible and controllable mechanism for non-trivial use cases.
The formatting of data containing numbers and dates takes current locale into accout by using Intl MessageFormat specification. The formatting of data containing numbers and dates takes current locale into account by using Intl MessageFormat specification.
## LocalizeManager ## LocalizeManager
The core of the system is a `LocalizeManager` instance which is responsible for data loading and working with this data. The core of the system is a `LocalizeManager` instance which is responsible for data loading and working with these data.
It is exposed as a `localize` singleton instance. It is exposed as a `localize` singleton instance.
This ensures that the data can be cached in the single place and reused across different components and same component instances. This ensures that the data can be cached in the single place and reused across different components and same component instances.
@ -25,12 +25,14 @@ Component developers will have a unified way to integrate with localization syst
As a component developer you get: As a component developer you get:
- unified data structure for different locales; - unified data structure for different locales;
- promisified helper to load data; - async helper to load data;
- notification about page locale changes; - notification about page locale changes;
- formatting using Intl MessageFormat; - formatting using Intl MessageFormat;
- mixins simplifying integration with components. - mixins simplifying integration with components.
Data is split into locales. ### Storing data
Data are split into locales.
Typically the locale is an ES module which is by convention put into the `/translations` directory of your project. Typically the locale is an ES module which is by convention put into the `/translations` directory of your project.
But there is also a possibility to serve data from an API using JSON format. But there is also a possibility to serve data from an API using JSON format.
Localization data modules for `my-hello-component` might look like these: Localization data modules for `my-hello-component` might look like these:
@ -80,11 +82,13 @@ The approach with ES modules is great because it allows to simply reuse basic lo
}; };
``` ```
To load this data the method `loadNamespace()` which returns a promise can be used. ### Loading data
Async method `loadNamespace()` can be used to load these data.
```js ```js
localize.loadNamespace(namespace).then(() => { localize.loadNamespace(namespace).then(() => {
// do smth when data is loaded // do smth when data are loaded
}); });
``` ```
@ -106,7 +110,7 @@ localize.loadNamespace({
Usage of dynamic imports is recommended if you want to be able to create smart bundles later on for a certain locale. Usage of dynamic imports is recommended if you want to be able to create smart bundles later on for a certain locale.
The module must have a `default` export as shown above to be handled properly. The module must have a `default` export as shown above to be handled properly.
But in fact you are not limited in the way how exactly the data is loaded. But in fact you are not limited in the way how exactly the data are loaded.
If you want to fetch it from some API this is also possible. If you want to fetch it from some API this is also possible.
```js ```js
@ -114,7 +118,7 @@ If you want to fetch it from some API this is also possible.
localize.loadNamespace({ localize.loadNamespace({
'my-hello-component': async locale => { 'my-hello-component': async locale => {
const response = await fetch( const response = await fetch(
`http://api.example.com/?namespace=my-hello-component&locale=${locale}`, `https://api.example.com/?namespace=my-hello-component&locale=${locale}`,
); );
return response.json(); // resolves to the JSON object `{ greeting: 'Hallo {name}!' }` return response.json(); // resolves to the JSON object `{ greeting: 'Hallo {name}!' }`
}, },
@ -129,62 +133,79 @@ And this is there the second option comes in handy.
```js ```js
// using the regexp to match all component names staring with 'my-' // using the regexp to match all component names staring with 'my-'
localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => { localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
const response = await fetch(`http://api.example.com/?namespace=${namespace}&locale=${locale}`); const response = await fetch(`https://api.example.com/?namespace=${namespace}&locale=${locale}`);
return response.json(); return response.json();
}); });
Promise.all([ await Promise.all([
localize.loadNamespace('my-hello-component'); localize.loadNamespace('my-hello-component');
localize.loadNamespace('my-goodbuy-component'); localize.loadNamespace('my-goodbye-component');
]) ]);
``` ```
Thus there is a loder function for all components having a certain prefix in a name. Thus there is a loader function for all components having a certain prefix in a name.
The locale which will be loaded by default is accesed via the `localize.locale`. ### Page locale
The single source of truth for page's locale is `<html lang="my-LOCALE">`. The single source of truth for page locale is `<html lang="my-LOCALE">`.
At the same time the interaction should happen via `localize.locale` getter/setter to be able to notify and react to the change. Accessing it via `localize.locale` getter/setter is preferable.
External tools like Google Chrome translate or a demo plugin to change a page locale (e.g. in the Storybook) can directly change the attribute on the `<html>` tag.
If `<html lang>` is empty the default locale will be set to `en-GB`.
#### Changing the page locale
Changing the locale is simple yet powerful way to change the language on the page without reloading it:
```js ```js
localize.addEventListener('localeChanged', () => { localize.addEventListener('localeChanged', () => {
// do smth when data is loaded for a new locale // do smth when data are loaded for a new locale
}); });
// changes locale, syncs to `<html lang="es-ES">` and fires the event above // change locale (syncs to `<html lang="es-ES">` and fires the event above)
localize.locale = 'es-ES'; localize.locale = 'es-ES';
``` ```
If the locale is changed when a few namespaces have been already loaded for the previous one, all the data will be requested for existing namespaces for a new locale and only after that the event listeneres will be called. If the locale is changed when a few namespaces have been already loaded for the previous one, all the data will be requested for existing namespaces for a new locale and only after that the event listeners will be called.
This ensures that all data necessary for localization is loaded prior to rendering. This ensures that all data necessary for localization is loaded prior to rendering.
If a certain namespace for a certain locale has been loaded previously, it will never be fetched again until the tab is reloaded in the browser. If a certain namespace for a certain locale has been loaded previously, it will never be fetched again until the tab is reloaded in the browser.
When all necessary data is loaded and you want to show localized content on the page you need to format the data. #### Fallback locale
Due to the need to develop application code with not everything translated (yet) to all languages, it is good to have a fallback.
By default the fallback is `en-GB`, meaning that if some namespace does not have data for the current page locale, the `en-GB` will be loaded, making it an important foundation for all other locales data.
In addition to that the fallback is a necessary mechanism to allow some features of the browsers like Google Chrome translate to work and use the same original data for translations into all not officially supported languages.
### Rendering data
When all necessary data are loaded and you want to show localized content on the page you need to format the data.
`localize.msg` comes into play here. `localize.msg` comes into play here.
It expects a key in the format of `namespace:name` and can also receive variables as a second argument. It expects a key in the format of `namespace:name` and can also receive variables as a second argument.
```js ```js
_onNameChanged() { _onNameChanged() {
// inserts 'Hello John!' into the element with id="name" // inserts 'Hello John!' into the element with id="name"
const name = localize.msg('my-hello-component:greeting', { name: 'John' }); const greeting = localize.msg('my-hello-component:greeting', { name: 'John' });
this.$idNameElement.innerText = name; this.shadowRoot.querySelector('#greeting').innerText = greeting;
} }
``` ```
`localize.msg` uses [Intl MessageFormat implementation](https://www.npmjs.com/package/message-format) under the hood, so you can use all of its powerful features like placing a little bit different content based on number ranges or format a date according to the current locale. `localize.msg` uses [Intl MessageFormat implementation](https://www.npmjs.com/package/message-format) under the hood, so you can use all of its powerful features like placing a little bit different content based on number ranges or format a date according to the current locale.
### Using with LocalizeMixin #### Rendering with LocalizeMixin
This mixin was created to significantly simplify integration with LionLitElement. This mixin was created to significantly simplify integration with LitElement.
It provides several capabilities: It provides many capabilities:
- automatic loading of specified namespaces; - automatic loading of specified namespaces;
- life-cycle callbacks for localization events; - control of the rendering flow via `waitForLocalizeNamespaces`;
- alias `_m` for `localize.msg`; - smart wrapper `msgLit` for `localize.msg`;
- promisified alias `_msgAsync` for `localize.msg` resolved when data is loaded. - life-cycle callbacks and properties for localization events;
- automatic update of DOM after locale was changed.
```js ```js
class MyHelloComponent extends LocalizeMixin(LionLitElement) { class MyHelloComponent extends LocalizeMixin(LitElement) {
static get localizeNamespaces() { static get localizeNamespaces() {
// using an explicit loader function // using an explicit loader function
return [ return [
@ -196,49 +217,46 @@ class MyHelloComponent extends LocalizeMixin(LionLitElement) {
return ['my-hello-component', ...super.localizeNamespaces]; return ['my-hello-component', ...super.localizeNamespaces];
} }
setupShadowDom() { static get waitForLocalizeNamespaces() {
// setup initial DOM with ids for insertion points // return false to unblock the rendering till the data are loaded
return true;
}
render() {
return html`
<!-- use this.msgLit() here to inject data, e.g.: -->
<span>${this.msgLit('my-hello-component:greeting')}</span>
`;
} }
onLocaleReady() { onLocaleReady() {
// life-cycle callback - when data is loaded for initial locale super.onLocaleReady();
// life-cycle callback - when data are loaded for initial locale
// (reaction to loaded namespaces defined in `localizeNamespaces`) // (reaction to loaded namespaces defined in `localizeNamespaces`)
} }
onLocaleChanged() { onLocaleChanged() {
// life-cycle callback - when data is loaded for new locale super.onLocaleChanged();
// life-cycle callback - when data are loaded for new locale
// (reaction to `localize.locale` change and namespaces loaded for it) // (reaction to `localize.locale` change and namespaces loaded for it)
} }
onLocaleUpdated() { onLocaleUpdated() {
super.onLocaleUpdated();
// life-cycle callback - when localized content needs to be updated // life-cycle callback - when localized content needs to be updated
// (literally after `onLocaleReady` or `onLocaleChanged`) // (literally after `onLocaleReady` or `onLocaleChanged`)
// most DOM updates should be done here with the help of `this.msgLit()` and cached id selectors // most DOM updates should be done here with the help of `this.msgLit()` and cached id selectors
} }
async inYourOwnMethod() {
// before data are loaded or reloaded
await this.localizeNamespacesLoaded;
// after data are loaded or reloaded
}
} }
``` ```
Refer to demos to see a full example. In the majority of cases defining `localizeNamespaces` and using `msgLit` in the `render` is enough to have a fully working localized component.
### Using with LocalizeLitRenderMixin
This is an extension of LocalizeMixin for usage with LionLitElement and LitRenderMixin.
It provides extra capabilities on top of LocalizeMixin:
- smart wrapper `msg` for `localize.msg`;
- automatic update of DOM after locale was changed.
With the help of this mixin writing a component can be as easy as defining namespaces in `localizeNamespaces` and writing lit-html template using `this.msgLit()`:
```js
render() {
return html`
<div>${this.name ? this.msgLit('my-hello-component:greeting', { name: this.name }) : ''}</div>
`;
}
```
Refer to demos to see a full example.
## Usage for application developers ## Usage for application developers
@ -246,8 +264,8 @@ As an application developer you get:
- ability to inline localization data for any locales and namespaces to prevent async loading and improve rendering speed in critical cases; - ability to inline localization data for any locales and namespaces to prevent async loading and improve rendering speed in critical cases;
- smart defaults for data loading; - smart defaults for data loading;
- simple customization of paths where the data is loaded from for common use cases; - simple customization of paths where the data are loaded from for common use cases;
- full control over how the data is loaded for very specific use cases; - full control over how the data are loaded for very specific use cases;
### Inlining of data ### Inlining of data
@ -267,7 +285,7 @@ localize.addData('nl-NL', 'my-namespace', {
import './my-inlined-data.js'; // must be on top to be executed before any other code using the data import './my-inlined-data.js'; // must be on top to be executed before any other code using the data
``` ```
This code must come before any other code which might potentially render before the data is added. This code must come before any other code which might potentially render before the data are added.
You can inline as much locales as you support or sniff request headers on the server side and inline only the needed one. You can inline as much locales as you support or sniff request headers on the server side and inline only the needed one.
### Customize loading ### Customize loading
@ -282,14 +300,14 @@ This is sort of a router for the data and is typically needed to fetch it from a
// for one specific component // for one specific component
localize.setupNamespaceLoader('my-hello-component', async locale => { localize.setupNamespaceLoader('my-hello-component', async locale => {
const response = await fetch( const response = await fetch(
`http://api.example.com/?namespace=my-hello-component&locale=${locale}`, `https://api.example.com/?namespace=my-hello-component&locale=${locale}`,
); );
return response.json(); return response.json();
}); });
// for all components which have a prefix in their names // for all components which have a prefix in their names
localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => { localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
const response = await fetch(`http://api.example.com/?namespace=${namespace}&locale=${locale}`); const response = await fetch(`https://api.example.com/?namespace=${namespace}&locale=${locale}`);
return response.json(); return response.json();
}); });
``` ```

View file

@ -226,7 +226,7 @@ describe('LocalizeMixin', () => {
expect(updateSpy.callCount).to.equal(1); expect(updateSpy.callCount).to.equal(1);
}); });
it('has msg() which integrates with lit-html', async () => { it('has msgLit() which integrates with lit-html', async () => {
const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) }; const myElementNs = { 'my-element': locale => fakeImport(`./my-element/${locale}.js`) };
setupFakeImport('./my-element/en-GB.js', { setupFakeImport('./my-element/en-GB.js', {
default: { default: {