Skip to content

Commit d45d73a

Browse files
committed
Add Localize mixin for LitElement
1 parent 1770253 commit d45d73a

File tree

12 files changed

+295
-18
lines changed

12 files changed

+295
-18
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,49 @@ template for each emitted locale. For example:
110110
```typescript
111111
html`Hola <b>${getUsername()}!</b>`;
112112
```
113+
114+
### `addLocaleChangeCallback(callback: () => void)`
115+
116+
Add the given function to the set of callbacks that will be invoked whenever the
117+
locale changes and its localized messages are ready.
118+
119+
Use this function to re-render your application whenever the locale is changed.
120+
121+
If you are using `LitElement`, consider using
122+
[`LocalizedLitElement`](#localizedlitelement), which performs this re-rendering
123+
automatically.
124+
125+
In transform mode, calls to this function are replaced with `undefined`.
126+
127+
### `removeLocaleChangeCallback(callback: () => void)`
128+
129+
Remove the given function from the set of callbacks that will be invoked
130+
whenever the locale changes and its localized messages are ready.
131+
132+
In transform mode, calls to this function are replaced with `undefined`.
133+
134+
## `LocalizedLitElement`
135+
136+
If you are using [LitElement](https://lit-element.polymer-project.org/), then
137+
you can use the `Localized`
138+
[mixin](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/)
139+
from `lit-localize/localized-element.js` to ensure that your elements
140+
automatically re-render whenever the locale changes.
141+
142+
```typescript
143+
import {Localized} from 'lit-localize/localized-element.js';
144+
import {msg} from 'lit-localize';
145+
import {LitElement, html} from 'lit-element';
146+
147+
class MyElement extends Localized(LitElement) {
148+
render() {
149+
// Whenever setLocale() is called, and templates for that locale have
150+
// finished loading, this render() function will be re-invoked.
151+
return html`<p>
152+
${msg('greeting', html`Hello <b>World!</b>`)}
153+
</p>`;
154+
}
155+
}
156+
```
157+
158+
In transform mode, applications of the `Localized` mixin are removed.

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"fs-extra": "^9.0.0",
3333
"glob": "^7.1.6",
3434
"jsonschema": "^1.2.6",
35+
"lit-element": "^2.3.1",
3536
"lit-html": "^1.2.1",
3637
"minimist": "^1.2.5",
3738
"parse5": "^6.0.0",

src/outputters/transform.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,38 @@ class Transformer {
144144
);
145145
}
146146

147+
// addLocaleChangeCallback(...) -> undefined
148+
if (
149+
this.isCallToTaggedFunction(
150+
node,
151+
'_LIT_LOCALIZE_ADD_LOCALE_CHANGE_CALLBACK_'
152+
)
153+
) {
154+
return ts.createIdentifier('undefined');
155+
}
156+
157+
// removeLocaleChangeCallback(...) -> undefined
158+
if (
159+
this.isCallToTaggedFunction(
160+
node,
161+
'_LIT_LOCALIZE_REMOVE_LOCALE_CHANGE_CALLBACK_'
162+
)
163+
) {
164+
return ts.createIdentifier('undefined');
165+
}
166+
167+
// Localized(LitElement) -> LitElement
168+
if (this.isCallToTaggedFunction(node, '_LIT_LOCALIZE_LOCALIZED_')) {
169+
if (node.arguments.length !== 1) {
170+
// TODO(aomarks) Surface as diagnostic instead.
171+
throw new KnownError(
172+
`Expected Localized mixin call to have one argument, ` +
173+
`got ${node.arguments.length}`
174+
);
175+
}
176+
return node.arguments[0];
177+
}
178+
147179
return ts.visitEachChild(node, this.boundVisitNode, this.context);
148180
}
149181

@@ -368,7 +400,8 @@ class Transformer {
368400
}
369401

