From e76fa4c2dd9448d1e055c87a79114f687c062b76 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 29 Jul 2019 17:11:36 +0200 Subject: [PATCH 1/3] chore(localize): fix typo in test --- packages/localize/test/LocalizeMixin.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/localize/test/LocalizeMixin.test.js b/packages/localize/test/LocalizeMixin.test.js index f323a0ff0..8bf2c714b 100644 --- a/packages/localize/test/LocalizeMixin.test.js +++ b/packages/localize/test/LocalizeMixin.test.js @@ -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: { From 22c0e893c7a654c59cee9142b99c57c64eba8501 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Wed, 24 Jul 2019 18:11:59 +0200 Subject: [PATCH 2/3] chore(localize): fix typos in documentation --- packages/localize/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/localize/README.md b/packages/localize/README.md index 3bbf5965d..38abf3090 100644 --- a/packages/localize/README.md +++ b/packages/localize/README.md @@ -4,7 +4,7 @@ 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 @@ -135,13 +135,13 @@ localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => { 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`. +The locale which will be loaded by default is accessed via the `localize.locale`. The single source of truth for page's locale is ``. At the same time the interaction should happen via `localize.locale` getter/setter to be able to notify and react to the change. @@ -155,7 +155,7 @@ localize.addEventListener('localeChanged', () => { 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. From 8ecf9d0f2626bb8ece2801e1f9b466212fe53560 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Mon, 29 Jul 2019 17:14:23 +0200 Subject: [PATCH 3/3] chore(localize): update documentation --- packages/localize/README.md | 128 ++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/packages/localize/README.md b/packages/localize/README.md index 38abf3090..312a02735 100644 --- a/packages/localize/README.md +++ b/packages/localize/README.md @@ -8,7 +8,7 @@ The formatting of data containing numbers and dates takes current locale into ac ## 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,29 +133,37 @@ 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-goodbye-component'); -]) +]); ``` 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 accessed via the `localize.locale`. +### Page locale -The single source of truth for page's locale is ``. -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 ``. +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 `` tag. + +If `` 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 `` and fires the event above +// change locale (syncs to `` and fires the event above) localize.locale = 'es-ES'; ``` @@ -159,32 +171,41 @@ If the locale is changed when a few namespaces have been already loaded for the 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` + + ${this.msgLit('my-hello-component:greeting')} + `; } 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` -
${this.name ? this.msgLit('my-hello-component:greeting', { name: this.name }) : ''}
- `; -} -``` - -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(); }); ```