i18n & localization

Although any localization solution can be used, Open Cells offers a localization helper that can be used directly on Web Components or applied as a mixin. It relies in two principles:

  • Components that contain messages inside them that are language-dependant should use the t function to manage them.
  • The application should provide the final texts or resources (locales) in JSON format and manage the internationalization configuration.

This helper uses Format.JS Intl MessageFormat to help applying specific formats to translations based on language and other parameters.

Any component using localization should install the package as a dependency:

npm install --save @open-cells/localize

The t function is the main piece provided by this module. It can be directly imported from the module and used wherever needed. Example:

import {LitElement, html} from 'lit';
import {t, updateWhenLocaleResourcesChange} from '@open-cells/localize';

class ExampleComponent extends LitElement {
  constructor() {
    super();
    updateWhenLocaleResourcesChange(this);
  }

  render() {
    return html` <p>${t('simple-key')}</p> `;
  }
}

The t function will look for simple-key in the available locale resources and for the current application language. If found, the value will be passed to Intl MessageFormat (and the result will be cached for future uses), then returned. If the key is not found, t will return null.

updateWhenLocaleResourcesChanges is a helper function that adds a Lit Controller to the component that listens for global intl status change events and triggers a component update accordingly. Thus, if the application language is updated (from 'en-US' to 'es-ES', for example), the component will be automatically re-rendered and its messages will be updated to the new language.

Note: if no resources are loaded when updateWhenLocaleResourcesChanges is called, it will automatically try to fetch the resources using the current configuration.

By default, the localization helper will try to fetch a locales/locales.json file from the current path. As it's explained below, this can be easily configured. Anyway, the JSON file should contain entries for each language with all keys in components. Example:

{
  "en": {
    "simple-key": "Value",
    "key-a": "Value A",
    "key-b": "Value B"
  },
  "en-US": {
    "key-b": "Value B for US"
  },
  "es": {
    "simple-key": "Valor",
    "key-a": "Valor A",
    "key-b": "Valor B"
  },
  "es-ES": {
    "key-b": "Valor B para ES"
  },
  "es-MX": {
    "key-b": "Valor B para MX"
  }
}

When the language code used in the application includes a region code (like 'en-US' or 'es-ES'), t will look for the key in the locale resources for that specific code first. If the key is not found, then it will look for it in the base language code (like 'en' or 'es'), without the region part. This allows to define a base set of translations for a language and then override them with specific translations for a region.

In the following example, when translating 'key-b' for 'en-US', the value 'Value B for US' will be used. But when translating 'key-b' for 'en-GB', the value 'Value B' (retrieved from 'en' base language) will be used.

{
  "en": {
    "key-a": "Value A",
    "key-b": "Value B"
  },
  "en-US": {
    "key-b": "Value B for US"
  }
}

When there are no resources loaded, or a specific key is not found in the locale resources for the current language, null will be returned. So, it's easy to provide fallbacks for not-found translations. For example:

 render() {
    return html`
      <p>${t('simple-key') ?? 'This is a fallback text'}</p>
    `;
  }

Using the Nullish coalescing operator (??) ensures that if the key is found but its value is an empty string, it does not get overriden by the fallback.

The values retrieved from locales will be passed to Intl MessageFormat. This allows to use parameters and gives a lot of flexibility to the system. For example, a component could need to show a message with a dynamic value, but adjusting the text message based on that value. Example:

  render() {
    return html`
      <p>${t('simple-key', { numItems: this.items })}</p>
    `;
  }
{
  "en": {
    "simple-key": "You have {numItems, plural, =0 {no elements.} =1 {one element.} other {# elements.}}"
  },
  "es": {
    "simple-key": "{numItems, plural, =0 {No tienes elementos.} =1 {Tienes un elemento.} other {Tienes # elementos.}}"
  }
}

You can pass other parameters for the available formatting options as documented in Intl MessageFormat. Examples:

{
  "en": {
    "html-key": "This is a <b>value</b>",
    "simple-key-plural": "You have {numItems, plural, =0 {no elements.} =1 {one element.} other {# elements.}}",
    "simple-key-date": "Starts on {exampleDate, date, medium}",
    "simple-key-intl-date-lang-demo": "The date is {exampleDate, date, long}"
  }
}
  render() {
    return html`
      <p>${t('html-key', {'b': chunks => html`<strong>${chunks}</strong>`})}</p>
      <p>${t('simple-key-plural', { numItems: this.items })}</p>
      <p>${t('simple-key-date', {'exampleDate': new Date()})}</p>
      <p>${t('simple-key-intl-date-lang-demo', {'exampleDate': new Date()})}</p>
    `;
  }

