Skip to content

Commit 10d02c1

Browse files
authored
Merge pull request #43 from PolymerLabs/localized-element
Add lit-localize-status event and Localized mixin for re-rendering LitElement classes
2 parents 935bb91 + 3ba7e1d commit 10d02c1

File tree

12 files changed

+513
-70
lines changed

12 files changed

+513
-70
lines changed

README.md

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@ bundle. For example:
8888
const {getLocale} = {getLocale: () => 'es-419'};
8989
```
9090

91-
### `getLocale(): string`
91+
### `getLocale() => string`
9292

9393
Return the active locale code.
9494

95-
### `setLocale(locale: string): Promise`
95+
### `setLocale(locale: string) => Promise`
9696

9797
Set the active locale code, and begin loading templates for that locale using
9898
the `loadLocale` function that was passed to `configureLocalization`. Returns a
@@ -107,7 +107,7 @@ promise resolves.
107107
Throws if the given locale is not contained by the configured `sourceLocale` or
108108
`targetLocales`.
109109

110-
### `msg(id: string, template, ...args): string|TemplateResult`
110+
### `msg(id: string, template, ...args) => string|TemplateResult`
111111

112112
Make a string or lit-html template localizable.
113113

@@ -155,3 +155,102 @@ template for each emitted locale. For example:
155155
```typescript
156156
html`Hola <b>${getUsername()}!</b>`;
157157
```
158+
159+
### `LOCALE_STATUS_EVENT`
160+
161+
Name of the [`lit-localize-status` event](#lit-localize-status-event).
162+
163+
## `lit-localize-status` event
164+
165+
In runtime mode, whenever a locale change starts, finishes successfully, or
166+
fails, lit-localize will dispatch a `lit-localize-status` event to `window`.
167+
168+
You can listen for this event to know when your application should be
169+
re-rendered following a locale change. See also the
170+
[`Localized`](#localized-mixin) mixin, which automatically re-renders
171+
`LitElement` classes using this event.
172+
173+
### Event types
174+
175+
The `detail.status` string property tells you what kind of status change has occured,
176+
and can be one of: `loading`, `ready`, or `error`:
177+
178+
#### `loading`
179+
180+
A new locale has started to load. The `detail` object also contains:
181+
182+
- `loadingLocale: string`: Code of the locale that has started loading.
183+
184+
A `loading` status can be followed by a `ready`, `error`, or `loading` status.
185+
186+
In the case that a second locale is requested before the first one finishes
187+
loading, a new `loading` event is dispatched, and no `ready` or `error` event
188+
will be dispatched for the first request, because it is now stale.
189+
190+
#### `ready`
191+
192+
A new locale has successfully loaded and is ready for rendering. The `detail` object also contains:
193+
194+
- `readyLocale: string`: Code of the locale that has successfully loaded.
195+
196+
A `ready` status can be followed only by a `loading` status.
197+
198+
#### `error`
199+
200+
A new locale failed to load. The `detail` object also contains the following
201+
properties:
202+
203+
- `errorLocale: string`: Code of the locale that failed to load.
204+
- `errorMessage: string`: Error message from locale load failure.
205+
206+
An `error` status can be followed only by a `loading` status.
207+
208+
### Event example
209+
210+
```typescript
211+
// Show/hide a progress indicator whenever a new locale is loading,
212+
// and re-render the application every time a new locale successfully loads.
213+
window.addEventListener('lit-localize-status', (event) => {
214+
const spinner = document.querySelector('#spinner');
215+
if (event.detail.status === 'loading') {
216+
console.log(`Loading new locale: ${event.detail.loadingLocale}`);
217+
spinner.removeAttribute('hidden');
218+
} else if (event.detail.status === 'ready') {
219+
console.log(`Loaded new locale: ${event.detail.readyLocale}`);
220+
spinner.addAttribute('hidden');
221+
renderApplication();
222+
} else if (event.detail.status === 'error') {
223+
console.error(
224+
`Error loading locale ${event.detail.errorLocale}: ` +
225+
event.detail.errorMessage
226+
);
227+
spinner.addAttribute('hidden');
228+
}
229+
});
230+
```
231+
232+
## `Localized` mixin
233+
234+
If you are using [LitElement](https://lit-element.polymer-project.org/), then
235+
you can use the `Localized`
236+
[mixin](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/)
237+
from `lit-localize/localized-element.js` to ensure that your elements
238+
automatically re-render whenever the locale changes.
239+
240+
```typescript
241+
import {Localized} from 'lit-localize/localized-element.js';
242+
import {msg} from 'lit-localize';
243+
import {LitElement, html} from 'lit-element';
244+
245+
class MyElement extends Localized(LitElement) {
246+
render() {
247+
// Whenever setLocale() is called, and templates for that locale have
248+
// finished loading, this render() function will be re-invoked.
249+
return html`<p>
250+
${msg('greeting', html`Hello <b>World!</b>`)}
251+
</p>`;
252+
}
253+
}
254+
```
255+
256+
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: 111 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -114,45 +114,104 @@ class Transformer {
114114
}
115115

116116
// import ... from 'lit-localize' -> (removed)
117-
if (this.isLitLocalizeImport(node)) {
118-
return undefined;
117+
if (ts.isImportDeclaration(node)) {
118+
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
119+
node.moduleSpecifier
120+
);
121+
if (moduleSymbol && this.isLitLocalizeModule(moduleSymbol)) {
122+
return undefined;
123+
}
119124
}
120125

121-
// configureTransformLocalization(...) -> {getLocale: () => "es-419"}
122-
if (
123-
this.isCallToTaggedFunction(
124-
node,
125-
'_LIT_LOCALIZE_CONFIGURE_TRANSFORM_LOCALIZATION_'
126-
)
127-
) {
128-
return ts.createObjectLiteral(
129-
[
130-
ts.createPropertyAssignment(
131-
ts.createIdentifier('getLocale'),
132-
ts.createArrowFunction(
133-
undefined,
134-
undefined,
135-
[],
136-
undefined,
137-
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
138-
ts.createStringLiteral(this.locale)
139-
)
140-
),
141-
],
142-
false
143-
);
126+
if (ts.isCallExpression(node)) {
127+
// configureTransformLocalization(...) -> {getLocale: () => "es-419"}
128+
if (
129+
this.typeHasProperty(
130+
node.expression,
131+
'_LIT_LOCALIZE_CONFIGURE_TRANSFORM_LOCALIZATION_'
132+
)
133+
) {
134+
return ts.createObjectLiteral(
135+
[
136+
ts.createPropertyAssignment(
137+
ts.createIdentifier('getLocale'),
138+
ts.createArrowFunction(
139+
undefined,
140+
undefined,
141+
[],
142+
undefined,
143+
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
144+
ts.createStringLiteral(this.locale)
145+
)
146+
),
147+
],
148+
false
149+
);
150+
}
151+
152+
// configureLocalization(...) -> Error
153+
if (
154+
this.typeHasProperty(
155+
node.expression,
156+
'_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_'
157+
)
158+
) {
159+
// TODO(aomarks) This error is not surfaced earlier in the analysis phase
160+
// as a nicely formatted diagnostic, but it should be.
161+
throw new KnownError(
162+
'Cannot use configureLocalization in transform mode. ' +
163+
'Use configureTransformLocalization instead.'
164+
);
165+
}
166+
167+
// Localized(LitElement) -> LitElement
168+
if (this.typeHasProperty(node.expression, '_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+
}
144178
}
145179

146-
// configureLocalization(...) -> Error
147-
if (
148-
this.isCallToTaggedFunction(node, '_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_')
149-
) {
150-
// TODO(aomarks) This error is not surfaced earlier in the analysis phase
151-
// as a nicely formatted diagnostic, but it should be.
152-
throw new KnownError(
153-
'Cannot use configureLocalization in transform mode. ' +
154-
'Use configureTransformLocalization instead.'
155-
);
180+
// LOCALE_STATUS_EVENT -> "lit-localize-status"
181+
//
182+
// We want to replace this imported string constant with its static value so
183+
// that we can always safely remove the 'lit-localize' module import.
184+
//
185+
// TODO(aomarks) Maybe we should error here instead, since lit-localize
186+
// won't fire any of these events in transform mode? But I'm still thinking
187+
// about the use case of an app that can run in either runtime or transform
188+
// mode without code changes (e.g. runtime for dev, transform for
189+
// production)...
190+
//
191+
// We can't tag this string const with a special property like we do with
192+
// our exported functions, because doing so breaks lookups into
193+
// `WindowEventMap`. So we instead identify the symbol by name, and check
194+
// that it was declared in the lit-localize module.
195+
let eventSymbol = this.typeChecker.getSymbolAtLocation(node);
196+
if (eventSymbol && eventSymbol.name === 'LOCALE_STATUS_EVENT') {
197+
if (eventSymbol.flags & ts.SymbolFlags.Alias) {
198+
// Symbols will be aliased in the case of
199+
// `import {LOCALE_STATUS_EVENT} ...`
200+
// but not in the case of `import * as ...`.
201+
eventSymbol = this.typeChecker.getAliasedSymbol(eventSymbol);
202+
}
203+
for (const decl of eventSymbol.declarations) {
204+
let sourceFile: ts.Node = decl;
205+
while (!ts.isSourceFile(sourceFile)) {
206+
sourceFile = sourceFile.parent;
207+
}
208+
const sourceFileSymbol = this.typeChecker.getSymbolAtLocation(
209+
sourceFile
210+
);
211+
if (sourceFileSymbol && this.isLitLocalizeModule(sourceFileSymbol)) {
212+
return ts.createStringLiteral('lit-localize-status');
213+
}
214+
}
156215
}
157216

158217
return ts.visitEachChild(node, this.boundVisitNode, this.context);
@@ -380,16 +439,11 @@ class Transformer {
380439
}
381440

382441
/**
383-
* Return whether the given node is an import for the lit-localize module.
442+
* Return whether the given symbol looks like one of the lit-localize modules
443+
* (because it exports one of the special tagged functions).
384444
*/
385-
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
386-
if (!ts.isImportDeclaration(node)) {
387-
return false;
388-
}
389-
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
390-
node.moduleSpecifier
391-
);
392-
if (!moduleSymbol || !moduleSymbol.exports) {
445+
isLitLocalizeModule(moduleSymbol: ts.Symbol): boolean {
446+
if (!moduleSymbol.exports) {
393447
return false;
394448
}
395449
const exports = moduleSymbol.exports.values();
@@ -398,27 +452,30 @@ class Transformer {
398452
}) {
399453
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
400454
const props = this.typeChecker.getPropertiesOfType(type);
401-
if (props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_')) {
455+
if (
456+
props.some(
457+
(prop) =>
458+
prop.escapedName === '_LIT_LOCALIZE_MSG_' ||
459+
prop.escapedName === '_LIT_LOCALIZE_LOCALIZED_'
460+
)
461+
) {
402462
return true;
403463
}
404464
}
405465
return false;
406466
}
407467

408468
/**
409-
* Return whether the given node is call to a function which is is "tagged"
410-
* with the given special identifying property (e.g. "_LIT_LOCALIZE_MSG_").
469+
* Return whether the tpe of the given node is "tagged" with the given special
470+
* identifying property (e.g. "_LIT_LOCALIZE_MSG_").
411471
*/
412-
isCallToTaggedFunction(
472+
typeHasProperty(
413473
node: ts.Node,
414-
tagProperty: string
474+
propertyName: string
415475
): node is ts.CallExpression {
416-
if (!ts.isCallExpression(node)) {
417-
return false;
418-
}
419-
const type = this.typeChecker.getTypeAtLocation(node.expression);
476+
const type = this.typeChecker.getTypeAtLocation(node);
420477
const props = this.typeChecker.getPropertiesOfType(type);
421-
return props.some((prop) => prop.escapedName === tagProperty);
478+
return props.some((prop) => prop.escapedName === propertyName);
422479
}
423480
}
424481

0 commit comments

Comments
 (0)