Skip to content

Commit c214df3

Browse files
committed
Merge branch 'transform-more' into localized-element
2 parents e3bfd9b + d89861c commit c214df3

File tree

15 files changed

+367
-264
lines changed

15 files changed

+367
-264
lines changed

README.md

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@
22

33
[![Published on npm](https://img.shields.io/npm/v/lit-localize.svg)](https://www.npmjs.com/package/lit-localize) [![Test Status](https://github.com/PolymerLabs/lit-localize/workflows/tests/badge.svg?branch=master)](https://github.com/PolymerLabs/lit-localize/actions?query=workflow%3Atests+branch%3Amaster+event%3Apush)
44

5-
WIP
6-
75
## API
86

97
The `lit-localize` module exports the following functions:
108

9+
> Note that lit-localize relies on distinctive, annotated TypeScript type
10+
> signatures to identify calls to `msg` and other APIs during analysis of your
11+
> code. Casting a lit-localize function to a type that does not include its
12+
> annotation will prevent lit-localize from being able to extract and transform
13+
> templates from your application. For example, a cast like
14+
> `(msg as any)("greeting", "Hello")` will not be identified. It is safe to
15+
> re-assign lit-localize functions or pass them as parameters, as long as the
16+
> distinctive type signature is preserved. If needed, you can reference each
17+
> function's distinctive type with e.g. `typeof msg`.
18+
1119
### `configureLocalization(configuration)`
1220

13-
Set runtime localization configuration.
21+
Set configuration parameters for lit-localize when in runtime mode. Returns an
22+
object with functions:
23+
24+
- [`getLocale`](#getlocale-string): Return the active locale code.
25+
- [`setLocale`](#setlocalelocale-string-promise): Set the active locale code.
1426

15-
In runtime mode, this function must be called once, before any calls to `msg()`.
27+
Throws if called more than once.
28+
29+
When in transform mode, the lit-localize CLI will error if this function is
30+
called. Use
31+
[`configureTransformLocalization`](#configuretransformlocalizationconfiguration)
32+
instead.
1633

1734
The `configuration` object must have the following properties:
1835

@@ -30,37 +47,65 @@ The `configuration` object must have the following properties:
3047
Example:
3148

3249
```typescript
33-
configureLocalization({
50+
const {getLocale, setLocale} = configureLocalization({
3451
sourceLocale: 'en',
3552
targetLocales: ['es-419', 'zh_CN'],
3653
loadLocale: (locale) => import(`/${locale}.js`),
3754
});
3855
```
3956

40-
In transform mode, this function is not required, and calls to it will be
41-
replaced with `undefined`.
57+
### `configureTransformLocalization(configuration)`
4258

43-
### `getLocale() => string`
59+
Set configuration parameters for lit-localize when in transform mode. Returns an
60+
object with function:
4461

45-
Return the active locale code.
62+
- [`getLocale`](#getlocale-string): Return the active locale code.
4663

47-
In transform mode, calls to this function are transformed into the static locale
48-
code string for each emitted locale.
64+
(Note that [`setLocale`](#setlocalelocale-string-promise) is not available from
65+
this function, because changing locales at runtime is not supported in transform
66+
mode.)
4967

50-
### `setLocale(locale: string)`
68+
Throws if called more than once.
5169

52-
Set the active locale code, and begin loading templates for that locale using
53-
the `loadLocale` function that was passed to `configureLocalization`.
70+
The `configuration` object must have the following properties:
71+
72+
- `sourceLocale: string`: Required locale code in which source templates in this
73+
project are written, and the active locale.
74+
75+
Example:
76+
77+
```typescript
78+
const {getLocale} = configureLocalization({
79+
sourceLocale: 'en',
80+
});
81+
```
82+
83+
In transform mode, calls to this function are transformed to an object with a
84+
`getLocale` implementation that returns the static locale code for each locale
85+
bundle. For example:
86+
87+
```typescript
88+
const {getLocale} = {getLocale: () => 'es-419'};
89+
```
90+
91+
### `getLocale(): string`
5492

55-
In transform mode, calls to this function are replaced with `undefined`.
93+
Return the active locale code.
94+
95+
### `setLocale(locale: string): Promise`
5696

57-
### `localeReady() => Promise`
97+
Set the active locale code, and begin loading templates for that locale using
98+
the `loadLocale` function that was passed to `configureLocalization`. Returns a
99+
promise that resolves when the next locale is ready to be rendered.
58100

59-
Return a promise that is resolved when the next set of templates are loaded and
60-
available for rendering. Applications in runtime mode should always `await localeReady()` before rendering.
101+
Note that if a second call to `setLocale` is made while the first requested
102+
locale is still loading, then the second call takes precedence, and the promise
103+
returned from the first call will resolve when second locale is ready. If you
104+
need to know whether a particular locale was loaded, check `getLocale` after the
105+
promise resolves.
61106

62-
In transform mode, calls to this function are replaced with
63-
`Promise.resolve(undefined)`.
107+
Throws if the given locale is not contained by the configured `sourceLocale` or
108+
`targetLocales`.
64109

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

src/outputters/runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {applyPatches, Patches} from '../patches';
1414
import {Locale} from '../locales';
1515
import {Config} from '../config';
1616
import {KnownError} from '../error';
17-
import {escapeStringLiteral} from '../typescript';
17+
import {escapeStringToEmbedInTemplateLiteral} from '../typescript';
1818
import * as fs from 'fs';
1919
import * as pathLib from 'path';
2020

@@ -444,7 +444,7 @@ function makeMessageString(
444444
const fragments = [];
445445
for (const content of contents) {
446446
if (typeof content === 'string') {
447-
fragments.push(escapeStringLiteral(content));
447+
fragments.push(escapeStringToEmbedInTemplateLiteral(content));
448448
} else {
449449
fragments.push(content.untranslatable);
450450
}

src/outputters/transform.ts

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import {Message} from '../messages';
1313
import {Locale} from '../locales';
1414
import {Config} from '../config';
1515
import * as ts from 'typescript';
16-
import {isLitExpression, isMsgCall, isStaticString} from '../program-analysis';
16+
import {isLitTemplate, isMsgCall, isStaticString} from '../program-analysis';
1717
import {KnownError} from '../error';
18-
import {escapeStringLiteral} from '../typescript';
18+
import {escapeStringToEmbedInTemplateLiteral} from '../typescript';
1919
import * as pathLib from 'path';
2020

2121
/**
@@ -103,7 +103,7 @@ class Transformer {
103103
}
104104

105105
// html`<b>${msg('greeting', 'hello')}</b>` -> html`<b>hola</b>`
106-
if (isLitExpression(node)) {
106+
if (isLitTemplate(node)) {
107107
// If an html-tagged template literal embeds a msg call, we want to
108108
// collapse the result of that msg call into the parent template.
109109
return tagLit(
@@ -119,34 +119,43 @@ class Transformer {
119119
}
120120

121121
if (ts.isCallExpression(node)) {
122-
// configureLocalization(...) -> undefined
122+
// configureTransformLocalization(...) -> {getLocale: () => "es-419"}
123123
if (
124124
this.typeHasProperty(
125125
node.expression,
126-
'_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_'
126+
'_LIT_LOCALIZE_CONFIGURE_TRANSFORM_LOCALIZATION_'
127127
)
128128
) {
129-
return ts.createIdentifier('undefined');
130-
}
131-
132-
// getLocale() -> "es-419"
133-
if (this.typeHasProperty(node.expression, '_LIT_LOCALIZE_GET_LOCALE_')) {
134-
return ts.createStringLiteral(this.locale);
135-
}
136-
137-
// setLocale("es-419") -> undefined
138-
if (this.typeHasProperty(node.expression, '_LIT_LOCALIZE_SET_LOCALE_')) {
139-
return ts.createIdentifier('undefined');
129+
return ts.createObjectLiteral(
130+
[
131+
ts.createPropertyAssignment(
132+
ts.createIdentifier('getLocale'),
133+
ts.createArrowFunction(
134+
undefined,
135+
undefined,
136+
[],
137+
undefined,
138+
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
139+
ts.createStringLiteral(this.locale)
140+
)
141+
),
142+
],
143+
false
144+
);
140145
}
141146

142-
// localeReady() -> Promise.resolve(undefined)
147+
// configureLocalization(...) -> Error
143148
if (
144-
this.typeHasProperty(node.expression, '_LIT_LOCALIZE_LOCALE_READY_')
149+
this.typeHasProperty(
150+
node.expression,
151+
'_LIT_LOCALIZE_CONFIGURE_LOCALIZATION_'
152+
)
145153
) {
146-
return ts.createCall(
147-
ts.createPropertyAccess(ts.createIdentifier('Promise'), 'resolve'),
148-
[],
149-
[ts.createIdentifier('undefined')]
154+
// TODO(aomarks) This error is not surfaced earlier in the analysis phase
155+
// as a nicely formatted diagnostic, but it should be.
156+
throw new KnownError(
157+
'Cannot use configureLocalization in transform mode. ' +
158+
'Use configureTransformLocalization instead.'
150159
);
151160
}
152161

@@ -200,7 +209,7 @@ class Transformer {
200209
!(
201210
ts.isStringLiteral(arg1) ||
202211
ts.isTemplateLiteral(arg1) ||
203-
isLitExpression(arg1) ||
212+
isLitTemplate(arg1) ||
204213
ts.isArrowFunction(arg1)
205214
)
206215
) {
@@ -221,19 +230,19 @@ class Transformer {
221230
if (
222231
!ts.isStringLiteral(arg1.body) &&
223232
!ts.isTemplateLiteral(arg1.body) &&
224-
!isLitExpression(arg1.body)
233+
!isLitTemplate(arg1.body)
225234
) {
226235
throw new KnownError(
227236
'Expected function body to be a template or string'
228237
);
229238
}
230-
if (isLitExpression(arg1.body)) {
239+
if (isLitTemplate(arg1.body)) {
231240
isLitTagged = true;
232241
template = arg1.body.template;
233242
} else {
234243
template = arg1.body;
235244
}
236-
} else if (isLitExpression(arg1)) {
245+
} else if (isLitTemplate(arg1)) {
237246
isLitTagged = true;
238247
template = arg1.template;
239248
} else {
@@ -248,7 +257,7 @@ class Transformer {
248257
const templateLiteralBody = translation.contents
249258
.map((content) =>
250259
typeof content === 'string'
251-
? escapeStringLiteral(content)
260+
? escapeStringToEmbedInTemplateLiteral(content)
252261
: content.untranslatable
253262
)
254263
.join('');
@@ -260,6 +269,7 @@ class Transformer {
260269
// manipulation of placeholder contents. We should validate that the set
261270
// of translated placeholders is exactly equal to the set of original
262271
// source placeholders (order can change, but contents can't).
272+
// See https://github.com/PolymerLabs/lit-localize/issues/49
263273
template = parseStringAsTemplateLiteral(templateLiteralBody);
264274
}
265275
// TODO(aomarks) Emit a warning that a translation was missing.
@@ -371,7 +381,7 @@ class Transformer {
371381
fragments.push(expression.text);
372382
} else if (ts.isTemplateLiteral(expression)) {
373383
fragments.push(...this.recursivelyFlattenTemplate(expression, false));
374-
} else if (isLit && isLitExpression(expression)) {
384+
} else if (isLit && isLitTemplate(expression)) {
375385
fragments.push(
376386
...this.recursivelyFlattenTemplate(expression.template, true)
377387
);
@@ -410,18 +420,16 @@ class Transformer {
410420
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
411421
node.moduleSpecifier
412422
);
413-
if (!moduleSymbol) {
423+
if (!moduleSymbol || !moduleSymbol.exports) {
414424
return false;
415425
}
416-
// TODO(aomarks) Is there a better way to reliably identify the lit-localize
417-
// modules that don't require this cast? We could export a const with a
418-
// known name and then look through `exports`, but it doesn't seem good to
419-
// polute the modules like that.
420-
const file = (moduleSymbol.valueDeclaration as unknown) as {
421-
identifiers: Map<string, unknown>;
422-
};
423-
for (const id of file.identifiers.keys()) {
424-
if (id === '_LIT_LOCALIZE_MSG_' || id === '_LIT_LOCALIZE_LOCALIZED_') {
426+
const exports = moduleSymbol.exports.values();
427+
for (const xport of exports as typeof exports & {
428+
[Symbol.iterator](): Iterator<ts.Symbol>;
429+
}) {
430+
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
431+
const props = this.typeChecker.getPropertiesOfType(type);
432+
if (props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_')) {
425433
return true;
426434
}
427435
}

src/program-analysis.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function extractMsg(
107107
};
108108
}
109109

110-
if (isLitExpression(contentsArg)) {
110+
if (isLitTemplate(contentsArg)) {
111111
if (ts.isNoSubstitutionTemplateLiteral(contentsArg.template)) {
112112
// E.g. msg('foo', html`bar <b>baz</b>`)
113113
return {
@@ -186,7 +186,7 @@ function functionTemplate(
186186
if (
187187
!ts.isTemplateExpression(body) &&
188188
!ts.isNoSubstitutionTemplateLiteral(body) &&
189-
!isLitExpression(body)
189+
!isLitTemplate(body)
190190
) {
191191
return createDiagnostic(
192192
file,
@@ -195,7 +195,7 @@ function functionTemplate(
195195
`or a lit-html template, without braces`
196196
);
197197
}
198-
const template = isLitExpression(body) ? body.template : body;
198+
const template = isLitTemplate(body) ? body.template : body;
199199
const parts: Array<string | {identifier: string}> = [];
200200
if (ts.isTemplateExpression(template)) {
201201
const spans = template.templateSpans;
@@ -220,8 +220,8 @@ function functionTemplate(
220220
// A NoSubstitutionTemplateLiteral. No spans.
221221
parts.push(template.text);
222222
}
223-
const isLitTemplate = isLitExpression(body);
224-
const contents = isLitTemplate
223+
const isLit = isLitTemplate(body);
224+
const contents = isLit
225225
? replaceExpressionsAndHtmlWithPlaceholders(parts)
226226
: parts.map((part) =>
227227
typeof part === 'string'
@@ -235,7 +235,7 @@ function functionTemplate(
235235
file,
236236
descStack: descStack.map((desc) => desc.text),
237237
params,
238-
isLitTemplate,
238+
isLitTemplate: isLit,
239239
};
240240
}
241241

@@ -480,7 +480,7 @@ export function isStaticString(
480480
/**
481481
* E.g. html`foo` or html`foo${bar}`
482482
*/
483-
export function isLitExpression(
483+
export function isLitTemplate(
484484
node: ts.Node
485485
): node is ts.TaggedTemplateExpression {
486486
return (

0 commit comments

Comments
 (0)