370402
/**
371-
* Return whether the given node is an import for the lit-localize module.
403+
* Return whether the given node is an import for the lit-localize main
404+
* module, or the localized-element module.
372405
*/
373406
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
374407
if (!ts.isImportDeclaration(node)) {
@@ -381,14 +414,14 @@ class Transformer {
381414
return false;
382415
}
383416
// TODO(aomarks) Is there a better way to reliably identify the lit-localize
384-
// module that doesn't require this cast? We could export a const with a
417+
// modules that don't require this cast? We could export a const with a
385418
// known name and then look through `exports`, but it doesn't seem good to
386-
// polute the module like that.
419+
// polute the modules like that.
387420
const file = (moduleSymbol.valueDeclaration as unknown) as {
388421
identifiers: Map<string, unknown>;
389422
};
390423
for (const id of file.identifiers.keys()) {
391-
if (id === '_LIT_LOCALIZE_MSG_') {
424+
if (id === '_LIT_LOCALIZE_MSG_' || id === '_LIT_LOCALIZE_LOCALIZED_') {
392425
return true;
393426
}
394427
}

src/tests/transform.unit.test.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ test('configureLocalization() -> undefined', (t) => {
334334
});`,
335335
`undefined;`,
336336
[],
337-
true
337+
false
338338
);
339339
});
340340

@@ -345,7 +345,7 @@ test('getLocale() -> "es-419"', (t) => {
345345
getLocale();`,
346346
`"en";`,
347347
[],
348-
true,
348+
false,
349349
'en'
350350
);
351351

@@ -355,7 +355,7 @@ test('getLocale() -> "es-419"', (t) => {
355355
getLocale();`,
356356
`"es-419";`,
357357
[],
358-
true,
358+
false,
359359
'es-419'
360360
);
361361
});
@@ -367,17 +367,61 @@ test('setLocale() -> undefined', (t) => {
367367
setLocale("es-419");`,
368368
`undefined;`,
369369
[],
370-
true
370+
false
371371
);
372372
});
373373

