fix(ngcc): render localized strings when in ES5 format (#33857)
Recently the ngtsc translator was modified to be more `ScriptTarget` aware, which basically means that it will not generate non-ES5 code when the output format is ES5 or similar. This commit enhances that change by also "downleveling" localized messages. In ES2015 the messages use tagged template literals, which are not available in ES5. PR Close #33857
This commit is contained in:
parent
346e1c14d9
commit
bf1bcd1e08
|
@ -223,6 +223,38 @@ runInEachFileSystem(() => {
|
||||||
expect(esm5Contents).toContain(`export {InternalFooModule} from './src/internal';`);
|
expect(esm5Contents).toContain(`export {InternalFooModule} from './src/internal';`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use `$localize` calls rather than tagged templates in ES5 generated code', () => {
|
||||||
|
compileIntoFlatEs5Package('test-package', {
|
||||||
|
'/index.ts': `
|
||||||
|
import {Component, Input, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '[foo]',
|
||||||
|
template: '<div i18n="some:\`description\`">A message</div>'
|
||||||
|
})
|
||||||
|
export class FooComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [FooComponent],
|
||||||
|
})
|
||||||
|
export class FooModule {}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
mainNgcc({
|
||||||
|
basePath: '/node_modules',
|
||||||
|
targetEntryPointPath: 'test-package',
|
||||||
|
propertiesToConsider: ['main'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
|
||||||
|
expect(jsContents).not.toMatch(/\$localize\s*`/);
|
||||||
|
expect(jsContents)
|
||||||
|
.toMatch(
|
||||||
|
/\$localize\(ɵngcc\d+\.__makeTemplateObject\(\[":some:`description`:A message"], \[":some\\\\:\\\\`description\\\\`:A message"]\)\);/);
|
||||||
|
});
|
||||||
|
|
||||||
describe('in async mode', () => {
|
describe('in async mode', () => {
|
||||||
it('should run ngcc without errors for fesm2015', async() => {
|
it('should run ngcc without errors for fesm2015', async() => {
|
||||||
const promise = mainNgcc({
|
const promise = mainNgcc({
|
||||||
|
|
|
@ -262,13 +262,9 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
|
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
|
||||||
if (this.scriptTarget < ts.ScriptTarget.ES2015) {
|
return this.scriptTarget >= ts.ScriptTarget.ES2015 ?
|
||||||
// This should never happen.
|
createLocalizedStringTaggedTemplate(ast, context, this) :
|
||||||
throw new Error(
|
createLocalizedStringFunctionCall(ast, context, this, this.imports);
|
||||||
'Unsupported mode: Visiting a localized string (which produces a tagged template ' +
|
|
||||||
`literal) ' while targeting ${ts.ScriptTarget[this.scriptTarget]}.`);
|
|
||||||
}
|
|
||||||
return visitLocalizedString(ast, context, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression
|
visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression
|
||||||
|
@ -467,7 +463,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
|
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
|
||||||
return visitLocalizedString(ast, context, this);
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
visitExternalExpr(ast: ExternalExpr, context: Context): ts.TypeNode {
|
visitExternalExpr(ast: ExternalExpr, context: Context): ts.TypeNode {
|
||||||
|
@ -550,10 +546,11 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper to reduce duplication, since this functionality is required in both
|
* Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted
|
||||||
* `ExpressionTranslatorVisitor` and `TypeTranslatorVisitor`.
|
* output.
|
||||||
*/
|
*/
|
||||||
function visitLocalizedString(ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
|
function createLocalizedStringTaggedTemplate(
|
||||||
|
ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
|
||||||
let template: ts.TemplateLiteral;
|
let template: ts.TemplateLiteral;
|
||||||
const metaBlock = ast.serializeI18nHead();
|
const metaBlock = ast.serializeI18nHead();
|
||||||
if (ast.messageParts.length === 1) {
|
if (ast.messageParts.length === 1) {
|
||||||
|
@ -575,3 +572,58 @@ function visitLocalizedString(ast: LocalizedString, context: Context, visitor: E
|
||||||
}
|
}
|
||||||
return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
|
return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the `LocalizedString` node into a `$localize` call using the imported
|
||||||
|
* `__makeTemplateObject` helper for ES5 formatted output.
|
||||||
|
*/
|
||||||
|
function createLocalizedStringFunctionCall(
|
||||||
|
ast: LocalizedString, context: Context, visitor: ExpressionVisitor, imports: ImportManager) {
|
||||||
|
// A `$localize` message consists `messageParts` and `expressions`, which get interleaved
|
||||||
|
// together. The interleaved pieces look like:
|
||||||
|
// `[messagePart0, expression0, messagePart1, expression1, messagePart2]`
|
||||||
|
//
|
||||||
|
// Note that there is always a message part at the start and end, and so therefore
|
||||||
|
// `messageParts.length === expressions.length + 1`.
|
||||||
|
//
|
||||||
|
// Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters.
|
||||||
|
// The metadata is attached to the first and subsequent message parts by calls to
|
||||||
|
// `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively.
|
||||||
|
|
||||||
|
// The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` array.
|
||||||
|
const messageParts = [ast.serializeI18nHead()];
|
||||||
|
const expressions: any[] = [];
|
||||||
|
|
||||||
|
// The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed
|
||||||
|
// into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]`
|
||||||
|
for (let i = 1; i < ast.messageParts.length; i++) {
|
||||||
|
expressions.push(ast.expressions[i - 1].visitExpression(visitor, context));
|
||||||
|
messageParts.push(ast.serializeI18nTemplatePart(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()`
|
||||||
|
// helper, so we must ensure it has been imported.
|
||||||
|
const {moduleImport, symbol} = imports.generateNamedImport('tslib', '__makeTemplateObject');
|
||||||
|
const __makeTemplateObjectHelper = (moduleImport === null) ?
|
||||||
|
ts.createIdentifier(symbol) :
|
||||||
|
ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
|
||||||
|
|
||||||
|
// Generate the call in the form:
|
||||||
|
// `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);`
|
||||||
|
return ts.createCall(
|
||||||
|
/* expression */ ts.createIdentifier('$localize'),
|
||||||
|
/* typeArguments */ undefined,
|
||||||
|
/* argumentsArray */[
|
||||||
|
ts.createCall(
|
||||||
|
/* expression */ __makeTemplateObjectHelper,
|
||||||
|
/* typeArguments */ undefined,
|
||||||
|
/* argumentsArray */
|
||||||
|
[
|
||||||
|
ts.createArrayLiteral(
|
||||||
|
messageParts.map(messagePart => ts.createStringLiteral(messagePart.cooked))),
|
||||||
|
ts.createArrayLiteral(
|
||||||
|
messageParts.map(messagePart => ts.createStringLiteral(messagePart.raw))),
|
||||||
|
]),
|
||||||
|
...expressions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue