Merge pull request #211 from ing-bank/chore/improveLocalizeDocs
[localize] Improve documentation
This commit is contained in:
commit
635b09743d
2 changed files with 78 additions and 60 deletions
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
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 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
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
- unified data structure for different locales;
|
||||
- promisified helper to load data;
|
||||
- async helper to load data;
|
||||
- notification about page locale changes;
|
||||
- formatting using Intl MessageFormat;
|
||||
- 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.
|
||||
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:
|
||||
|
|
@ -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
|
||||
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.
|
||||
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.
|
||||
|
||||
```js
|
||||
|
|
@ -114,7 +118,7 @@ If you want to fetch it from some API this is also possible.
|
|||
localize.loadNamespace({
|
||||
'my-hello-component': async locale => {
|
||||
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}!' }`
|
||||
},
|
||||
|
|
@ -129,62 +133,79 @@ And this is there the second option comes in handy.
|
|||
```js
|
||||
// using the regexp to match all component names staring with 'my-'
|
||||
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();
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
await Promise.all([
|
||||
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">`.
|
||||
At the same time the interaction should happen via `localize.locale` getter/setter to be able to notify and react to the change.
|
||||
The single source of truth for page locale is `<html lang="my-LOCALE">`.
|
||||
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
|
||||
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';
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
It expects a key in the format of `namespace:name` and can also receive variables as a second argument.
|
||||
|
||||
```js
|
||||
_onNameChanged() {
|
||||
// inserts 'Hello John!' into the element with id="name"
|
||||
const name = localize.msg('my-hello-component:greeting', { name: 'John' });
|
||||
this.$idNameElement.innerText = name;
|
||||
const greeting = localize.msg('my-hello-component:greeting', { name: 'John' });
|
||||
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.
|
||||
|
||||
### Using with LocalizeMixin
|
||||
#### Rendering with LocalizeMixin
|
||||
|
||||
This mixin was created to significantly simplify integration with LionLitElement.
|
||||
It provides several capabilities:
|
||||
This mixin was created to significantly simplify integration with LitElement.
|
||||
It provides many capabilities:
|
||||
|
||||
- automatic loading of specified namespaces;
|
||||
- life-cycle callbacks for localization events;
|
||||
- alias `_m` for `localize.msg`;
|
||||
- promisified alias `_msgAsync` for `localize.msg` resolved when data is loaded.
|
||||
- control of the rendering flow via `waitForLocalizeNamespaces`;
|
||||
- smart wrapper `msgLit` for `localize.msg`;
|
||||
- life-cycle callbacks and properties for localization events;
|
||||
- automatic update of DOM after locale was changed.
|
||||
|
||||
```js
|
||||
class MyHelloComponent extends LocalizeMixin(LionLitElement) {
|
||||
class MyHelloComponent extends LocalizeMixin(LitElement) {
|
||||
static get localizeNamespaces() {
|
||||
// using an explicit loader function
|
||||
return [
|
||||
|
|
@ -196,49 +217,46 @@ class MyHelloComponent extends LocalizeMixin(LionLitElement) {
|
|||
return ['my-hello-component', ...super.localizeNamespaces];
|
||||
}
|
||||
|
||||
setupShadowDom() {
|
||||
// setup initial DOM with ids for insertion points
|
||||
static get waitForLocalizeNamespaces() {
|
||||
// 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() {
|
||||
// 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`)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
onLocaleUpdated() {
|
||||
super.onLocaleUpdated();
|
||||
// life-cycle callback - when localized content needs to be updated
|
||||
// (literally after `onLocaleReady` or `onLocaleChanged`)
|
||||
// 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.
|
||||
|
||||
### 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.
|
||||
In the majority of cases defining `localizeNamespaces` and using `msgLit` in the `render` is enough to have a fully working localized component.
|
||||
|
||||
## 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;
|
||||
- smart defaults for data loading;
|
||||
- simple customization of paths where the data is loaded from for common use cases;
|
||||
- full control over how the data is loaded for very specific use cases;
|
||||
- simple customization of paths where the data are loaded from for common use cases;
|
||||
- full control over how the data are loaded for very specific use cases;
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
localize.setupNamespaceLoader('my-hello-component', async locale => {
|
||||
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();
|
||||
});
|
||||
|
||||
// for all components which have a prefix in their names
|
||||
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();
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ describe('LocalizeMixin', () => {
|
|||
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`) };
|
||||
setupFakeImport('./my-element/en-GB.js', {
|
||||
default: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue