Skip to content

Commit 38dc4bd

Browse files
committed
Rework lit-localize-status event to convey loading/ready/error state
1 parent c214df3 commit 38dc4bd

File tree

10 files changed

+270
-62
lines changed

10 files changed

+270
-62
lines changed

README.md

Lines changed: 62 additions & 9 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
@@ -156,21 +156,74 @@ template for each emitted locale. For example:
156156
html`Hola <b>${getUsername()}!</b>`;
157157
```
158158

159-
### `LOCALE_CHANGED_EVENT: string`
159+
### `LOCALE_STATUS_EVENT`
160160

161-
Whenever the locale changes and templates have finished loading, an event by
162-
this name (`"lit-localize-locale-changed"`) is dispatched to `window`.
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`.
163167

164168
You can listen for this event to know when your application should be
165169
re-rendered following a locale change. See also the
166170
[`Localized`](#localized-mixin) mixin, which automatically re-renders
167171
`LitElement` classes using this event.
168172

169-
```typescript
170-
import {LOCALE_CHANGED_EVENT} from 'lit-localize';
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+
It will be followed by another `loading` status in the case that a second locale
186+
was requested before the first one finished loading.
187+
188+
#### `ready`
189+
190+
A new locale has successfully loaded and is ready for rendering. The `detail` object also contains:
171191

172-
window.addEventListener(LOCALE_CHANGED_EVENT, () => {
173-
renderApplication();
192+
- `readyLocale: string`: Code of the locale that has successfully loaded.
193+
194+
A `ready` status can be followed only by a `loading` status.
195+
196+
#### `error`
197+
198+
A new locale failed to load. The `detail` object also contains the following
199+
properties:
200+
201+
- `errorLocale: string`: Code of the locale that failed to load.
202+
- `errorMessage: string`: Error message from locale load failure.
203+
204+
An `error` status can be followed only by a `loading` status.
205+
206+
### Event example
207+
208+
```typescript
209+
// Show/hide a progress indicator whenever a new locale is loading,
210+
// and re-render the application every time a new locale successfully loads.
211+
window.addEventListener('lit-localize-status', (event) => {
212+
const spinner = document.querySelector('#spinner');
213+
if (event.detail.status === 'loading') {
214+
console.log(`Loading new locale: ${event.detail.loadingLocale}`);
215+
spinner.removeAttribute('hidden');
216+
} else if (event.detail.status === 'ready') {
217+
console.log(`Loaded new locale: ${event.detail.readyLocale}`);
218+
spinner.addAttribute('hidden');
219+
renderApplication();
220+
} else if (event.detail.status === 'error') {
221+
console.error(
222+
`Error loading locale ${event.detail.errorLocale}: ` +
223+
event.detail.errorMessage
224+
);
225+
spinner.addAttribute('hidden');
226+
}
174227
});
175228
```
176229

src/outputters/transform.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ 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

121126
if (ts.isCallExpression(node)) {
@@ -172,17 +177,41 @@ class Transformer {
172177
}
173178
}
174179

175-
// LOCALE_CHANGED_EVENT -> "lit-localize-locale-changed"
180+
// LOCALE_STATUS_EVENT -> "lit-localize-status"
176181
//
177-
// This is slightly odd, but by replacing the LOCALE_CHANGED_EVENT const
178-
// with its static string value, we don't have to be smart about deciding
179-
// when to remove the 'lit-localize' module import, since we can assume that
180-
// everything it exports will be transformed out.
181-
if (
182-
ts.isIdentifier(node) &&
183-
this.typeHasProperty(node, '_LIT_LOCALIZE_LOCALE_CHANGED_EVENT_')
184-
) {
185-
return ts.createStringLiteral('lit-localize-locale-changed');
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+
}
186215
}
187216

188217
return ts.visitEachChild(node, this.boundVisitNode, this.context);
@@ -410,17 +439,11 @@ class Transformer {
410439
}
411440

412441
/**
413-
* Return whether the given node is an import for the lit-localize main
414-
* module, or the localized-element 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).
415444
*/
416-
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
417-
if (!ts.isImportDeclaration(node)) {
418-
return false;
419-
}
420-
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
421-
node.moduleSpecifier
422-
);
423-
if (!moduleSymbol || !moduleSymbol.exports) {
445+
isLitLocalizeModule(moduleSymbol: ts.Symbol): boolean {
446+
if (!moduleSymbol.exports) {
424447
return false;
425448
}
426449
const exports = moduleSymbol.exports.values();
@@ -429,7 +452,13 @@ class Transformer {
429452
}) {
430453
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
431454
const props = this.typeChecker.getPropertiesOfType(type);
432-
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+
) {
433462
return true;
434463
}
435464
}

src/tests/transform.unit.test.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,48 @@ test('configureLocalization() throws', (t) => {
366366
);
367367
});
368368

369-
test('LOCALE_CHANGED_EVENT => "lit-localize-locale-changed"', (t) => {
369+
test('LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
370370
checkTransform(
371371
t,
372-
`import {LOCALE_CHANGED_EVENT} from './lib_client/index.js';
373-
window.addEventListener(LOCALE_CHANGED_EVENT, () => console.log('ok'));`,
374-
`window.addEventListener('lit-localize-locale-changed', () => console.log('ok'));`
372+
`import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
373+
window.addEventListener(LOCALE_STATUS_EVENT, () => console.log('ok'));`,
374+
`window.addEventListener('lit-localize-status', () => console.log('ok'));`
375+
);
376+
});
377+
378+
test('litLocalize.LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
379+
checkTransform(
380+
t,
381+
`import * as litLocalize from './lib_client/index.js';
382+
window.addEventListener(litLocalize.LOCALE_STATUS_EVENT, () => console.log('ok'));`,
383+
`window.addEventListener('lit-localize-status', () => console.log('ok'));`
384+
);
385+
});
386+
387+
test('re-assigned LOCALE_STATUS_EVENT', (t) => {
388+
checkTransform(
389+
t,
390+
`import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
391+
const event = LOCALE_STATUS_EVENT;
392+
window.addEventListener(event, () => console.log('ok'));`,
393+
`const event = 'lit-localize-status';
394+
window.addEventListener(event, () => console.log('ok'));`
395+
);
396+
});
397+
398+
test('different LOCALE_STATUS_EVENT variable unchanged', (t) => {
399+
checkTransform(
400+
t,
401+
`const LOCALE_STATUS_EVENT = "x";`,
402+
`const LOCALE_STATUS_EVENT = "x";`
403+
);
404+
});
405+
406+
test('different variable cast to "lit-localie-status" unchanged', (t) => {
407+
checkTransform(
408+
t,
409+
`const x = "x" as "lit-localize-status";`,
410+
`const x = "x";`
375411
);
376412
});
377413

@@ -380,6 +416,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
380416
t,
381417
`import {LitElement, html} from 'lit-element';
382418
import {Localized} from './lib_client/localized-element.js';
419+
import {msg} from './lib_client/index.js';
383420
class MyElement extends Localized(LitElement) {
384421
render() {
385422
return html\`<b>\${msg('greeting', 'Hello World!')}</b>\`;
@@ -390,6 +427,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
390427
render() {
391428
return html\`<b>Hello World!</b>\`;
392429
}
393-
}`
430+
}`,
431+
{autoImport: false}
394432
);
395433
});

0 commit comments

Comments
 (0)