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 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();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue