Skip to content

Commit

Permalink
Merge pull request #43 from PolymerLabs/localized-element
Browse files Browse the repository at this point in the history
Add lit-localize-status event and Localized mixin for re-rendering LitElement classes
  • Loading branch information
aomarks authored Aug 16, 2020
2 parents 935bb91 + 3ba7e1d commit 10d02c1
Show file tree
Hide file tree
Showing 12 changed files with 513 additions and 70 deletions.
105 changes: 102 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -107,7 +107,7 @@ promise resolves.
Throws if the given locale is not contained by the configured `sourceLocale` or
`targetLocales`.

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

Make a string or lit-html template localizable.

Expand Down Expand Up @@ -155,3 +155,102 @@ template for each emitted locale. For example:
```typescript
html`Hola <b>${getUsername()}!</b>`;
```

### `LOCALE_STATUS_EVENT`

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.

### 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.

In the case that a second locale is requested before the first one finishes
loading, a new `loading` event is dispatched, and no `ready` or `error` event
will be dispatched for the first request, because it is now stale.

#### `ready`

A new locale has successfully loaded and is ready for rendering. The `detail` object also contains:

- `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');
}
});
```

## `Localized` mixin

If you are using [LitElement](https://lit-element.polymer-project.org/), then
you can use the `Localized`
[mixin](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/)
from `lit-localize/localized-element.js` to ensure that your elements
automatically re-render whenever the locale changes.

```typescript
import {Localized} from 'lit-localize/localized-element.js';
import {msg} from 'lit-localize';
import {LitElement, html} from 'lit-element';

class MyElement extends Localized(LitElement) {
render() {
// Whenever setLocale() is called, and templates for that locale have
// finished loading, this render() function will be re-invoked.
return html`<p>
${msg('greeting', html`Hello <b>World!</b>`)}
</p>`;
}
}
```

In transform mode, applications of the `Localized` mixin are removed.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonschema": "^1.2.6",
"lit-element": "^2.3.1",
"lit-html": "^1.2.1",
"minimist": "^1.2.5",
"parse5": "^6.0.0",
Expand Down
165 changes: 111 additions & 54 deletions src/outputters/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,45 +114,104 @@ 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;
}
}

// configureTransformLocalization(...) -> {getLocale: () => "es-419"}
if (
this.isCallToTaggedFunction(
node,
'_LIT_LOCALIZE_CONFIGURE_TRANSFORM_LOCALIZATION_'
)
) {
return ts.createObjectLiteral(
[
ts.createPropertyAssignment(
ts.createIdentifier('getLocale'),
ts.createArrowFunction(
undefined,
undefined,
[],
undefined,
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.createStringLiteral(this.locale)
)
),
],
false
);
if (ts.isCallExpression(node)) {
// configureTransformLocalization(...) -> {getLocale: () => "es-419"}
if (
this.typeHasProperty(
node.expression,
'_LIT_LOCALIZE_CONFIGURE_TRANSFORM_LOCALIZATION_'
)
) {
return ts.createObjectLiteral(
[
ts.createPropertyAssignment(
ts.createIdentifier('getLocale'),
ts.createArrowFunction(
undefined,
undefined,
[],
undefined,
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.createStringLiteral(this.locale)
)
),
],
false
);
}

// configureLocalization(...) -> Error
if (
this.typeHasProperty(
node.expression,
'_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_'
)
) {
// TODO(aomarks) This error is not surfaced earlier in the analysis phase
// as a nicely formatted diagnostic, but it should be.
throw new KnownError(
'Cannot use configureLocalization in transform mode. ' +
'Use configureTransformLocalization instead.'
);
}

// Localized(LitElement) -> LitElement
if (this.typeHasProperty(node.expression, '_LIT_LOCALIZE_LOCALIZED_')) {
if (node.arguments.length !== 1) {
// TODO(aomarks) Surface as diagnostic instead.
throw new KnownError(
`Expected Localized mixin call to have one argument, ` +
`got ${node.arguments.length}`
);
}
return node.arguments[0];
}
}

// configureLocalization(...) -> Error
if (
this.isCallToTaggedFunction(node, '_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_')
) {
// TODO(aomarks) This error is not surfaced earlier in the analysis phase
// as a nicely formatted diagnostic, but it should be.
throw new KnownError(
'Cannot use configureLocalization in transform mode. ' +
'Use configureTransformLocalization instead.'
);
// LOCALE_STATUS_EVENT -> "lit-localize-status"
//
// 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);
Expand Down Expand Up @@ -380,16 +439,11 @@ class Transformer {
}

/**
* Return whether the given node is an import for the lit-localize 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();
Expand All @@ -398,27 +452,30 @@ 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;
}
}
return false;
}

/**
* Return whether the given node is call to a function which is is "tagged"
* with the given special identifying property (e.g. "_LIT_LOCALIZE_MSG_").
* Return whether the tpe of the given node is "tagged" with the given special
* identifying property (e.g. "_LIT_LOCALIZE_MSG_").
*/
isCallToTaggedFunction(
typeHasProperty(
node: ts.Node,
tagProperty: string
propertyName: string
): node is ts.CallExpression {
if (!ts.isCallExpression(node)) {
return false;
}
const type = this.typeChecker.getTypeAtLocation(node.expression);
const type = this.typeChecker.getTypeAtLocation(node);
const props = this.typeChecker.getPropertiesOfType(type);
return props.some((prop) => prop.escapedName === tagProperty);
return props.some((prop) => prop.escapedName === propertyName);
}
}

Expand Down
Loading

0 comments on commit 10d02c1

Please sign in to comment.