Skip to content

Commit 103cb7f

Browse files
authored
Merge pull request #40 from PolymerLabs/runtime3
Expand and document lit-localize runtime module
2 parents 55cffc1 + 66518a2 commit 103cb7f

File tree

2 files changed

+341
-15
lines changed

2 files changed

+341
-15
lines changed

README.md

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,135 @@
22

33
[![Published on npm](https://img.shields.io/npm/v/lit-localize.svg)](https://www.npmjs.com/package/lit-localize) [![Test Status](https://github.com/PolymerLabs/lit-localize/workflows/tests/badge.svg?branch=master)](https://github.com/PolymerLabs/lit-localize/actions?query=workflow%3Atests+branch%3Amaster+event%3Apush)
44

5-
WIP
5+
## API
6+
7+
The `lit-localize` module exports the following functions:
8+
9+
### `configureLocalization(configuration)`
10+
11+
Set configuration parameters for lit-localize when in runtime mode. Returns an
12+
object with functions:
13+
14+
- [`getLocale`](#getLocale): Return the active locale code.
15+
- [`setLocale`](#setLocale): Set the active locale code.
16+
17+
Throws if called more than once.
18+
19+
The `configuration` object must have the following properties:
20+
21+
- `sourceLocale: string`: Required locale code in which source templates in this
22+
project are written, and the initial active locale.
23+
24+
- `targetLocales: Iterable<string>`: Required locale codes that are supported by
25+
this project. Should not include the `sourceLocale` code.
26+
27+
- `loadLocale: (locale: string) => Promise<LocaleModule>`: Required function
28+
that returns a promise of the localized templates for the given locale code.
29+
For security, this function will only ever be called with a `locale` that is
30+
contained by `targetLocales`.
31+
32+
Example:
33+
34+
```typescript
35+
const {getLocale, setLocale} = configureLocalization({
36+
sourceLocale: 'en',
37+
targetLocales: ['es-419', 'zh_CN'],
38+
loadLocale: (locale) => import(`/${locale}.js`),
39+
});
40+
```
41+
42+
### `configureTransformLocalization(configuration)`
43+
44+
Set configuration parameters for lit-localize when in transform mode. Returns an
45+
object with functions:
46+
47+
- [`getLocale`](#getLocale): Return the active locale code.
48+
49+
(Note that [`setLocale`](#setLocale) is not available, because changing locales
50+
at runtime is not supported in transform mode.)
51+
52+
Throws if called more than once.
53+
54+
The `configuration` object must have the following properties:
55+
56+
- `sourceLocale: string`: Required locale code in which source templates in this
57+
project are written, and the active locale.
58+
59+
Example:
60+
61+
```typescript
62+
const {getLocale} = configureLocalization({
63+
sourceLocale: 'en',
64+
});
65+
```
66+
67+
### `getLocale(): string`
68+
69+
Return the active locale code.
70+
71+
In transform mode, calls to this function are transformed into the static locale
72+
code string for each emitted locale.
73+
74+
### `setLocale(locale: string)`
75+
76+
Set the active locale code, and begin loading templates for that locale using
77+
the `loadLocale` function that was passed to `configureLocalization`. Returns a
78+
promise that resolves when the next locale is ready to be rendered.
79+
80+
Note that if a second call to `setLocale` is made while the first requested
81+
locale is still loading, then the second call takes precedence, and the promise
82+
returned from the first call will resolve when second locale is ready. If you
83+
need to know whether a particular locale was loaded, check `getLocale` after the
84+
promise resolves.
85+
86+
Throws if the given locale is not contained by the configured `sourceLocale` or
87+
`targetLocales`.
88+
89+
### `msg(id: string, template, ...args): string|TemplateResult`
90+
91+
Make a string or lit-html template localizable.
92+
93+
The `id` parameter is a project-wide unique identifier for this template.
94+
95+
The `template` parameter can have any of these types:
96+
97+
- A plain string with no placeholders:
98+
99+
```typescript
100+
msg('greeting', 'Hello World!');
101+
```
102+
103+
- A lit-html
104+
[`TemplateResult`](https://lit-html.polymer-project.org/api/classes/_lit_html_.templateresult.html)
105+
that may contain embedded HTML:
106+
107+
```typescript
108+
msg('greeting', html`Hello <b>World</b>!`);
109+
```
110+
111+
- A function that returns a [template
112+
literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
113+
string that may contain placeholders. Placeholders may only reference
114+
parameters of the function, which will be called with the 3rd and onwards
115+
parameters to `msg`.
116+
117+
```typescript
118+
msg('greeting', (name) => `Hello ${name}!`, getUsername());
119+
```
120+
121+
- A function that returns a lit-html
122+
[`TemplateResult`](https://lit-html.polymer-project.org/api/classes/_lit_html_.templateresult.html)
123+
that may contain embedded HTML, and may contain placeholders. Placeholders may
124+
only reference parameters of the function, which will be called with the 3rd
125+
and onwards parameters to `msg`:
126+
127+
```typescript
128+
msg('greeting', (name) => html`Hello <b>${name}</b>!`, getUsername());
129+
```
130+
131+
In transform mode, calls to this function are replaced with the static localized
132+
template for each emitted locale. For example:
133+
134+
```typescript
135+
html`Hola <b>${getUsername()}!</b>`;
136+
```

src_client/index.ts

Lines changed: 209 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,206 @@ import {TemplateResult} from 'lit-html';
1313

1414
/* eslint-disable @typescript-eslint/no-explicit-any */
1515

16+
/**
17+
* Configuration parameters for lit-localize when in runtime mode.
18+
*/
19+
export interface RuntimeConfiguration {
20+
/**
21+
* Required locale code in which source templates in this project are written,
22+
* and the initial active locale.
23+
*/
24+
sourceLocale: string;
25+
26+
/**
27+
* Required locale codes that are supported by this project. Should not
28+
* include the `sourceLocale` code.
29+
*/
30+
targetLocales: Iterable<string>;
31+
32+
/**
33+
* Required function that returns a promise of the localized templates for the
34+
* given locale code. For security, this function will only ever be called
35+
* with a `locale` that is contained by `targetLocales`.
36+
*/
37+
loadLocale: (locale: string) => Promise<LocaleModule>;
38+
}
39+
40+
/**
41+
* Configuration parameters for lit-localize when in transform mode.
42+
*/
43+
export interface TransformConfiguration {
44+
/**
45+
* Required locale code in which source templates in this project are written,
46+
* and the active locale.
47+
*/
48+
sourceLocale: string;
49+
}
50+
51+
/**
52+
* The template-like types that can be passed to `msg`.
53+
*/
54+
export type TemplateLike =
55+
| string
56+
| TemplateResult
57+
| ((...args: any[]) => string)
58+
| ((...args: any[]) => TemplateResult);
59+
60+
/**
61+
* A mapping from template ID to template.
62+
*/
63+
export type TemplateMap = {[id: string]: TemplateLike};
64+
65+
/**
66+
* The expected exports of a locale module.
67+
*/
68+
export interface LocaleModule {
69+
templates: TemplateMap;
70+
}
71+
72+
class Deferred<T> {
73+
readonly promise: Promise<T>;
74+
private _resolve!: (value: T) => void;
75+
private _reject!: (error: Error) => void;
76+
settled = false;
77+
78+
constructor() {
79+
this.promise = new Promise<T>((resolve, reject) => {
80+
this._resolve = resolve;
81+
this._reject = reject;
82+
});
83+
}
84+
85+
resolve(value: T) {
86+
this.settled = true;
87+
this._resolve(value);
88+
}
89+
90+
reject(error: Error) {
91+
this.settled = true;
92+
this._reject(error);
93+
}
94+
}
95+
96+
let activeLocale = '';
97+
let loadingLocale: string | undefined;
98+
let sourceLocale: string | undefined;
99+
let validLocales: Set<string> | undefined;
100+
let loadLocale: ((locale: string) => Promise<LocaleModule>) | undefined;
101+
let configured = false;
102+
let templates: TemplateMap | undefined;
103+
let loading = new Deferred<void>();
104+
105+
/**
106+
* Set configuration parameters for lit-localize when in runtime mode. Returns
107+
* an object with functions:
108+
*
109+
* - `getLocale`: Return the active locale code.
110+
* - `setLocale`: Set the active locale code.
111+
*
112+
* Throws if called more than once.
113+
*/
114+
export function configureLocalization(config: RuntimeConfiguration) {
115+
if (configured === true) {
116+
throw new Error('lit-localize can only be configured once');
117+
}
118+
configured = true;
119+
activeLocale = sourceLocale = config.sourceLocale;
120+
validLocales = new Set(config.targetLocales);
121+
validLocales.add(config.sourceLocale);
122+
loadLocale = config.loadLocale;
123+
return {getLocale, setLocale};
124+
}
125+
126+
/**
127+
* Set configuration parameters for lit-localize when in transform mode. Returns
128+
* an object with function:
129+
*
130+
* - `getLocale`: Return the active locale code.
131+
*
132+
* Throws if called more than once.
133+
*/
134+
export function configureTransformLocalization(config: TransformConfiguration) {
135+
if (configured === true) {
136+
throw new Error('lit-localize can only be configured once');
137+
}
138+
configured = true;
139+
activeLocale = sourceLocale = config.sourceLocale;
140+
return {getLocale};
141+
}
142+
143+
/**
144+
* Return the active locale code.
145+
*/
146+
function getLocale(): string {
147+
return activeLocale;
148+
}
149+
150+
/**
151+
* Set the active locale code, and begin loading templates for that locale using
152+
* the `loadLocale` function that was passed to `configureLocalization`. Returns
153+
* a promise that resolves when the next locale is ready to be rendered.
154+
*
155+
* Note that if a second call to `setLocale` is made while the first requested
156+
* locale is still loading, then the second call takes precedence, and the
157+
* promise returned from the first call will resolve when second locale is
158+
* ready. If you need to know whether a particular locale was loaded, check
159+
* `getLocale` after the promise resolves.
160+
*
161+
* Throws if the given locale is not contained by the configured `sourceLocale`
162+
* or `targetLocales`.
163+
*/
164+
function setLocale(newLocale: string): Promise<void> {
165+
if (!configured || !validLocales || !loadLocale) {
166+
throw new Error('Must call configureLocalization before setLocale');
167+
}
168+
if (newLocale === loadingLocale ?? activeLocale) {
169+
return loading.promise;
170+
}
171+
if (!validLocales.has(newLocale)) {
172+
throw new Error('Invalid locale code');
173+
}
174+
loadingLocale = newLocale;
175+
if (loading.settled) {
176+
loading = new Deferred();
177+
}
178+
if (newLocale === sourceLocale) {
179+
activeLocale = newLocale;
180+
loadingLocale = undefined;
181+
templates = undefined;
182+
loading.resolve();
183+
} else {
184+
loadLocale(newLocale).then(
185+
(mod) => {
186+
if (newLocale === loadingLocale) {
187+
activeLocale = newLocale;
188+
loadingLocale = undefined;
189+
templates = mod.templates;
190+
loading.resolve();
191+
}
192+
// Else another locale was requested in the meantime. Don't resolve or
193+
// reject, because the newer load call is going to use the same promise.
194+
// Note the user can call getLocale() after the promise resolves if they
195+
// need to check if the locale is still the one they expected to load.
196+
},
197+
(err) => {
198+
if (newLocale === loadingLocale) {
199+
loading.reject(err);
200+
}
201+
}
202+
);
203+
}
204+
return loading.promise;
205+
}
206+
207+
/**
208+
* Make a string or lit-html template localizable.
209+
*
210+
* @param id A project-wide unique identifier for this template.
211+
* @param template A string, a lit-html template, or a function that returns
212+
* either a string or lit-html template.
213+
* @param args In the case that `template` is a function, it is invoked with
214+
* the 3rd and onwards arguments to `msg`.
215+
*/
16216
export function msg(id: string, template: string): string;
17217

18218
export function msg(id: string, template: TemplateResult): TemplateResult;
@@ -29,24 +229,19 @@ export function msg<F extends (...args: any[]) => TemplateResult>(
29229
...params: Parameters<F>
30230
): TemplateResult;
31231

32-
/**
33-
* TODO(aomarks) This is a temporary stub implementation of the msg function. It
34-
* does not yet support actual localization, and is only used by the "transform"
35-
* output mode, since the user needs something to import.
36-
*
37-
* It may actually make more sense to move most of the generated code from
38-
* "runtime" mode into this library, so that users can
39-
* `import {msg} from 'lit-localize'` and tell it where to fetch translation
40-
* (this will make more sense after the planned revamp to support runtime async
41-
* locale loading and re-rendering).
42-
*/
43232
export function msg(
44-
_id: string,
45-
template: string | TemplateResult | (() => string | TemplateResult),
233+
id: string,
234+
template: TemplateLike,
46235
...params: any[]
47236
): string | TemplateResult {
237+
if (templates) {
238+
const localized = templates[id];
239+
if (localized) {
240+
template = localized;
241+
}
242+
}
48243
if (typeof template === 'function') {
49-
return (template as any)(...params);
244+
return template(...params);
50245
}
51246
return template;
52247
}

0 commit comments

Comments
 (0)