374374
test('localeReady() -> Promise.resolve(undefined)', (t) => {
375375
checkTransform(
376376
t,
377377
`import {localeReady} from './lib_client/index.js';
378-
localeReady().then(() => console.log('ok'))`,
379-
`Promise.resolve(undefined).then(() => console.log('ok'))`,
378+
localeReady().then(() => console.log('ok'));`,
379+
`Promise.resolve(undefined).then(() => console.log('ok'));`,
380+
[],
381+
false
382+
);
383+
});
384+
385+
test('addLocaleChangeCallback -> undefined', (t) => {
386+
checkTransform(
387+
t,
388+
`import {addLocaleChangeCallback} from './lib_client/index.js';
389+
addLocaleChangeCallback(() => console.log('ok'));`,
390+
`undefined`,
391+
[],
392+
false
393+
);
394+
});
395+
396+
test('removeLocaleChangeCallback -> undefined', (t) => {
397+
checkTransform(
398+
t,
399+
`import {removeLocaleChangeCallback} from './lib_client/index.js';
400+
removeLocaleChangeCallback(() => console.log('ok'));`,
401+
`undefined`,
402+
[],
403+
false
404+
);
405+
});
406+
407+
test('Localized(LitElement) -> LitElement', (t) => {
408+
checkTransform(
409+
t,
410+
`import {LitElement, html} from 'lit-element';
411+
import {msg} from './lib_client/index.js';
412+
import {Localized} from './lib_client/localized-element.js';
413+
class MyElement extends Localized(LitElement) {
414+
render() {
415+
return html\`<b>\${msg('greeting', 'Hello World!')}</b>\`;
416+
}
417+
}`,
418+
`import {LitElement, html} from 'lit-element';
419+
class MyElement extends LitElement {
420+
render() {
421+
return html\`<b>Hello World!</b>\`;
422+
}
423+
}`,
380424
[],
381-
true
425+
false
382426
);
383427
});

src_client/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ let validLocales: Set<string> | undefined;
8888
let loadLocale: ((locale: string) => Promise<LocaleModule>) | undefined;
8989
let templates: TemplateMap | undefined;
9090
let loading = new Deferred<void>();
91+
const changeLocaleCallbacks = new Set<() => void>();
9192

9293
/**
9394
* Set runtime configuration parameters for lit-localize. This function must be
@@ -134,12 +135,14 @@ export const setLocale: ((newLocale: string) => void) & {
134135
}
135136
if (newLocale === sourceLocale) {
136137
loading.resolve();
138+
fireChangeLocaleCallbacks();
137139
} else {
138140
loadLocale(newLocale).then(
139141
(mod) => {
140142
if (newLocale === activeLocale) {
141143
templates = mod.templates;
142144
loading.resolve();
145+
fireChangeLocaleCallbacks();
143146
}
144147
// Else another locale was requested in the meantime. Don't resolve or
145148
// reject, because the newer load call is going to use the same promise.
@@ -163,6 +166,40 @@ export const localeReady: (() => Promise<void>) & {
163166
_LIT_LOCALIZE_LOCALE_READY_?: never;
164167
} = () => loading.promise;
165168

169+
/**
170+
* Add the given function to the set of callbacks that will be invoked whenever
171+
* the locale changes and its localized messages are ready.
172+
*
173+
* Use this function to re-render your application whenever the locale is changed.
174+
*
175+
* If you are using LitElement, consider using LocalizedLitElement, which performs
176+
* this re-rendering automatically.
177+
*/
178+
export const addLocaleChangeCallback: ((callback: () => void) => void) & {
179+
_LIT_LOCALIZE_ADD_LOCALE_CHANGE_CALLBACK_?: never;
180+
} = (callback: () => void) => {
181+
changeLocaleCallbacks.add(callback);
182+
};
183+
184+
/**
185+
* Remove the given function from the set of callbacks that will be invoked
186+
* whenever the locale changes and its localized messages are ready.
187+
*/
188+
export const removeLocaleChangeCallback: ((callback: () => void) => void) & {
189+
_LIT_LOCALIZE_REMOVE_LOCALE_CHANGE_CALLBACK_?: never;
190+
} = (callback: () => void) => {
191+
changeLocaleCallbacks.delete(callback);
192+
};
193+
194+
/**
195+
* Fire all of the registered change locale callback functions.
196+
*/
197+
const fireChangeLocaleCallbacks = () => {
198+
for (const callback of changeLocaleCallbacks) {
199+
callback();
200+
}
201+
};
202+
166203
/**
167204
* Make a string or lit-html template localizable.
168205
*

src_client/localized-element.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
6+
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
7+
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
8+
* Google as part of the polymer project is also subject to an additional IP
9+
* rights grant found at http://polymer.github.io/PATENTS.txt
10+
*/
11+
12+
import {LitElement} from 'lit-element';
13+
import {
14+
addLocaleChangeCallback,
15+
removeLocaleChangeCallback,
16+
localeReady,
17+
} from './index.js';
18+
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
type Constructor<T> = new (...args: any[]) => T;
21+
22+
/**
23+
* Class mixin for LitElement-based custom elements that ensures rendering is
24+
* aligned with the currently active locale.
25+
*
26+
* Defers rendering until messages for the active locale have loaded, and
27+
* triggers re-rendering every time the active locale changes.
28+
*
29+
* When using lit-localize in transform mode, applications of this mixin are
30+
* automatically removed.
31+
*
32+
* Usage:
33+
*
34+
* import {Localized} from 'lit-localize/localized-element.js';
35+
* import {msg} from 'lit-localize';
36+
* import {LitElement, html} from 'lit-html';
37+
*
38+
* class MyElement extends Localized(LitElement) {
39+
* render() {
40+
* return html`<b>${msg('greeting', 'Hello World')}</b>`;
41+
* }
42+
* }
43+
*/
44+
function _Localized<T extends Constructor<LitElement>>(Base: T): T {
45+
class LocalizedLitElement extends Base {
46+
private __boundRequestUpdate = this.requestUpdate.bind(this);
47+
48+
connectedCallback() {
49+
super.connectedCallback();
50+
addLocaleChangeCallback(this.__boundRequestUpdate);
51+
}
52+
53+
disconnectedCallback() {
54+
super.disconnectedCallback();
55+
removeLocaleChangeCallback(this.__boundRequestUpdate);
56+
}
57+
58+
protected async performUpdate(): Promise<unknown> {
59+
await localeReady();
60+
return super.performUpdate();
61+
}
62+
}
63+
64+
return LocalizedLitElement;
65+
}
66+
67+
export const Localized: typeof _Localized & {
68+
_LIT_LOCALIZE_LOCALIZED_?: never;
69+
} = _Localized;

testdata/transform/goldens/foo.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {html} from 'lit-html';
2-
import {msg} from '../../../lib_client/index.js';
1+
import {LitElement, html} from 'lit-element';
2+
import {msg, getLocale} from '../../../lib_client/index.js';
3+
import {Localized} from '../../../lib_client/localized-element.js';
34

45
msg('string', 'Hello World!');
56

@@ -14,3 +15,11 @@ msg(
1415
'https://www.example.com/',
1516
'World'
1617
);
18+
19+
export class MyElement extends Localized(LitElement) {
20+
render() {
21+
return html`<p>
22+
${msg('lit', html`Hello <b><i>World!</i></b>`)} (${getLocale()})
23+
</p>`;
24+
}
25+
}

0 commit comments

Comments
 (0)