@lit/localize
⚠️ Active work in progress! Subject to rapid major changes. ⚠️
@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(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="h3c44aff2d5f5ef6b">
<source>Hello <ph id="0"><b></ph>World<ph id="1"></b></ph>!</source>
<target>Hola <ph id="0"><b></ph>Mundo<ph id="1"></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 | (Optional)
const {getLocale} =
configureTransformLocalization(...); | const {getLocale, setLocale} =
configureLocalization(...); |
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
-
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
-
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"
}
-
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(html`Hello <b>World</b>!`)}</p>`, document.body);
-
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/Polymer/lit-html/lit-next/packages/localize/config.schema.json",
"sourceLocale": "en",
"targetLocales": ["es-419"],
"tsConfig": "tsconfig.json",
"output": {
"mode": "transform"
},
"interchange": {
"format": "xliff",
"xliffDir": "xliff/"
}
}
-
Run the lit-localize
CLI with the extract
command:
npx lit-localize extract
-
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="h3c44aff2d5f5ef6b">
<source>Hello <ph id="0"><b></ph>World<ph id="1"></b></ph>!</source>
</trans-unit>
-
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="h3c44aff2d5f5ef6b">
<source>Hello <ph id="0"><b></ph>World<ph id="1"></b></ph>!</source>
<target>Hola <ph id="0"><b></ph>Mundo<ph id="1"></b></ph>!</target>
</trans-unit>
-
Run the lit-localize
CLI with the build
command:
npx lit-localize build
-
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)("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(template: TemplateResult|string|StrResult, options?: {id?: string}) => string|TemplateResult
Make lit-html template or string localizable.
The options.id
parameter is an optional project-wide unique identifier for
this template. If omitted, an id will be automatically generated from the
template strings.
The template
parameter can take any of these forms:
-
A Lit html
tagged string that may contain embedded HTML and arbitrary expressions. HTML
markup and template expressions are automatically encoded into placeholders.
Placeholders can be seen and re-ordered by translators, but they can't be
modified.
msg(html`Hello <b>${getUsername()}</b>!`);
-
A plain string with no expressions.
msg('Hello World!');
-
A str
tagged string with arbitrary expressions. This str
tag is required
if your plain string has expressions, because lit-localize needs dynamic
access to the template's values
at runtime.
msg(str`Hello ${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>`;
str(strings: TemplateStringsArray, ...values: unknown[]): StrResult
Template tag function that allows string literals containing expressions to be
localized.
LOCALE_STATUS_EVENT
Name of the lit-localize-status
event.
Descriptions
You can add descriptions to messages using special // msgdesc:
comments.
Message descriptions help translators understand the context of each string they
are translating.
msg(html`Hello <b>World</b>!`);
Descriptions are represented in XLIFF using <note>
elements.
<trans-unit id="h3c44aff2d5f5ef6b">
<note>Greeting to everybody on homepage</note>
<source>Hello <ph id="0"><b></ph>World<ph id="1"></b></ph>!</source>
</trans-unit>
You can also apply a // msgdesc:
comment to a class, function, or block, in
which case the description will apply recursively to all msg
calls within it.
If there are multiple descriptions that apply to a msg
call, then they are
concatenated with a forward-slash in top-down order. This can be useful for
describing the context of an entire group of messages.
class MyHomepage extends Localized(LitElement) {
render() {
return msg(html`Hello <b>World</b>!`);
}
}
<trans-unit id="h3c44aff2d5f5ef6b">
<note>Homepage / Greeting to everybody</note>
<source>Hello <ph id="0"><b></ph>World<ph id="1"></b></ph>!</source>
</trans-unit>
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';
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() {
return html`<p>${msg(html`Hello <b>World!</b>`)}</p>`;
}
}
In transform mode, applications of the Localized
mixin are removed.
CLI
Usage
lit-localize command [--flags]
Command | Description |
---|
extract | Extract templates from msg() calls across all source files included by your tsconfig.json , and create or update XLIFF (.xlf ) files containing translation requests. |
build | Read translations and build the project according to the configured mode.
In transform mode, compile your TypeScript project for each locale, replacing msg calls with localized templates. See also the Rollup section for performing this transform as part of a Rollup pipeline.
In runtime mode, generate a <locale>.ts file for each locale, which can be dynamically loaded by the @lit/localize module. |
Flag | Description |
---|
--help | Display help about usage. |
--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. |
Rollup
To integrate locale transformation into a Rollup
project, import the localeTransformers
function from
@lit/localize/lib/rollup.js
.
This function generates an array of {locale, transformer}
objects, which you
can use in conjunction with the
transformers
option of
@rollup/plugin-typescript
to generate a separate bundle for each locale.
NOTE: This approach is only supported for transform mode. For runtime mode,
run lit-localize build
before invoking Rollup. Runtime mode generates
TypeScript files that can be treated as normal source inputs by an existing
build pipeline.
For example, the following rollup.config.js
generates a minified bundle for
each of your locales into ./bundled/<locale>/
directories.
import typescript from '@rollup/plugin-typescript';
import {localeTransformers} from '@lit/localize/lib/rollup.js';
import resolve from '@rollup/plugin-node-resolve';
import {terser} from 'rollup-plugin-terser';
const locales = localeTransformers();
export default locales.map(({locale, localeTransformer}) => ({
input: `src/index.ts`,
plugins: [
typescript({
transformers: {
before: [localeTransformer],
},
}),
resolve(),
terser(),
],
output: {
file: `bundled/${locale}/index.js`,
format: 'es',
},
}));
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>
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);