The package offers a mixin, LocalizeMixin, which automatically imports the t method, invokes updateWhenLocaleResourcesChange, and assigns t to a method in the component interface. Example:

import {LitElement, html} from 'lit';
import {LocalizeMixin} from '@open-cells/localize';

class ExampleComponent extends LocalizeMixin(LitElement) {
  render() {
    return html` <p>${this.t('simple-key')}</p> `;
  }
}

You can also combine the mixin with the imported t method:

import {LitElement, html} from 'lit';
import {LocalizeMixin, t} from '@open-cells/localize';

class ExampleComponent extends LocalizeMixin(LitElement) {
  render() {
    return html` <p>${t('simple-key')}</p> `;
  }
}

The mixin also adds a _intlConfig state to the component, which will be updated when the global intl configuration changes. This property allows to get the current state of the intl configuration in the component. Example:

import { LitElement, html } from 'lit';
import { LocalizeMixin } from '@open-cells/localize';

class ExampleComponent extends LocalizeMixin(LitElement) {
  willUpdate(props) {
    super?.willUpdate(props);
    if (props.has('_intlConfig')) {
      this._globalIntlLang = this._intlConfig.lang;
    }
  }

  render() {
    return html`
      <p>${this.t('simple-key')}</p>
      <p>Current language is: ${this._globalIntlLang}</p>
    `;
  }
}

This is useful, for instance, when a component has other language-dependant features; for example, if the component needs to show the first day of the week (which could be sunday or monday, generally).

The global configuration is stored in the module state. The module provides a set of methods for managing and updating the configuration.

setLang updates the current language used in the application. It will also update the document.documentElement.lang attribute.

import {setLang} from '@open-cells/localize';

setLang('es-ES');

Note: intl lang defaults to the <html> tag lang attribute. In order to define the initial language in your application, you can just set that attribute. For example:

<html lang="es">
  ...
</html>

setFormats allows to set formats for Intl MessageFormat.

import {setFormats} from '@open-cells/localize';

setFormats({
  number: {
    currency: {
      style: 'currency',
      currency: 'USD',
      currencyDisplay: 'symbol',
    },
  },
});

If setWarnOnMissingKeys is invoked with true, it will show a console warning when a key is not found in the locale resources. This can be useful for debugging, in order to locate missing translations in your app.

import {setWarnOnMissingKeys} from '@open-cells/localize';

setWarnOnMissingKeys(true);

The path for locale resources is set using two properties: url and localesHost. They default to locales/locales.json and . respectively, so the default path for locales is ./locales/locales.json.

The resources are not automatically fetched when the module is loaded. This allows the user to modify localesHost and url without triggering any requests, using the setLocalesHost and setUrl methods. When the resources are needed, the requestResources method can be used to fetch them.

import {setUrl, setLocalesHost, requestResources} from '@open-cells/localize';

setLocalesHost('base/path/for/app');
setUrl('locales/app-locales.json');

// Manually retrieve resources
requestResources();

Alternatively, instead of manually invoking requestResources, the first component that uses updateWhenLocaleResourcesChange will automatically trigger the request.

Thus, apps should set all their initial configuration parameters (localesHost, url, lang...) before any other component is loaded; then, it could just wait for the first component using updateWhenLocaleResourcesChange to trigger the request.

After resources are retrieved, any new modification made to localesHost or url will automatically trigger a new request. The new resources will override the previous ones. You can set useBundles to true to merge the new resources with the previous ones instead of replacing them.

import {
  setUrl,
  setLocalesHost,
  requestResources,
  setUseBundles,
} from '@open-cells/localize';

setUseBundles(true);
setLocalesHost('base/path/for/app');
setUrl('locales/app-locales.json');

// Manually retrieve resources
requestResources();

setTimeout(() => {
  setUrl('locales/other-locales.json');
}, 3000);

The module fires events on window to notify about changes.

  • app-localize-resources-loaded is fired when a locale resources file is loaded and is available for use.
  • app-localize-resources-error is fired when an error occurs while loading resources.
  • app-localize-status-change is fired when the intl configuration changes; for example, when the language is updated from en to es, when the formats are updated, resources are loaded, etc.

The default lang of the module will be set to the document.documentElement.lang property, which matches the lang attribute in <html> tag. When loaded, the intl module automatically sets a MutationObserver on the lang attribute of <html>: when it changes, the module will update the lang config property accordingly.