diff --git a/README.md b/README.md
index 4f89a8a3..37d3a01c 100644
--- a/README.md
+++ b/README.md
@@ -88,11 +88,11 @@ bundle. For example:
const {getLocale} = {getLocale: () => 'es-419'};
```
-### `getLocale(): string`
+### `getLocale() => string`
Return the active locale code.
-### `setLocale(locale: string): Promise`
+### `setLocale(locale: string) => Promise`
Set the active locale code, and begin loading templates for that locale using
the `loadLocale` function that was passed to `configureLocalization`. Returns a
@@ -156,21 +156,74 @@ template for each emitted locale. For example:
html`Hola ${getUsername()}!`;
```
-### `LOCALE_CHANGED_EVENT: string`
+### `LOCALE_STATUS_EVENT`
-Whenever the locale changes and templates have finished loading, an event by
-this name (`"lit-localize-locale-changed"`) is dispatched to `window`.
+Name of the [`lit-localize-status` event](#lit-localize-status-event).
+
+## `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`](#localized-mixin) mixin, which automatically re-renders
`LitElement` classes using this event.
-```typescript
-import {LOCALE_CHANGED_EVENT} from 'lit-localize';
+### 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.
+It will be followed by another `loading` status in the case that a second locale
+was requested before the first one finished loading.
+
+#### `ready`
+
+A new locale has successfully loaded and is ready for rendering. The `detail` object also contains:
-window.addEventListener(LOCALE_CHANGED_EVENT, () => {
- renderApplication();
+- `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
+
+```typescript
+// Show/hide a progress indicator whenever a new locale is loading,
+// and re-render the application every time a new locale successfully loads.
+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.addAttribute('hidden');
+ renderApplication();
+ } else if (event.detail.status === 'error') {
+ console.error(
+ `Error loading locale ${event.detail.errorLocale}: ` +
+ event.detail.errorMessage
+ );
+ spinner.addAttribute('hidden');
+ }
});
```
diff --git a/src/outputters/transform.ts b/src/outputters/transform.ts
index 2d2e4ed6..49ea9632 100644
--- a/src/outputters/transform.ts
+++ b/src/outputters/transform.ts
@@ -114,8 +114,13 @@ class Transformer {
}
// import ... from 'lit-localize' -> (removed)
- if (this.isLitLocalizeImport(node)) {
- return undefined;
+ if (ts.isImportDeclaration(node)) {
+ const moduleSymbol = this.typeChecker.getSymbolAtLocation(
+ node.moduleSpecifier
+ );
+ if (moduleSymbol && this.isLitLocalizeModule(moduleSymbol)) {
+ return undefined;
+ }
}
if (ts.isCallExpression(node)) {
@@ -172,17 +177,41 @@ class Transformer {
}
}
- // LOCALE_CHANGED_EVENT -> "lit-localize-locale-changed"
+ // LOCALE_STATUS_EVENT -> "lit-localize-status"
//
- // This is slightly odd, but by replacing the LOCALE_CHANGED_EVENT const
- // with its static string value, we don't have to be smart about deciding
- // when to remove the 'lit-localize' module import, since we can assume that
- // everything it exports will be transformed out.
- if (
- ts.isIdentifier(node) &&
- this.typeHasProperty(node, '_LIT_LOCALIZE_LOCALE_CHANGED_EVENT_')
- ) {
- return ts.createStringLiteral('lit-localize-locale-changed');
+ // We want to replace this imported string constant with its static value so
+ // that we can always safely remove the 'lit-localize' module import.
+ //
+ // TODO(aomarks) Maybe we should error here instead, since lit-localize
+ // won't fire any of these events in transform mode? But I'm still thinking
+ // about the use case of an app that can run in either runtime or transform
+ // mode without code changes (e.g. runtime for dev, transform for
+ // production)...
+ //
+ // We can't tag this string const with a special property like we do with
+ // our exported functions, because doing so breaks lookups into
+ // `WindowEventMap`. So we instead identify the symbol by name, and check
+ // that it was declared in the lit-localize module.
+ let eventSymbol = this.typeChecker.getSymbolAtLocation(node);
+ if (eventSymbol && eventSymbol.name === 'LOCALE_STATUS_EVENT') {
+ if (eventSymbol.flags & ts.SymbolFlags.Alias) {
+ // Symbols will be aliased in the case of
+ // `import {LOCALE_STATUS_EVENT} ...`
+ // but not in the case of `import * as ...`.
+ eventSymbol = this.typeChecker.getAliasedSymbol(eventSymbol);
+ }
+ for (const decl of eventSymbol.declarations) {
+ let sourceFile: ts.Node = decl;
+ while (!ts.isSourceFile(sourceFile)) {
+ sourceFile = sourceFile.parent;
+ }
+ const sourceFileSymbol = this.typeChecker.getSymbolAtLocation(
+ sourceFile
+ );
+ if (sourceFileSymbol && this.isLitLocalizeModule(sourceFileSymbol)) {
+ return ts.createStringLiteral('lit-localize-status');
+ }
+ }
}
return ts.visitEachChild(node, this.boundVisitNode, this.context);
@@ -410,17 +439,11 @@ class Transformer {
}
/**
- * Return whether the given node is an import for the lit-localize main
- * module, or the localized-element module.
+ * Return whether the given symbol looks like one of the lit-localize modules
+ * (because it exports one of the special tagged functions).
*/
- isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
- if (!ts.isImportDeclaration(node)) {
- return false;
- }
- const moduleSymbol = this.typeChecker.getSymbolAtLocation(
- node.moduleSpecifier
- );
- if (!moduleSymbol || !moduleSymbol.exports) {
+ isLitLocalizeModule(moduleSymbol: ts.Symbol): boolean {
+ if (!moduleSymbol.exports) {
return false;
}
const exports = moduleSymbol.exports.values();
@@ -429,7 +452,13 @@ class Transformer {
}) {
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
const props = this.typeChecker.getPropertiesOfType(type);
- if (props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_')) {
+ if (
+ props.some(
+ (prop) =>
+ prop.escapedName === '_LIT_LOCALIZE_MSG_' ||
+ prop.escapedName === '_LIT_LOCALIZE_LOCALIZED_'
+ )
+ ) {
return true;
}
}
diff --git a/src/tests/transform.unit.test.ts b/src/tests/transform.unit.test.ts
index 9a92bf68..f49bb98b 100644
--- a/src/tests/transform.unit.test.ts
+++ b/src/tests/transform.unit.test.ts
@@ -366,12 +366,48 @@ test('configureLocalization() throws', (t) => {
);
});
-test('LOCALE_CHANGED_EVENT => "lit-localize-locale-changed"', (t) => {
+test('LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
checkTransform(
t,
- `import {LOCALE_CHANGED_EVENT} from './lib_client/index.js';
- window.addEventListener(LOCALE_CHANGED_EVENT, () => console.log('ok'));`,
- `window.addEventListener('lit-localize-locale-changed', () => console.log('ok'));`
+ `import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
+ window.addEventListener(LOCALE_STATUS_EVENT, () => console.log('ok'));`,
+ `window.addEventListener('lit-localize-status', () => console.log('ok'));`
+ );
+});
+
+test('litLocalize.LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
+ checkTransform(
+ t,
+ `import * as litLocalize from './lib_client/index.js';
+ window.addEventListener(litLocalize.LOCALE_STATUS_EVENT, () => console.log('ok'));`,
+ `window.addEventListener('lit-localize-status', () => console.log('ok'));`
+ );
+});
+
+test('re-assigned LOCALE_STATUS_EVENT', (t) => {
+ checkTransform(
+ t,
+ `import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
+ const event = LOCALE_STATUS_EVENT;
+ window.addEventListener(event, () => console.log('ok'));`,
+ `const event = 'lit-localize-status';
+ window.addEventListener(event, () => console.log('ok'));`
+ );
+});
+
+test('different LOCALE_STATUS_EVENT variable unchanged', (t) => {
+ checkTransform(
+ t,
+ `const LOCALE_STATUS_EVENT = "x";`,
+ `const LOCALE_STATUS_EVENT = "x";`
+ );
+});
+
+test('different variable cast to "lit-localie-status" unchanged', (t) => {
+ checkTransform(
+ t,
+ `const x = "x" as "lit-localize-status";`,
+ `const x = "x";`
);
});
@@ -380,6 +416,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
t,
`import {LitElement, html} from 'lit-element';
import {Localized} from './lib_client/localized-element.js';
+ import {msg} from './lib_client/index.js';
class MyElement extends Localized(LitElement) {
render() {
return html\`\${msg('greeting', 'Hello World!')}\`;
@@ -390,6 +427,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
render() {
return html\`Hello World!\`;
}
- }`
+ }`,
+ {autoImport: false}
);
});
diff --git a/src_client/index.ts b/src_client/index.ts
index 3c4a8707..d9ccb0e3 100644
--- a/src_client/index.ts
+++ b/src_client/index.ts
@@ -69,6 +69,77 @@ export interface LocaleModule {
templates: TemplateMap;
}
+/**
+ * Name of the event dispatched to `window` whenever a locale change starts,
+ * finishes successfully, or fails. Only relevant to runtime mode.
+ *
+ * The `detail` of this event is an object with a `status` string that can be:
+ * "loading", "ready", or "error", along with the relevant locale code, and
+ * error message if applicable.
+ *
+ * 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.
+ */
+export const LOCALE_STATUS_EVENT = 'lit-localize-status';
+
+declare global {
+ interface WindowEventMap {
+ [LOCALE_STATUS_EVENT]: CustomEvent;
+ }
+}
+
+/**
+ * The possible details of the "lit-localize-status" event.
+ */
+export type LocaleStatusEventDetail = LocaleLoading | LocaleReady | LocaleError;
+
+/**
+ * Detail of the "lit-localize-status" event when a new locale has started to
+ * load.
+ *
+ * A "loading" status can be followed by [1] another "loading" status (in the
+ * case that a second locale is requested before the first one completed), [2] a
+ * "ready" status, or [3] an "error" status.
+ */
+export interface LocaleLoading {
+ status: 'loading';
+ /** Code of the locale that has started loading. */
+ loadingLocale: string;
+}
+
+/**
+ * Detail of the "lit-localize-status" event when a new locale has successfully
+ * loaded and is ready for rendering.
+ *
+ * A "ready" status can be followed only by a "loading" status.
+ */
+export interface LocaleReady {
+ status: 'ready';
+ /** Code of the locale that has successfully loaded. */
+ readyLocale: string;
+}
+
+/**
+ * Detail of the "lit-localize-status" event when a new locale failed to load.
+ *
+ * An "error" status can be followed only by a "loading" status.
+ */
+export interface LocaleError {
+ status: 'error';
+ /** Code of the locale that failed to load. */
+ errorLocale: string;
+ /** Error message from locale load failure. */
+ errorMessage: string;
+}
+
+/**
+ * Dispatch a "lit-localize-status" event to `window` with the given detail.
+ */
+function dispatchStatusEvent(detail: LocaleStatusEventDetail) {
+ window.dispatchEvent(new CustomEvent(LOCALE_STATUS_EVENT, {detail}));
+}
+
class Deferred {
readonly promise: Promise;
private _resolve!: (value: T) => void;
@@ -103,8 +174,6 @@ let templates: TemplateMap | undefined;
let loading = new Deferred();
/**
- * Set runtime configuration parameters for lit-localize. This function must be
- * called before using any other lit-localize function.
* Set configuration parameters for lit-localize when in runtime mode. Returns
* an object with functions:
*
@@ -150,18 +219,6 @@ export const configureTransformLocalization: ((
return {getLocale};
};
-/**
- * Whenever the locale changes and templates have finished loading, an event by
- * this name ("lit-localize-locale-changed") is dispatched 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.
- */
-export const LOCALE_CHANGED_EVENT: string & {
- _LIT_LOCALIZE_LOCALE_CHANGED_EVENT_?: never;
-} = 'lit-localize-locale-changed';
-
/**
* Return the active locale code.
*/
@@ -201,12 +258,13 @@ const setLocale: ((newLocale: string) => void) & {
if (loading.settled) {
loading = new Deferred();
}
+ dispatchStatusEvent({status: 'loading', loadingLocale: newLocale});
if (newLocale === sourceLocale) {
activeLocale = newLocale;
loadingLocale = undefined;
templates = undefined;
loading.resolve();
- window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
+ dispatchStatusEvent({status: 'ready', readyLocale: newLocale});
} else {
loadLocale(newLocale).then(
(mod) => {
@@ -215,7 +273,7 @@ const setLocale: ((newLocale: string) => void) & {
loadingLocale = undefined;
templates = mod.templates;
loading.resolve();
- window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
+ dispatchStatusEvent({status: 'ready', readyLocale: newLocale});
}
// Else another locale was requested in the meantime. Don't resolve or
// reject, because the newer load call is going to use the same promise.
@@ -225,6 +283,11 @@ const setLocale: ((newLocale: string) => void) & {
(err) => {
if (newLocale === loadingLocale) {
loading.reject(err);
+ dispatchStatusEvent({
+ status: 'error',
+ errorLocale: newLocale,
+ errorMessage: err.toString(),
+ });
}
}
);
diff --git a/src_client/localized-element.ts b/src_client/localized-element.ts
index 1ebb25ae..92ca04bb 100644
--- a/src_client/localized-element.ts
+++ b/src_client/localized-element.ts
@@ -10,7 +10,7 @@
*/
import {LitElement} from 'lit-element';
-import {LOCALE_CHANGED_EVENT} from './index.js';
+import {LOCALE_STATUS_EVENT} from './index.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor = new (...args: any[]) => T;
@@ -39,19 +39,28 @@ type Constructor = new (...args: any[]) => T;
*/
function _Localized>(Base: T): T {
class Localized extends Base {
- private __boundRequestUpdate = () => this.requestUpdate();
+ private readonly __litLocalizeEventHandler = (
+ event: WindowEventMap[typeof LOCALE_STATUS_EVENT]
+ ) => {
+ if (event.detail.status === 'ready') {
+ this.requestUpdate();
+ }
+ };
connectedCallback() {
super.connectedCallback();
- window.addEventListener(LOCALE_CHANGED_EVENT, this.__boundRequestUpdate);
+ window.addEventListener(
+ LOCALE_STATUS_EVENT,
+ this.__litLocalizeEventHandler
+ );
}
disconnectedCallback() {
- super.disconnectedCallback();
window.removeEventListener(
- LOCALE_CHANGED_EVENT,
- this.__boundRequestUpdate
+ LOCALE_STATUS_EVENT,
+ this.__litLocalizeEventHandler
);
+ super.disconnectedCallback();
}
}
diff --git a/testdata/transform/goldens/foo.ts b/testdata/transform/goldens/foo.ts
index 15ebd77d..7a7ac604 100644
--- a/testdata/transform/goldens/foo.ts
+++ b/testdata/transform/goldens/foo.ts
@@ -2,12 +2,17 @@ import {LitElement, html} from 'lit-element';
import {
msg,
configureTransformLocalization,
+ LOCALE_STATUS_EVENT,
} from '../../../lib_client/index.js';
import {Localized} from '../../../lib_client/localized-element.js';
const {getLocale} = configureTransformLocalization({sourceLocale: 'en'});
console.log(`Locale is ${getLocale()}`);
+window.addEventListener(LOCALE_STATUS_EVENT, (event) => {
+ console.log(event.detail.status);
+});
+
msg('string', 'Hello World!');
msg('lit', html`Hello World!`);
diff --git a/testdata/transform/goldens/tsout/en/foo.js b/testdata/transform/goldens/tsout/en/foo.js
index ebe946db..d2a99887 100644
--- a/testdata/transform/goldens/tsout/en/foo.js
+++ b/testdata/transform/goldens/tsout/en/foo.js
@@ -1,7 +1,9 @@
import {LitElement, html} from 'lit-element';
-import {Localized} from '../../../lib_client/localized-element.js';
const {getLocale} = {getLocale: () => 'en'};
console.log(`Locale is ${getLocale()}`);
+window.addEventListener('lit-localize-status', (event) => {
+ console.log(event.detail.status);
+});
('Hello World!');
html`Hello World!`;
`Hello World!`;
diff --git a/testdata/transform/goldens/tsout/es-419/foo.js b/testdata/transform/goldens/tsout/es-419/foo.js
index 7a235bd5..08e425df 100644
--- a/testdata/transform/goldens/tsout/es-419/foo.js
+++ b/testdata/transform/goldens/tsout/es-419/foo.js
@@ -1,7 +1,9 @@
import {LitElement, html} from 'lit-element';
-import {Localized} from '../../../lib_client/localized-element.js';
const {getLocale} = {getLocale: () => 'es-419'};
console.log(`Locale is ${getLocale()}`);
+window.addEventListener('lit-localize-status', (event) => {
+ console.log(event.detail.status);
+});
`Hola Mundo!`;
html`Hola Mundo!`;
`Hola World!`;
diff --git a/testdata/transform/goldens/tsout/zh_CN/foo.js b/testdata/transform/goldens/tsout/zh_CN/foo.js
index bbbead47..00f3f0c0 100644
--- a/testdata/transform/goldens/tsout/zh_CN/foo.js
+++ b/testdata/transform/goldens/tsout/zh_CN/foo.js
@@ -1,7 +1,9 @@
import {LitElement, html} from 'lit-element';
-import {Localized} from '../../../lib_client/localized-element.js';
const {getLocale} = {getLocale: () => 'zh_CN'};
console.log(`Locale is ${getLocale()}`);
+window.addEventListener('lit-localize-status', (event) => {
+ console.log(event.detail.status);
+});
`\u4F60\u597D\uFF0C\u4E16\u754C\uFF01`;
html`你好, 世界!`;
`Hello World!`;
diff --git a/testdata/transform/input/foo.ts b/testdata/transform/input/foo.ts
index 15ebd77d..7a7ac604 100644
--- a/testdata/transform/input/foo.ts
+++ b/testdata/transform/input/foo.ts
@@ -2,12 +2,17 @@ import {LitElement, html} from 'lit-element';
import {
msg,
configureTransformLocalization,
+ LOCALE_STATUS_EVENT,
} from '../../../lib_client/index.js';
import {Localized} from '../../../lib_client/localized-element.js';
const {getLocale} = configureTransformLocalization({sourceLocale: 'en'});
console.log(`Locale is ${getLocale()}`);
+window.addEventListener(LOCALE_STATUS_EVENT, (event) => {
+ console.log(event.detail.status);
+});
+
msg('string', 'Hello World!');
msg('lit', html`Hello World!`);