lit-localizedeprecated

Localization for lit-html

Usage no npm install needed!

<script type="module">
  import litLocalize from 'https://cdn.skypack.dev/lit-localize';
</script>

README

lit-localize

Published on npm Test Status

Features | Overview | Modes | Tutorial | API | Status event | Localized mixin | CLI | Config file | FAQ

lit-localize is a library and command-line tool for localizing web applications that are based on lit-html and LitElement.

Features

  • 🌐 Localize your lit-html and LitElement applications
  • 🔥 Safely embed HTML markup within localized templates
  • 🍦 Write vanilla code that works in development with no new tooling
  • 📄 Standard XLIFF interchange format
  • 🆓 Generate a zero-overhead bundle for each locale
  • 🔁 ... or dynamically load locales and automatically re-render

Overview

Wrap your template with the msg function to make it localizable:

import {html} from 'lit-html';
import {msg} from 'lit-localize';
render(msg('greeting', html`Hello <b>World</b>!`), document.body);

Run lit-localize to extract all localizable templates and generate an XLIFF file, a format which is supported by many localization tools and services:

<trans-unit id="greeting">
  <source>Hello <ph id="0">&lt;b></ph>World<ph id="1">&lt;/b></ph>!</source>
  <!-- target tag added by your localization process -->
  <target>Hola <ph id="0">&lt;b></ph>Mundo<ph id="1">&lt;/b></ph>!</target>
</trans-unit>

Use transform mode to generate an optimized bundle for each locale:

import {html} from 'lit-html';
render(html`Hola <b>Mundo</b>!`, document.body);

Alternatively, use runtime mode to dynamically switch locales without a page reload:

import {configureLocalization} from 'lit-localize';

const {setLocale} = configureLocalization({
  sourceLocale: 'en',
  targetLocales: ['es-419', 'zh_CN'],
  loadLocale: (locale) => import(`/locales/${locale}.js`),
});

(async () => {
  await setLocale('es-419');
  renderApplication();
})();

See examples/transform and examples/runtime for full working examples.

Modes

lit-localize supports two output modes: transform and runtime.

Transform mode Runtime mode
Output A full build of your application for each locale, with all msg calls replaced with static localized templates. A dynamically loadable template module for each target locale.
Make template localizable msg() msg()
Configure const {getLocale, setLocale} =
configureLocalization(...);
(Optional)

const {getLocale} =
configureTransformLocalization(...);
Switch locales Refresh page and load a different .js file Call setLocale() and re-render using any of:

- lit-localize-status event
- setLocale promise
- Localized mixin for LitElement
Advantages - Fastest rendering
- Fewer bytes for a single locale
- Faster locale switching
- Fewer marginal bytes when switching locales

