diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
index bb4bdf2e30..96ab23dc36 100644
--- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
+++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
@@ -223,6 +223,38 @@ runInEachFileSystem(() => {
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: '
A message
'
+ })
+ 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', () => {
it('should run ngcc without errors for fesm2015', async() => {
const promise = mainNgcc({
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
index 1cdeb0e46f..77456f8677 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
@@ -262,13 +262,9 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
}
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
- if (this.scriptTarget < ts.ScriptTarget.ES2015) {
- // This should never happen.
- throw new Error(
- 'Unsupported mode: Visiting a localized string (which produces a tagged template ' +
- `literal) ' while targeting ${ts.ScriptTarget[this.scriptTarget]}.`);
- }
- return visitLocalizedString(ast, context, this);
+ return this.scriptTarget >= ts.ScriptTarget.ES2015 ?
+ createLocalizedStringTaggedTemplate(ast, context, this) :
+ createLocalizedStringFunctionCall(ast, context, this, this.imports);
}
visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression
@@ -467,7 +463,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
}
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 {
@@ -550,10 +546,11 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
}
/**
- * A helper to reduce duplication, since this functionality is required in both
- * `ExpressionTranslatorVisitor` and `TypeTranslatorVisitor`.
+ * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted
+ * output.
*/
-function visitLocalizedString(ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
+function createLocalizedStringTaggedTemplate(
+ ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
let template: ts.TemplateLiteral;
const metaBlock = ast.serializeI18nHead();
if (ast.messageParts.length === 1) {
@@ -575,3 +572,58 @@ function visitLocalizedString(ast: LocalizedString, context: Context, visitor: E
}
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,
+ ]);
+}