Tutorial

  1. Install lit-localize. You get both a client library and a command-line tool. You'll always use both together.

    npm install --save lit-localize
    
  2. Set up a TypeScript lit-html project if you don't have one already:

    npm install --save lit-html
    npm install --save-dev typescript
    npx tsc --init
    

    You'll want these TypeScript settings:

    "compilerOptions": {
      "target": "es2018",
      "module": "esnext",
      "moduleResolution": "node"
    }
    
  3. Create an index.ts, and declare a localizable template using the msg function. The first argument is a unique identifier for this template, and the second is a string or lit-html template.

    (Note that this code will directly compile and run, just as it would if you were rendering the lit template directly, so your build process doesn't need to change until you want to integrate localized templates.)

    import {html, render} from 'lit-html';
    import {msg} from 'lit-localize';
    
    render(
      html`<p>${msg('greeting', html`Hello <b>World</b>!`)}</p>`,
      document.body
    );
    
  4. Make a JSON config file at the root of your project called lit-localize.json. In this example we're using transform mode, but you can also use runtime mode. The $schema property is optional, and lets editors like VSCode auto-complete and check for errors.

    {
      "$schema": "https://raw.githubusercontent.com/PolymerLabs/lit-localize/master/config.schema.json",
      "sourceLocale": "en",
      "targetLocales": ["es-419"],
      "tsConfig": "tsconfig.json",
      "output": {
        "mode": "transform"
      },
      "interchange": {
        "format": "xliff",
        "xliffDir": "xliff/"
      }
    }
    
  5. Run the lit-localize CLI:

    npx lit-localize
    
  6. Take a look at the generated XLIFF file xliff/es-419.xlf. Note that we have a <source> template extracted from your source code, but we don't have a localized version yet. Also note that embedded HTML markup has been encoded into <ph> tags.

    <trans-unit id="greeting">
      <source>Hello <ph id="0">&lt;b></ph>World<ph id="1">&lt;/b></ph>!</source>
    </trans-unit>
    
  7. Edit xliff/es-419.xlf to add a <target> tag containing a localized version of the template. Usually you would use a tool or service to generate this tag by feeding it this XLIFF file.

    <trans-unit id="greeting">
      <source>Hello <ph id="0">&lt;b></ph>World<ph id="1">&lt;/b></ph>!</source>
      <target>Hola <ph id="0">&lt;b></ph>Mundo<ph id="1">&lt;/b></ph>!</target>
    </trans-unit>
    
  8. Run lit-localize again:

    npx lit-localize
    
  9. Now take a look at the generated file es-419/index.js:

    import {html, render} from 'lit-html';
    render(html`<p>Hola <b>Mundo</b>!</p>`, document.body);
    

    and en/index.js:

    import {html, render} from 'lit-html';
    render(html`<p>Hello <b>World</b>!</p>`, document.body);
    

    Note that the localized template has been substituted into your code, the msg call has been removed, the lit-html template has been reduced to its simplest form, and the lit-localize module is no longer imported.

API

The lit-localize module exports the following functions:

Note that lit-localize relies on distinctive, annotated TypeScript type signatures to identify calls to msg and other APIs during analysis of your code. Casting a lit-localize function to a type that does not include its annotation will prevent lit-localize from being able to extract and transform templates from your application. For example, a cast like (msg as any)("greeting", "Hello") will not be identified. It is safe to re-assign lit-localize functions or pass them as parameters, as long as the distinctive type signature is preserved. If needed, you can reference each function's distinctive type with e.g. typeof msg.

configureLocalization(configuration)

Set configuration parameters for lit-localize when in runtime mode. Returns an object with functions:

Throws if called more than once.

When in transform mode, the lit-localize CLI will error if this function is called. Use configureTransformLocalization instead.

The configuration object must have the following properties:

  • sourceLocale: string: Required locale code in which source templates in this project are written, and the initial active locale.

  • targetLocales: Iterable<string>: Required locale codes that are supported by this project. Should not include the sourceLocale code.

  • loadLocale: (locale: string) => Promise<LocaleModule>: Required function that returns a promise of the localized templates for the given locale code. For security, this function will only ever be called with a locale that is contained by targetLocales.

It is recommended to use the output.localeCodesModule config file setting to generate a module which exports a sourceLocale string and targetLocales array that can be passed to configureLocalization, to ensure that your client remains in sync with your config file.

Example:

const {getLocale, setLocale} = configureLocalization({
  sourceLocale: 'en',
  targetLocales: ['es-419', 'zh_CN'],
  loadLocale: (locale) => import(`/${locale}.js`),
});

configureTransformLocalization(configuration)

Set configuration parameters for lit-localize when in transform mode. Returns an object with function:

(Note that setLocale is not available from this function, because changing locales at runtime is not supported in transform mode.)

Throws if called more than once.

The configuration object must have the following properties:

  • sourceLocale: string: Required locale code in which source templates in this project are written, and the active locale.

It is recommended to use the output.localeCodesModule config file setting to generate a module which exports a sourceLocale string that can be passed to configureTansformLocalization, to ensure that your client remains in sync with your config file.

Example:

const {getLocale} = configureLocalization({
  sourceLocale: 'en',
});

In transform mode, calls to this function are transformed to an object with a getLocale implementation that returns the static locale code for each locale bundle. For example:

const {getLocale} = {getLocale: () => 'es-419'};

getLocale() => string

Return the active locale code.

setLocale(locale: string) => Promise

Available only in runtime mode. Set the active locale code, and begin loading templates for that locale using the loadLocale function that was passed to configureLocalization. Returns a promise that resolves when the next locale is ready to be rendered.

Note that if a second call to setLocale is made while the first requested locale is still loading, then the second call takes precedence, and the promise returned from the first call will resolve when second locale is ready. If you need to know whether a particular locale was loaded, check getLocale after the promise resolves.

Throws if the given locale is not contained by the configured sourceLocale or targetLocales.

msg(id: string, template, ...args) => string|TemplateResult

Make a string or lit-html template localizable.

The id parameter is a project-wide unique identifier for this template.

The template parameter can have any of these types:

  • A plain string with no placeholders:

    msg('greeting', 'Hello World!');
    
  • A lit-html TemplateResult that may contain embedded HTML:

    msg('greeting', html`Hello <b>World</b>!`);
    
  • A function that returns a template literal string that may contain placeholders. Placeholders may only reference parameters of the function, which will be called with the 3rd and onwards parameters to msg.

    msg('greeting', (name) => `Hello ${name}!`, getUsername());
    
  • A function that returns a lit-html TemplateResult that may contain embedded HTML, and may contain placeholders. Placeholders may only reference parameters of the function, which will be called with the 3rd and onwards parameters to msg:

    msg('greeting', (name) => html`Hello <b>${name}</b>!`, getUsername());
    

In transform mode, calls to this function are replaced with the static localized template for each emitted locale. For example:

html`Hola <b>${getUsername()}!</b>`;

LOCALE_STATUS_EVENT

Name of the lit-localize-status event.

lit-localize-status event

In runtime mode, whenever a locale change starts, finishes successfully, or fails, lit-localize will dispatch a lit-localize-status event to window.

You can listen for this event to know when your application should be re-rendered following a locale change. See also the Localized mixin, which automatically re-renders LitElement classes using this event.

Event types

The detail.status string property tells you what kind of status change has occured, and can be one of: loading, ready, or error:

loading

A new locale has started to load. The detail object also contains:

  • loadingLocale: string: Code of the locale that has started loading.

A loading status can be followed by a ready, error, or loading status.

In the case that a second locale is requested before the first one finishes loading, a new loading event is dispatched, and no ready or error event will be dispatched for the first request, because it is now stale.

ready

A new locale has successfully loaded and is ready for rendering. The detail object also contains:

  • readyLocale: string: Code of the locale that has successfully loaded.

A ready status can be followed only by a loading status.

error

A new locale failed to load. The detail object also contains the following properties:

  • errorLocale: string: Code of the locale that failed to load.
  • errorMessage: string: Error message from locale load failure.

An error status can be followed only by a loading status.

Event example

import {LIT_LOCALIZE_STATUS} from 'lit-localize';

// Show/hide a progress indicator whenever a new locale is loading,
// and re-render the application every time a new locale successfully loads.
window.addEventListener(LIT_LOCALIZE_STATUS, (event) => {
  const spinner = document.querySelector('#spinner');
  if (event.detail.status === 'loading') {
    console.log(`Loading new locale: ${event.detail.loadingLocale}`);
    spinner.removeAttribute('hidden');
  } else if (event.detail.status === 'ready') {
    console.log(`Loaded new locale: ${event.detail.readyLocale}`);
    spinner.setAttribute('hidden', '');
    renderApplication();
  } else if (event.detail.status === 'error') {
    console.error(
      `Error loading locale ${event.detail.errorLocale}: ` +
        event.detail.errorMessage
    );
    spinner.setAttribute('hidden', '');
  }
});

Localized mixin

If you are using LitElement, then you can use the Localized mixin from lit-localize/localized-element.js to ensure that your elements automatically re-render whenever the locale changes in runtime mode.

import {Localized} from 'lit-localize/localized-element.js';
import {msg} from 'lit-localize';
import {LitElement, html} from 'lit-element';

class MyElement extends Localized(LitElement) {
  render() {
    // Whenever setLocale() is called, and templates for that locale have
    // finished loading, this render() function will be re-invoked.
    return html`<p>${msg('greeting', html`Hello <b>World!</b>`)}</p>`;
  }
}

In transform mode, applications of the Localized mixin are removed.

CLI

Running the lit-localize command-line program does the following:

  1. Reads your config file according to the --config flag.

  2. Analyzes all TypeScript files covered by your tsconfig.json, and discovers all calls to the lit-localize msg function.

  3. Creates or updates an XLIFF (.xlf) file for each of your target locales, with a <source> tag corresponding to each msg call.

  4. Reads existing <target> tags from existing XLIFF files for each msg call.

  5. When in transform mode, compiles your TypeScript project for each locale, where all msg calls are replaced with the corresponding static, localized version from that locale's XLIFF file.

  6. When in runtime mode, generates a <locale>.ts file for each locale, which can be dynamically loaded by the lit-localize module.

It takes the following flags:

Flag Description
--help Display this list of flags.
--config Path to JSON config file. Defaults to ./lit-localize.json

Config file

Property Type Description
sourceLocale string Required locale code that templates in the source code are written in.
targetLocales string[] Required locale codes that templates will be localized to.
tsConfig string Path to a tsconfig.json file that describes the TypeScript source files from which messages will be extracted.
output.mode "transform", "runtime" What kind of output should be produced. See modes.
output.localeCodesModule string Optional filepath for a generated TypeScript module that exports sourceLocale, targetLocales, and allLocales using the locale codes from your config file. Use to keep your config file and client config in sync.
interchange.format "xliff", "xlb" Data format to be consumed by your localization process. Options:

- "xliff": XLIFF 1.2 XML format
- "xlb": Google-internal XML format

Transform mode only

output.outputDir string Output directory for generated TypeScript modules. Into this directory will be generated a <locale>.ts for each targetLocale, each a TypeScript module that exports the translations in that locale keyed by message ID.

XLIFF only

interchange.xliffDir string Directory on disk to read/write .xlf XML files. For each target locale, the file path "<xliffDir>/<locale>.xlf" will be used.

FAQ

How should I set the initial locale in transform mode?

In transform mode, the locale is determined simply by the JavaScript bundle you load. How you determine which bundle to load when your page loads is up to you.

IMPORTANT: Take care to always validate your locale codes when dynamically choosing a script name! The example below is safe because a script can only be loaded if it matches one of the fixed locale codes in the regular expression, but if our matching logic was less precise, it could result in bugs or attacks that inject insecure JavaScript.

For example, if your application's locale is reflected in the URL, you can include an inline script in your HTML file that checks the URL and inserts the appropriate <script> tag:

<!DOCTYPE html>
<script>
  // If the subdomain matches one of our locale codes, load that bundle.
  // Otherwise, load the default locale bundle.
  //
  // E.g. https://es-419.example.com/
  //              ^^^^^^
  const match = window.location.href.match(
    /^https?:\/\/(es-419|zh_CN|en|es)\./
  );
  const locale = match ? match[1] : 'en';
  const script = document.createElement('script');
  script.type = 'module';
  script.src = `/${locale}.js`;
  document.head.appendChild(script);
</script>

Implementing logic similar to this on your server so that the appropriate script tag is statically rendered into your HTML file will usually result in the best performance, because the browser will start downloading your script as early as possible.

How should I switch locales in transform mode?

In transform mode, the setLocale function is not available. Instead, reload the page so that the next load will pick a different locale bundle.

For example, this locale-picker custom element loads a new subdomain whenever a new locale is selected from a drop-down list:

import {LitElement, html} from 'lit-element';
import {getLocale} from './localization.js';
import {allLocales} from './locale-codes.js';
import {Localized} from 'lit-localize/localized-element.js';

export class LocalePicker extends Localized(LitElement) {
  render() {
    return html`
      <select @change=${this.localeChanged}>
        ${allLocales.map(
          (locale) =>
            html`<option value=${locale} selected=${locale === getLocale()}>
              ${locale}
            </option>`
        )}
      </select>
    `;
  }

  localeChanged(event: Event) {
    const newLocale = (event.target as HTMLSelectElement).value;
    const newHostname = `${newLocale}.example.com`;
    const url = new URL(window.location.href);
    if (newHostname !== url.hostname) {
      url.hostname = newHostname;
      window.location.assign(url.toString());
    }
  }
}
customElements.define('locale-picker', LocalePicker);