From fa79f516451bb5347f3151065210b215c567c9ee Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 30 Jul 2019 18:02:17 +0100 Subject: [PATCH] refactor(ivy): update the compiler to emit `$localize` tags (#31609) This commit changes the Angular compiler (ivy-only) to generate `$localize` tagged strings for component templates that use `i18n` attributes. BREAKING CHANGE Since `$localize` is a global function, it must be included in any applications that use i18n. This is achieved by importing the `@angular/localize` package into an appropriate bundle, where it will be executed before the renderer needs to call `$localize`. For CLI based projects, this is best done in the `polyfills.ts` file. ```ts import '@angular/localize'; ``` For non-CLI applications this could be added as a script to the index.html file or another suitable script file. PR Close #31609 --- integration/_payload-limits.json | 2 +- .../side-effects/snapshots/core/esm2015.js | 14 + .../side-effects/snapshots/core/esm5.js | 14 + karma-js.conf.js | 1 + .../benchmarks/src/expanding_rows/BUILD.bazel | 1 + .../benchmarks/src/expanding_rows/index.ts | 2 + .../src/ngtsc/translator/src/translator.ts | 50 ++ .../src/transformers/node_emitter.ts | 5 + .../test/compliance/mock_compile.ts | 33 +- .../compliance/r3_view_compiler_i18n_spec.ts | 536 ++++++++++-------- packages/compiler/src/constant_pool.ts | 1 + .../compiler/src/output/abstract_emitter.ts | 13 + packages/compiler/src/output/output_ast.ts | 38 ++ .../compiler/src/output/output_interpreter.ts | 5 +- .../compiler/src/render3/view/i18n/context.ts | 4 +- .../src/render3/view/i18n/get_msg_utils.ts | 74 +++ .../src/render3/view/i18n/icu_serializer.ts | 47 ++ .../src/render3/view/i18n/localize_utils.ts | 128 +++++ .../compiler/src/render3/view/i18n/meta.ts | 63 +- .../src/render3/view/i18n/serializer.ts | 66 --- .../compiler/src/render3/view/i18n/util.ts | 148 +---- .../compiler/src/render3/view/template.ts | 181 +++--- .../compiler/test/render3/view/i18n_spec.ts | 184 ++++-- packages/core/src/core.ts | 11 + packages/core/src/render3/i18n.ts | 90 ++- packages/core/src/render3/index.ts | 1 - packages/core/test/BUILD.bazel | 1 + packages/core/test/acceptance/i18n_spec.ts | 3 +- .../core/test/bundling/todo_i18n/BUILD.bazel | 2 + .../core/test/bundling/todo_i18n/index.ts | 23 +- .../test/bundling/todo_i18n/todo_e2e_spec.ts | 6 + .../linker/ng_container_integration_spec.ts | 3 +- packages/core/testing/BUILD.bazel | 1 + scripts/ci/run_angular_material_unit_tests.sh | 3 + test-main.js | 2 + 35 files changed, 1173 insertions(+), 583 deletions(-) create mode 100644 packages/compiler/src/render3/view/i18n/get_msg_utils.ts create mode 100644 packages/compiler/src/render3/view/i18n/icu_serializer.ts create mode 100644 packages/compiler/src/render3/view/i18n/localize_utils.ts delete mode 100644 packages/compiler/src/render3/view/i18n/serializer.ts diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index fb86788fb6..9773f8a944 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 125448, + "main": 125882, "polyfills": 45340 } } diff --git a/integration/side-effects/snapshots/core/esm2015.js b/integration/side-effects/snapshots/core/esm2015.js index 5f8109856a..47de5e8713 100644 --- a/integration/side-effects/snapshots/core/esm2015.js +++ b/integration/side-effects/snapshots/core/esm2015.js @@ -1,3 +1,17 @@ import "rxjs"; import "rxjs/operators"; + +const __globalThis = "undefined" !== typeof globalThis && globalThis; + +const __window = "undefined" !== typeof window && window; + +const __self = "undefined" !== typeof self && "undefined" !== typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && self; + +const __global = "undefined" !== typeof global && global; + +const _global = __globalThis || __global || __window || __self; + +if (ngDevMode) _global.$localize = _global.$localize || function() { + throw new Error("The global function `$localize` is missing. Please add `import '@angular/localize';` to your polyfills.ts file."); +}; diff --git a/integration/side-effects/snapshots/core/esm5.js b/integration/side-effects/snapshots/core/esm5.js index 09cc6db200..d5fd28b2d8 100644 --- a/integration/side-effects/snapshots/core/esm5.js +++ b/integration/side-effects/snapshots/core/esm5.js @@ -3,3 +3,17 @@ import "tslib"; import "rxjs"; import "rxjs/operators"; + +var __globalThis = "undefined" !== typeof globalThis && globalThis; + +var __window = "undefined" !== typeof window && window; + +var __self = "undefined" !== typeof self && "undefined" !== typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && self; + +var __global = "undefined" !== typeof global && global; + +var _global = __globalThis || __global || __window || __self; + +if (ngDevMode) _global.$localize = _global.$localize || function() { + throw new Error("The global function `$localize` is missing. Please add `import '@angular/localize';` to your polyfills.ts file."); +}; diff --git a/karma-js.conf.js b/karma-js.conf.js index b5c6ea800f..7eacd89f86 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -82,6 +82,7 @@ module.exports = function(config) { 'dist/all/@angular/elements/schematics/**', 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', + 'dist/all/@angular/localize/**/test/**', 'dist/all/@angular/router/**/test/**', 'dist/all/@angular/platform-browser/testing/e2e_util.js', 'dist/all/angular1_router.js', diff --git a/modules/benchmarks/src/expanding_rows/BUILD.bazel b/modules/benchmarks/src/expanding_rows/BUILD.bazel index b3d6925e74..08362e9e72 100644 --- a/modules/benchmarks/src/expanding_rows/BUILD.bazel +++ b/modules/benchmarks/src/expanding_rows/BUILD.bazel @@ -14,6 +14,7 @@ ng_module( "//packages:types", "//packages/common", "//packages/core", + "//packages/localize", "//packages/platform-browser", "@npm//rxjs", ], diff --git a/modules/benchmarks/src/expanding_rows/index.ts b/modules/benchmarks/src/expanding_rows/index.ts index 743f0ee323..f46ffce318 100644 --- a/modules/benchmarks/src/expanding_rows/index.ts +++ b/modules/benchmarks/src/expanding_rows/index.ts @@ -5,6 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +// This benchmark uses i18n in its `ExpandingRowSummary` component so `$localize` must be loaded. +import '@angular/localize'; import {enableProdMode} from '@angular/core'; import {platformBrowser} from '@angular/platform-browser'; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 4cef26deac..58422df55c 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -7,6 +7,7 @@ */ import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeVisitor, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {LocalizedString} from '@angular/compiler/src/output/output_ast'; import * as ts from 'typescript'; import {DefaultImportRecorder, ImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports'; @@ -249,6 +250,10 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor return expr; } + visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression { + return visitLocalizedString(ast, context, this); + } + visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression |ts.Identifier { if (ast.value.moduleName === null || ast.value.name === null) { @@ -435,6 +440,10 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { return ts.createLiteral(ast.value as string); } + visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression { + return visitLocalizedString(ast, context, this); + } + visitExternalExpr(ast: ExternalExpr, context: Context): ts.TypeNode { if (ast.value.moduleName === null || ast.value.name === null) { throw new Error(`Import unknown module or symbol`); @@ -512,3 +521,44 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { return ts.createTypeQueryNode(expr as ts.Identifier); } } + +/** + * A helper to reduce duplication, since this functionality is required in both + * `ExpressionTranslatorVisitor` and `TypeTranslatorVisitor`. + */ +function visitLocalizedString(ast: LocalizedString, context: Context, visitor: ExpressionVisitor) { + let template: ts.TemplateLiteral; + if (ast.messageParts.length === 1) { + template = ts.createNoSubstitutionTemplateLiteral(ast.messageParts[0]); + } else { + const head = ts.createTemplateHead(ast.messageParts[0]); + const spans: ts.TemplateSpan[] = []; + for (let i = 1; i < ast.messageParts.length; i++) { + const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context); + spans.push(ts.createTemplateSpan( + resolvedExpression, ts.createTemplateMiddle(prefixWithPlaceholderMarker( + ast.messageParts[i], ast.placeHolderNames[i - 1])))); + } + if (spans.length > 0) { + // The last span is supposed to have a tail rather than a middle + spans[spans.length - 1].literal.kind = ts.SyntaxKind.TemplateTail; + } + template = ts.createTemplateExpression(head, spans); + } + return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); +} + +/** + * We want our tagged literals to include placeholder name information to aid runtime translation. + * + * The expressions are marked with placeholder names by postfixing the expression with + * `:placeHolderName:`. To achieve this, we actually "prefix" the message part that follows the + * expression. + * + * @param messagePart the message part that follows the current expression. + * @param placeHolderName the name of the placeholder for the current expression. + * @returns the prefixed message part. + */ +function prefixWithPlaceholderMarker(messagePart: string, placeHolderName: string) { + return `:${placeHolderName}:${messagePart}`; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index 66c31a1458..1cd37b45e8 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -7,6 +7,7 @@ */ import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {LocalizedString} from '@angular/compiler/src/output/output_ast'; import * as ts from 'typescript'; import {error} from './util'; @@ -535,6 +536,10 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { visitLiteralExpr(expr: LiteralExpr) { return this.record(expr, createLiteral(expr.value)); } + visitLocalizedString(expr: LocalizedString, context: any) { + throw new Error('localized strings are not supported in pre-ivy mode.'); + } + visitExternalExpr(expr: ExternalExpr) { return this.record(expr, this._visitIdentifier(expr.value)); } diff --git a/packages/compiler-cli/test/compliance/mock_compile.ts b/packages/compiler-cli/test/compliance/mock_compile.ts index 47d2c1f171..7fc3fc6b87 100644 --- a/packages/compiler-cli/test/compliance/mock_compile.ts +++ b/packages/compiler-cli/test/compliance/mock_compile.ts @@ -15,12 +15,14 @@ import {NgtscProgram} from '../../src/ngtsc/program'; const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; const OPERATOR = /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./; -const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"|`(\\`[\s\S])*?`/; +const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"/; +const BACKTICK_STRING = /\\`(([\s\S]*?)(\$\{[^}]*?\})?)*?\\`/; +const BACKTICK_INTERPOLATION = /(\$\{[^}]*\})/; const NUMBER = /\d+/; const ELLIPSIS = '…'; const TOKEN = new RegExp( - `\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`, + `\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|(${BACKTICK_STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`, 'y'); type Piece = string | RegExp; @@ -30,6 +32,8 @@ const SKIP = /(?:.|\n|\r)*/; const ERROR_CONTEXT_WIDTH = 30; // Transform the expected output to set of tokens function tokenize(text: string): Piece[] { + // TOKEN.lastIndex is stateful so we cache the `lastIndex` and restore it at the end of the call. + const lastIndex = TOKEN.lastIndex; TOKEN.lastIndex = 0; let match: RegExpMatchArray|null; @@ -42,6 +46,8 @@ function tokenize(text: string): Piece[] { pieces.push(IDENTIFIER); } else if (token === ELLIPSIS) { pieces.push(SKIP); + } else if (match = BACKTICK_STRING.exec(token)) { + pieces.push(...tokenizeBackTickString(token)); } else { pieces.push(token); } @@ -57,10 +63,33 @@ function tokenize(text: string): Piece[] { `Invalid test, no token found for "${text[tokenizedTextEnd]}" ` + `(context = '${text.substr(from, to)}...'`); } + // Reset the lastIndex in case we are in a recursive `tokenize()` call. + TOKEN.lastIndex = lastIndex; return pieces; } +/** + * Back-ticks are escaped as "\`" so we must strip the backslashes. + * Also the string will likely contain interpolations and if an interpolation holds an + * identifier we will need to match that later. So tokenize the interpolation too! + */ +function tokenizeBackTickString(str: string): Piece[] { + const pieces: Piece[] = ['`']; + const backTickPieces = str.slice(2, -2).split(BACKTICK_INTERPOLATION); + backTickPieces.forEach((backTickPiece) => { + if (BACKTICK_INTERPOLATION.test(backTickPiece)) { + // An interpolation so tokenize this expression + pieces.push(...tokenize(backTickPiece)); + } else { + // Not an interpolation so just add it as a piece + pieces.push(backTickPiece); + } + }); + pieces.push('`'); + return pieces; +} + export function expectEmit( source: string, expected: string, description: string, assertIdentifiers?: {[name: string]: RegExp}) { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index f0390bf501..720e49b14b 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -183,7 +183,7 @@ describe('i18n support in the view compiler', () => {
Content G
`; - const output = ` + const output = String.raw ` var $I18N_0$; if (ngI18nClosureMode) { /** @@ -194,7 +194,11 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_idA$$APP_SPEC_TS_1$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Content A"); + /** + * @desc descA + * @meaning meaningA + */ + $I18N_0$ = $localize \`Content A\`; } const $_c2$ = [${AttributeMarker.I18n}, "title"]; var $I18N_3$; @@ -207,7 +211,11 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_idB$$APP_SPEC_TS_4$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("Title B"); + /** + * @desc descB + * @meaning meaningB + */ + $I18N_3$ = $localize \`Title B\`; } const $_c5$ = ["title", $I18N_3$]; var $I18N_7$; @@ -219,7 +227,10 @@ describe('i18n support in the view compiler', () => { $I18N_7$ = $MSG_EXTERNAL_4978592519614169666$$APP_SPEC_TS_8$; } else { - $I18N_7$ = $r3$.ɵɵi18nLocalize("Title C"); + /** + * @desc meaningC + */ + $I18N_7$ = $localize \`Title C\`; } const $_c9$ = ["title", $I18N_7$]; var $I18N_11$; @@ -232,7 +243,11 @@ describe('i18n support in the view compiler', () => { $I18N_11$ = $MSG_EXTERNAL_5200291527729162531$$APP_SPEC_TS_12$; } else { - $I18N_11$ = $r3$.ɵɵi18nLocalize("Title D"); + /** + * @desc descD + * @meaning meaningD + */ + $I18N_11$ = $localize \`Title D\`; } const $_c13$ = ["title", $I18N_11$]; var $I18N_15$; @@ -244,7 +259,10 @@ describe('i18n support in the view compiler', () => { $I18N_15$ = $MSG_EXTERNAL_idE$$APP_SPEC_TS_16$; } else { - $I18N_15$ = $r3$.ɵɵi18nLocalize("Title E"); + /** + * @desc meaningE + */ + $I18N_15$ = $localize \`Title E\`; } const $_c17$ = ["title", $I18N_15$]; var $I18N_19$; @@ -253,7 +271,7 @@ describe('i18n support in the view compiler', () => { $I18N_19$ = $MSG_EXTERNAL_idF$$APP_SPEC_TS_20$; } else { - $I18N_19$ = $r3$.ɵɵi18nLocalize("Title F"); + $I18N_19$ = $localize \`Title F\`; } const $_c21$ = ["title", $I18N_19$]; var $I18N_23$; @@ -265,7 +283,10 @@ describe('i18n support in the view compiler', () => { $I18N_23$ = $MSG_EXTERNAL_idG$$APP_SPEC_TS_24$; } else { - $I18N_23$ = $r3$.ɵɵi18nLocalize("Title G"); + /** + * @desc [BACKUP_MESSAGE_ID:idH]desc + */ + $I18N_23$ = $localize \`Title G\`; } const $_c25$ = ["title", $I18N_23$]; … @@ -353,7 +374,7 @@ describe('i18n support in the view compiler', () => {
`; - const output = ` + const output = String.raw ` const $_c0$ = ["id", "static", ${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { @@ -365,7 +386,11 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_8809028065680254561$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("introduction"); + /** + * @desc d + * @meaning m + */ + $I18N_1$ = $localize \`introduction\`; } const $_c1$ = ["title", $I18N_1$]; … @@ -402,7 +427,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_5526535577705876535$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("static text"); + $I18N_1$ = $localize \`static text\`; } var $I18N_2$; if (ngI18nClosureMode) { @@ -416,9 +441,12 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_8977039798304050198$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("intro {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d + * @meaning m + */ + $I18N_2$ = $localize \`intro $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } var $I18N_3$; if (ngI18nClosureMode) { @@ -432,9 +460,12 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_7432761130955693041$$APP_SPEC_TS_3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d1 + * @meaning m1 + */ + $I18N_3$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c1$ = [ "aria-roledescription", $I18N_1$, @@ -454,9 +485,14 @@ describe('i18n support in the view compiler', () => { $I18N_6$ = $MSG_EXTERNAL_7566208596013750546$$APP_SPEC_TS_6$; } else { - $I18N_6$ = $r3$.ɵɵi18nLocalize("{$interpolation} and {$interpolation_1} and again {$interpolation_2}", { - "interpolation": "\uFFFD0\uFFFD", "interpolation_1": "\uFFFD1\uFFFD", "interpolation_2": "\uFFFD2\uFFFD" - }); + /** + * @desc d2 + * @meaning m2 + */ + $I18N_6$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: and $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1: and again $` + + String.raw `{"\uFFFD2\uFFFD"}:interpolation_2:\`; } var $I18N_7$; if (ngI18nClosureMode) { @@ -466,9 +502,8 @@ describe('i18n support in the view compiler', () => { $I18N_7$ = $MSG_EXTERNAL_6639222533406278123$$APP_SPEC_TS_7$; } else { - $I18N_7$ = $r3$.ɵɵi18nLocalize("{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_7$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c3$ = [ "title", $I18N_6$, @@ -519,9 +554,12 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_8977039798304050198$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("intro {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d + * @meaning m + */ + $I18N_1$ = $localize \`intro $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c3$ = ["title", $I18N_1$]; … @@ -563,9 +601,12 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_8538466649243975456$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("different scope {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d + * @meaning m + */ + $I18N_1$ = $localize \`different scope $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c2$ = ["title", $I18N_1$]; function MyComponent_div_0_Template(rf, ctx) { @@ -624,7 +665,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_5526535577705876535$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("static text"); + $I18N_1$ = $localize \`static text\`; } var $I18N_2$; if (ngI18nClosureMode) { @@ -638,9 +679,12 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_8977039798304050198$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("intro {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d + * @meaning m + */ + $I18N_2$ = $localize \`intro $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } var $I18N_3$; if (ngI18nClosureMode) { @@ -654,9 +698,12 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_7432761130955693041$$APP_SPEC_TS_3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d1 + * @meaning m1 + */ + $I18N_3$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c1$ = [ "aria-roledescription", $I18N_1$, @@ -676,9 +723,14 @@ describe('i18n support in the view compiler', () => { $I18N_6$ = $MSG_EXTERNAL_7566208596013750546$$APP_SPEC_TS_6$; } else { - $I18N_6$ = $r3$.ɵɵi18nLocalize("{$interpolation} and {$interpolation_1} and again {$interpolation_2}", { - "interpolation": "\uFFFD0\uFFFD", "interpolation_1": "\uFFFD1\uFFFD", "interpolation_2": "\uFFFD2\uFFFD" - }); + /** + * @desc d2 + * @meaning m2 + */ + $I18N_6$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: and $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1: and again $` + + String.raw `{"\uFFFD2\uFFFD"}:interpolation_2:\`; } var $I18N_7$; if (ngI18nClosureMode) { @@ -688,9 +740,8 @@ describe('i18n support in the view compiler', () => { $I18N_7$ = $MSG_EXTERNAL_6639222533406278123$$APP_SPEC_TS_7$; } else { - $I18N_7$ = $r3$.ɵɵi18nLocalize("{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_7$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c3$ = [ "title", $I18N_6$, @@ -744,9 +795,12 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_8538466649243975456$$APP_SPEC_TS__3$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("different scope {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + /** + * @desc d + * @meaning m + */ + $I18N_2$ = $localize \`different scope $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c4$ = ["title", $I18N_2$]; function MyComponent_div_0_Template(rf, ctx) { @@ -798,7 +852,11 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7727043314656808423$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Element title"); + /** + * @desc d + * @meaning m + */ + $I18N_0$ = $localize \`Element title\`; } const $_c1$ = ["title", $I18N_0$]; var $I18N_2$; @@ -807,7 +865,7 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_4969674997806975147$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("Some content"); + $I18N_2$ = $localize \`Some content\`; } … template: function MyComponent_Template(rf, ctx) { @@ -819,7 +877,6 @@ describe('i18n support in the view compiler', () => { } } `; - verify(input, output); }); @@ -837,7 +894,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_ID_WITH_INVALID_CHARS$$APP_SPEC_TS_1$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Element title"); + $I18N_0$ = $localize \`Element title\`; } const $_c1$ = ["title", $I18N_0$]; var $I18N_2$; @@ -846,7 +903,7 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_ID_WITH_INVALID_CHARS_2$$APP_SPEC_TS_4$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize(" Some content "); + $I18N_2$ = $localize \` Some content \`; } … `; @@ -898,7 +955,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Some text 'with single quotes', \"with double quotes\" and without quotes."); + $I18N_0$ = $localize \`Some text 'with single quotes', "with double quotes" and without quotes.\`; } `; @@ -921,7 +978,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4890179241114413722$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("My i18n block #1"); + $I18N_0$ = $localize \`My i18n block #1\`; } var $I18N_1$; if (ngI18nClosureMode) { @@ -929,7 +986,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_2413150872298537152$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("My i18n block #2"); + $I18N_1$ = $localize \`My i18n block #2\`; } var $I18N_2$; if (ngI18nClosureMode) { @@ -937,7 +994,7 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_5023003143537152794$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("My i18n block #3"); + $I18N_2$ = $localize \`My i18n block #3\`; } … template: function MyComponent_Template(rf, ctx) { @@ -982,10 +1039,9 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7597881511811528589$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" Named interpolation: {$phA} Named interpolation with spaces: {$phB} ", { - "phA": "\uFFFD0\uFFFD", - "phB": "\uFFFD1\uFFFD" - }); + $I18N_0$ = $localize \` Named interpolation: $` + + String.raw `{"\uFFFD0\uFFFD"}:phA: Named interpolation with spaces: $` + + String.raw `{"\uFFFD1\uFFFD"}:phB: \`; } … consts: 2, @@ -1021,9 +1077,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_6749967533321674787$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_0$ = $localize \`$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } … template: function MyComponent_Template(rf, ctx) { @@ -1060,10 +1115,9 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_1482713963707913023$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$interpolation} {$interpolation_1} ", { - "interpolation": "\uFFFD0\uFFFD", - "interpolation_1": "\uFFFD1\uFFFD" - }); + $I18N_0$ = $localize \` $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1: \`; } … template: function MyComponent_Template(rf, ctx) { @@ -1099,9 +1153,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_572579892698764378$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("My i18n block #{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_0$ = $localize \`My i18n block #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } var $I18N_1$; if (ngI18nClosureMode) { @@ -1111,9 +1164,8 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_609623417156596326$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("My i18n block #{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_1$ = $localize \`My i18n block #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } var $I18N_2$; if (ngI18nClosureMode) { @@ -1123,9 +1175,8 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_3998119318957372120$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("My i18n block #{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_2$ = $localize \`My i18n block #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } … consts: 7, @@ -1189,11 +1240,10 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7905233330103651696$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" My i18n block #{$interpolation} {$startTagSpan}Plain text in nested element{$closeTagSpan}", { - "interpolation": "\uFFFD0\uFFFD", - "startTagSpan": "\uFFFD#2\uFFFD", - "closeTagSpan": "\uFFFD/#2\uFFFD" - }); + $I18N_0$ = $localize \` My i18n block #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + + String.raw `{"\uFFFD#2\uFFFD"}:startTagSpan:Plain text in nested element$` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeTagSpan:\`; } var $I18N_1$; if (ngI18nClosureMode) { @@ -1208,14 +1258,15 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_5788821996131681377$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize(" My i18n block #{$interpolation} {$startTagDiv}{$startTagDiv}{$startTagSpan} More bindings in more nested element: {$interpolation_1} {$closeTagSpan}{$closeTagDiv}{$closeTagDiv}", { - "interpolation": "\uFFFD0\uFFFD", - "startTagDiv": "[\uFFFD#6\uFFFD|\uFFFD#7\uFFFD]", - "startTagSpan": "\uFFFD#8\uFFFD", - "interpolation_1": "\uFFFD1\uFFFD", - "closeTagSpan": "\uFFFD/#8\uFFFD", - "closeTagDiv": "[\uFFFD/#7\uFFFD|\uFFFD/#6\uFFFD]" - }); + $I18N_1$ = $localize \` My i18n block #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + + String.raw `{"[\uFFFD#6\uFFFD|\uFFFD#7\uFFFD]"}:startTagDiv:$` + + String.raw `{"[\uFFFD#6\uFFFD|\uFFFD#7\uFFFD]"}:startTagDiv:$` + String.raw + `{"\uFFFD#8\uFFFD"}:startTagSpan: More bindings in more nested element: $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1: $` + + String.raw `{"\uFFFD/#8\uFFFD"}:closeTagSpan:$` + + String.raw `{"[\uFFFD/#7\uFFFD|\uFFFD/#6\uFFFD]"}:closeTagDiv:$` + + String.raw `{"[\uFFFD/#7\uFFFD|\uFFFD/#6\uFFFD]"}:closeTagDiv:\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$); … @@ -1280,10 +1331,9 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_4782264005467235841$$APP_SPEC_TS_3$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("Span title {$interpolation} and {$interpolation_1}", { - "interpolation": "\uFFFD0\uFFFD", - "interpolation_1": "\uFFFD1\uFFFD" - }); + $I18N_2$ = $localize \`Span title $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: and $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1:\`; } const $_c4$ = ["title", $I18N_2$]; var $I18N_0$; @@ -1296,11 +1346,10 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4446430594603971069$$APP_SPEC_TS_5$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" My i18n block #1 with value: {$interpolation} {$startTagSpan} Plain text in nested element (block #1) {$closeTagSpan}", { - "interpolation": "\uFFFD0\uFFFD", - "startTagSpan": "\uFFFD#2\uFFFD", - "closeTagSpan": "\uFFFD/#2\uFFFD" - }); + $I18N_0$ = $localize \` My i18n block #1 with value: $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + String.raw + `{"\uFFFD#2\uFFFD"}:startTagSpan: Plain text in nested element (block #1) $` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeTagSpan:\`; } var $I18N_7$; if (ngI18nClosureMode) { @@ -1310,9 +1359,8 @@ describe('i18n support in the view compiler', () => { $I18N_7$ = $MSG_EXTERNAL_2719594642740200058$$APP_SPEC_TS_8$; } else { - $I18N_7$ = $r3$.ɵɵi18nLocalize("Span title {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_7$ = $localize \`Span title $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c9$ = ["title", $I18N_7$]; var $I18N_6$; @@ -1325,11 +1373,10 @@ describe('i18n support in the view compiler', () => { $I18N_6$ = $MSG_EXTERNAL_2778714953278357902$$APP_SPEC_TS_10$; } else { - $I18N_6$ = $r3$.ɵɵi18nLocalize(" My i18n block #2 with value {$interpolation} {$startTagSpan} Plain text in nested element (block #2) {$closeTagSpan}", { - "interpolation": "\uFFFD0\uFFFD", - "startTagSpan": "\uFFFD#7\uFFFD", - "closeTagSpan": "\uFFFD/#7\uFFFD" - }); + $I18N_6$ = $localize \` My i18n block #2 with value $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + String.raw + `{"\uFFFD#7\uFFFD"}:startTagSpan: Plain text in nested element (block #2) $` + + String.raw `{"\uFFFD/#7\uFFFD"}:closeTagSpan:\`; } … consts: 9, @@ -1398,12 +1445,11 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_7679414751795588050$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize(" Some other content {$interpolation} {$startTagDiv} More nested levels with bindings {$interpolation_1} {$closeTagDiv}", { - "interpolation": "\uFFFD0\uFFFD", - "startTagDiv": "\uFFFD#3\uFFFD", - "interpolation_1": "\uFFFD1\uFFFD", - "closeTagDiv": "\uFFFD/#3\uFFFD" - }); + $I18N_1$ = $localize \` Some other content $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation: $` + + String.raw `{"\uFFFD#3\uFFFD"}:startTagDiv: More nested levels with bindings $` + + String.raw `{"\uFFFD1\uFFFD"}:interpolation_1: $` + + String.raw `{"\uFFFD/#3\uFFFD"}:closeTagDiv:\`; } … function MyComponent_div_2_Template(rf, ctx) { @@ -1469,9 +1515,8 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_2367729185105559721$$APP_SPEC_TS__2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("App logo #{$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_2$ = $localize \`App logo #$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } const $_c4$ = ["title", $I18N_2$]; function MyComponent_img_2_Template(rf, ctx) { @@ -1585,19 +1630,31 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_1221890473527419724$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" Some content {$startTagDiv_2} Some other content {$interpolation} {$startTagDiv} More nested levels with bindings {$interpolation_1} {$startTagDiv_1} Content inside sub-template {$interpolation_2} {$startTagDiv} Bottom level element {$interpolation_3} {$closeTagDiv}{$closeTagDiv}{$closeTagDiv}{$closeTagDiv}{$startTagDiv_3} Some other content {$interpolation_4} {$startTagDiv} More nested levels with bindings {$interpolation_5} {$closeTagDiv}{$closeTagDiv}", { - "startTagDiv_2": "\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD", - "closeTagDiv": "[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]", - "startTagDiv_3": "\uFFFD*3:3\uFFFD\uFFFD#1:3\uFFFD", - "interpolation": "\uFFFD0:1\uFFFD", - "startTagDiv": "[\uFFFD#2:1\uFFFD|\uFFFD#2:2\uFFFD|\uFFFD#2:3\uFFFD]", - "interpolation_1": "\uFFFD1:1\uFFFD", - "startTagDiv_1": "\uFFFD*4:2\uFFFD\uFFFD#1:2\uFFFD", - "interpolation_2": "\uFFFD0:2\uFFFD", - "interpolation_3": "\uFFFD1:2\uFFFD", - "interpolation_4": "\uFFFD0:3\uFFFD", - "interpolation_5": "\uFFFD1:3\uFFFD" - }); + $I18N_0$ = $localize \` Some content $` + + String.raw + `{"\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD"}:startTagDiv_2: Some other content $` + + String.raw `{"\uFFFD0:1\uFFFD"}:interpolation: $` + String.raw + `{"[\uFFFD#2:1\uFFFD|\uFFFD#2:2\uFFFD|\uFFFD#2:3\uFFFD]"}:startTagDiv: More nested levels with bindings $` + + String.raw `{"\uFFFD1:1\uFFFD"}:interpolation_1: $` + String.raw + `{"\uFFFD*4:2\uFFFD\uFFFD#1:2\uFFFD"}:startTagDiv_1: Content inside sub-template $` + + String.raw `{"\uFFFD0:2\uFFFD"}:interpolation_2: $` + String.raw + `{"[\uFFFD#2:1\uFFFD|\uFFFD#2:2\uFFFD|\uFFFD#2:3\uFFFD]"}:startTagDiv: Bottom level element $` + + String.raw `{"\uFFFD1:2\uFFFD"}:interpolation_3: $` + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:$` + + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:$` + + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:$` + + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:$` + + String.raw + `{"\uFFFD*3:3\uFFFD\uFFFD#1:3\uFFFD"}:startTagDiv_3: Some other content $` + + String.raw `{"\uFFFD0:3\uFFFD"}:interpolation_4: $` + String.raw + `{"[\uFFFD#2:1\uFFFD|\uFFFD#2:2\uFFFD|\uFFFD#2:3\uFFFD]"}:startTagDiv: More nested levels with bindings $` + + String.raw `{"\uFFFD1:3\uFFFD"}:interpolation_5: $` + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:$` + + String.raw + `{"[\uFFFD/#2:2\uFFFD|\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD|\uFFFD/#2:1\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD|\uFFFD/#2:3\uFFFD|\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD]"}:closeTagDiv:\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$); function MyComponent_div_3_Template(rf, ctx) { @@ -1656,11 +1713,10 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_119975189388320493$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("Some other content {$startTagSpan}{$interpolation}{$closeTagSpan}", { - "startTagSpan": "\uFFFD#2\uFFFD", - "interpolation": "\uFFFD0\uFFFD", - "closeTagSpan": "\uFFFD/#2\uFFFD" - }); + $I18N_1$ = $localize \`Some other content $` + + String.raw `{"\uFFFD#2\uFFFD"}:startTagSpan:$` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:$` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeTagSpan:\`; } … function MyComponent_div_0_Template(rf, ctx) { @@ -1707,7 +1763,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_APP_SPEC_TS_2$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("Hello"); + $I18N_1$ = $localize \`Hello\`; } … template: function MyComponent_Template(rf, ctx) { @@ -1737,7 +1793,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4890179241114413722$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("My i18n block #1"); + $I18N_0$ = $localize \`My i18n block #1\`; } … template: function MyComponent_Template(rf, ctx) { @@ -1764,7 +1820,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -1802,7 +1858,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_2413150872298537152$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("My i18n block #2"); + $I18N_0$ = $localize \`My i18n block #2\`; } var $I18N_1$; if (ngI18nClosureMode) { @@ -1810,7 +1866,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_4890179241114413722$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("My i18n block #1"); + $I18N_1$ = $localize \`My i18n block #1\`; } function MyComponent_ng_template_0_Template(rf, ctx) { if (rf & 1) { @@ -1845,7 +1901,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("Text #1"); + $I18N_1$ = $localize \`Text #1\`; } const $_c2$ = [${AttributeMarker.Styles}, "padding", "10px"]; var $I18N_3$; @@ -1854,7 +1910,7 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("Text #2"); + $I18N_3$ = $localize \`Text #2\`; } … consts: 4, @@ -1890,9 +1946,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_355394464191978948$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Some content: {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_0$ = $localize \`Some content: $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } … consts: 3, @@ -1929,9 +1984,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_355394464191978948$$APP_SPEC_TS__0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Some content: {$interpolation}", { - "interpolation": "\uFFFD0\uFFFD" - }); + $I18N_0$ = $localize \`Some content: $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation:\`; } function MyComponent_ng_template_0_Template(rf, ctx) { if (rf & 1) { @@ -1978,14 +2032,13 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_702706566400598764$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{$startTagNgTemplate}Template content: {$interpolation}{$closeTagNgTemplate}{$startTagNgContainer}Container content: {$interpolation_1}{$closeTagNgContainer}", { - "startTagNgTemplate": "\uFFFD*2:1\uFFFD", - "closeTagNgTemplate": "\uFFFD/*2:1\uFFFD", - "startTagNgContainer": "\uFFFD#3\uFFFD", - "interpolation_1": "\uFFFD0\uFFFD", - "closeTagNgContainer": "\uFFFD/#3\uFFFD", - "interpolation": "\uFFFD0:1\uFFFD" - }); + $I18N_0$ = $localize \`$` + + String.raw `{"\uFFFD*2:1\uFFFD"}:startTagNgTemplate:Template content: $` + + String.raw `{"\uFFFD0:1\uFFFD"}:interpolation:$` + + String.raw `{"\uFFFD/*2:1\uFFFD"}:closeTagNgTemplate:$` + + String.raw `{"\uFFFD#3\uFFFD"}:startTagNgContainer:Container content: $` + + String.raw `{"\uFFFD0\uFFFD"}:interpolation_1:$` + + String.raw `{"\uFFFD/#3\uFFFD"}:closeTagNgContainer:\`; } function MyComponent_ng_template_2_Template(rf, ctx) { if (rf & 1) { @@ -2035,7 +2088,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2046,7 +2099,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2132,13 +2185,19 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_2051477021417799640$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{$startTagNgTemplate} Template A: {$interpolation} {$startTagNgTemplate} Template B: {$interpolation_1} {$startTagNgTemplate} Template C: {$interpolation_2} {$closeTagNgTemplate}{$closeTagNgTemplate}{$closeTagNgTemplate}", { - "startTagNgTemplate": "[\uFFFD*2:1\uFFFD|\uFFFD*2:2\uFFFD|\uFFFD*1:3\uFFFD]", - "closeTagNgTemplate": "[\uFFFD/*1:3\uFFFD|\uFFFD/*2:2\uFFFD|\uFFFD/*2:1\uFFFD]", - "interpolation": "\uFFFD0:1\uFFFD", - "interpolation_1": "\uFFFD0:2\uFFFD", - "interpolation_2": "\uFFFD0:3\uFFFD" - }); + $I18N_0$ = $localize \`$` + + String.raw + `{"[\uFFFD*2:1\uFFFD|\uFFFD*2:2\uFFFD|\uFFFD*1:3\uFFFD]"}:startTagNgTemplate: Template A: $` + + String.raw `{"\uFFFD0:1\uFFFD"}:interpolation: $` + String.raw + `{"[\uFFFD*2:1\uFFFD|\uFFFD*2:2\uFFFD|\uFFFD*1:3\uFFFD]"}:startTagNgTemplate: Template B: $` + + String.raw `{"\uFFFD0:2\uFFFD"}:interpolation_1: $` + String.raw + `{"[\uFFFD*2:1\uFFFD|\uFFFD*2:2\uFFFD|\uFFFD*1:3\uFFFD]"}:startTagNgTemplate: Template C: $` + + String.raw `{"\uFFFD0:3\uFFFD"}:interpolation_2: $` + String.raw + `{"[\uFFFD/*1:3\uFFFD|\uFFFD/*2:2\uFFFD|\uFFFD/*2:1\uFFFD]"}:closeTagNgTemplate:$` + + String.raw + `{"[\uFFFD/*1:3\uFFFD|\uFFFD/*2:2\uFFFD|\uFFFD/*2:1\uFFFD]"}:closeTagNgTemplate:$` + + String.raw + `{"[\uFFFD/*1:3\uFFFD|\uFFFD/*2:2\uFFFD|\uFFFD/*2:1\uFFFD]"}:closeTagNgTemplate:\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$); function MyComponent_ng_template_2_Template(rf, ctx) { @@ -2184,7 +2243,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2195,7 +2254,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2251,9 +2310,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4891196282781544695$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{$tagImg} is my logo #1 ", { - "tagImg": "\uFFFD#2\uFFFD\uFFFD/#2\uFFFD" - }); + $I18N_0$ = $localize \`$` + + String.raw `{"\uFFFD#2\uFFFD\uFFFD/#2\uFFFD"}:tagImg: is my logo #1 \`; } var $I18N_2$; if (ngI18nClosureMode) { @@ -2263,9 +2321,8 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_461986953980355147$$APP_SPEC_TS__2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("{$tagImg} is my logo #2 ", { - "tagImg": "\uFFFD#1\uFFFD\uFFFD/#1\uFFFD" - }); + $I18N_2$ = $localize \`$` + + String.raw `{"\uFFFD#1\uFFFD\uFFFD/#1\uFFFD"}:tagImg: is my logo #2 \`; } function MyComponent_ng_template_3_Template(rf, ctx) { if (rf & 1) { @@ -2310,10 +2367,10 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_8537814667662432133$$APP_SPEC_TS__0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" Root content {$startTagNgContainer} Nested content {$closeTagNgContainer}", { - "startTagNgContainer": "\uFFFD*1:1\uFFFD\uFFFD#1:1\uFFFD", - "closeTagNgContainer": "\uFFFD/#1:1\uFFFD\uFFFD/*1:1\uFFFD" - }); + $I18N_0$ = $localize \` Root content $` + + String.raw + `{"\uFFFD*1:1\uFFFD\uFFFD#1:1\uFFFD"}:startTagNgContainer: Nested content $` + + String.raw `{"\uFFFD/#1:1\uFFFD\uFFFD/*1:1\uFFFD"}:closeTagNgContainer:\`; } … `; @@ -2337,7 +2394,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_6563391987554512024$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("Test"); + $I18N_0$ = $localize \`Test\`; } var $I18N_1$; if (ngI18nClosureMode) { @@ -2345,7 +2402,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_6563391987554512024$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("Test"); + $I18N_1$ = $localize \`Test\`; } … `; @@ -2367,7 +2424,9 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_APP_SPEC_TS_1$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" Hello {$startTagNgContainer}there{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" }); + $I18N_0$ = $localize \` Hello $` + + String.raw `{"\uFFFD#2\uFFFD"}:startTagNgContainer:there$` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeTagNgContainer:\`; } … consts: 3, @@ -2401,7 +2460,11 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_APP_SPEC_TS_1$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" Hello {$startTagNgContainer}there {$startTagStrong}!{$closeTagStrong}{$closeTagNgContainer}", { "startTagNgContainer": "\uFFFD#2\uFFFD", "startTagStrong": "\uFFFD#3\uFFFD", "closeTagStrong": "\uFFFD/#3\uFFFD", "closeTagNgContainer": "\uFFFD/#2\uFFFD" }); + $I18N_0$ = $localize \` Hello $` + + String.raw `{"\uFFFD#2\uFFFD"}:startTagNgContainer:there $` + + String.raw `{"\uFFFD#3\uFFFD"}:startTagStrong:!$` + + String.raw `{"\uFFFD/#3\uFFFD"}:closeTagStrong:$` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeTagNgContainer:\`; } … consts: 4, @@ -2443,10 +2506,9 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_963542717423364282$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("\n Some text\n {$startTagSpan}Text inside span{$closeTagSpan}\n ", { - "startTagSpan": "\uFFFD#3\uFFFD", - "closeTagSpan": "\uFFFD/#3\uFFFD" - }); + $I18N_0$ = $localize \`\n Some text\n $` + + String.raw `{"\uFFFD#3\uFFFD"}:startTagSpan:Text inside span$` + + String.raw `{"\uFFFD/#3\uFFFD"}:closeTagSpan:\n \`; } … template: function MyComponent_Template(rf, ctx) { @@ -2479,7 +2541,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2516,7 +2578,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_4166854826696768832$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, single {'single quotes'} double {\"double quotes\"} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, single {'single quotes'} double {"double quotes"} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2538,7 +2600,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2578,7 +2640,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2592,7 +2654,7 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}"); + $I18N_3$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}\`; } $I18N_3$ = $r3$.ɵɵi18nPostprocess($I18N_3$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2619,7 +2681,7 @@ describe('i18n support in the view compiler', () => { $I18N_5$ = $MSG_EXTERNAL_1922743304863699161$$APP_SPEC_TS__5$; } else { - $I18N_5$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}"); + $I18N_5$ = $localize \`{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}\`; } $I18N_5$ = $r3$.ɵɵi18nPostprocess($I18N_5$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -2678,7 +2740,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_2949673783721159566$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -2718,7 +2780,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_2417296354340576868$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -2744,15 +2806,14 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_5791551881115084301$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$icu} {$startBoldText}Other content{$closeBoldText}{$startTagDiv}{$startItalicText}Another content{$closeItalicText}{$closeTagDiv}", { - "startBoldText": "\uFFFD#2\uFFFD", - "closeBoldText": "\uFFFD/#2\uFFFD", - "startTagDiv": "\uFFFD#3\uFFFD", - "startItalicText": "\uFFFD#4\uFFFD", - "closeItalicText": "\uFFFD/#4\uFFFD", - "closeTagDiv": "\uFFFD/#3\uFFFD", - "icu": $I18N_1$ - }); + $I18N_0$ = $localize \` $` + + String.raw `{$I18N_1$}:icu: $` + + String.raw `{"\uFFFD#2\uFFFD"}:startBoldText:Other content$` + + String.raw `{"\uFFFD/#2\uFFFD"}:closeBoldText:$` + + String.raw `{"\uFFFD#3\uFFFD"}:startTagDiv:$` + + String.raw `{"\uFFFD#4\uFFFD"}:startItalicText:Another content$` + + String.raw `{"\uFFFD/#4\uFFFD"}:closeItalicText:$` + + String.raw `{"\uFFFD/#3\uFFFD"}:closeTagDiv:\`; } … consts: 5, @@ -2791,7 +2852,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_6879461626778511059$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -2832,7 +2893,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2843,7 +2904,7 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}"); + $I18N_2$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}\`; } $I18N_2$ = $r3$.ɵɵi18nPostprocess($I18N_2$, { "VAR_SELECT": "\uFFFD1\uFFFD" @@ -2857,10 +2918,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_2967249209167308918$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$icu} {$icu_1} ", { - "icu": $I18N_1$, - "icu_1": $I18N_2$ - }); + $I18N_0$ = $localize \` $` + + String.raw `{$I18N_1$}:icu: $` + String.raw `{$I18N_2$}:icu_1: \`; } … consts: 2, @@ -2902,7 +2961,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -2913,7 +2972,7 @@ describe('i18n support in the view compiler', () => { $I18N_2$ = $MSG_APP_SPEC_TS_2$; } else { - $I18N_2$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_2$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_2$ = $r3$.ɵɵi18nPostprocess($I18N_2$, { "VAR_SELECT": "\uFFFD1\uFFFD" @@ -2925,7 +2984,7 @@ describe('i18n support in the view compiler', () => { $I18N_4$ = $MSG_APP_SPEC_TS__4$; } else { - $I18N_4$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_4$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_4$ = $r3$.ɵɵi18nPostprocess($I18N_4$, { "VAR_SELECT": "\uFFFD0:1\uFFFD" @@ -2941,12 +3000,14 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$icu} {$startTagDiv} {$icu} {$closeTagDiv}{$startTagDiv_1} {$icu} {$closeTagDiv}", { - "startTagDiv": "\uFFFD#2\uFFFD", - "closeTagDiv": "[\uFFFD/#2\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*3:1\uFFFD]", - "startTagDiv_1": "\uFFFD*3:1\uFFFD\uFFFD#1:1\uFFFD", - "icu": "\uFFFDI18N_EXP_ICU\uFFFD" - }); + $I18N_0$ = $localize \` $` + + String.raw `{"\uFFFDI18N_EXP_ICU\uFFFD"}:icu: $` + + String.raw `{"\uFFFD#2\uFFFD"}:startTagDiv: $` + + String.raw `{"\uFFFDI18N_EXP_ICU\uFFFD"}:icu: $` + String.raw + `{"[\uFFFD/#2\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*3:1\uFFFD]"}:closeTagDiv:$` + + String.raw `{"\uFFFD*3:1\uFFFD\uFFFD#1:1\uFFFD"}:startTagDiv_1: $` + + String.raw `{"\uFFFDI18N_EXP_ICU\uFFFD"}:icu: $` + String.raw + `{"[\uFFFD/#2\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*3:1\uFFFD]"}:closeTagDiv:\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "ICU": [$I18N_1$, $I18N_2$, $I18N_4$] @@ -3008,7 +3069,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_343563413083115114$$APP_SPEC_TS_0$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT_1, select, male {male of age: {VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}} female {female} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT_1, select, male {male of age: {VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}} female {female} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -3020,7 +3081,8 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_3052001905251380936$$APP_SPEC_TS_3$; } else { - $I18N_0$ = i0.ɵɵi18nLocalize(" {$icu} ", { "icu": $I18N_1$ }); + $I18N_0$ = $localize \` $` + + String.raw `{$I18N_1$}:icu: \`; } … consts: 2, vars: 2, @@ -3063,7 +3125,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_6870293071705078389$$APP_SPEC_TS_1$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}} !} other {other - {INTERPOLATION}}}"); + $I18N_0$ = $localize \`{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}} !} other {other - {INTERPOLATION}}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -3107,7 +3169,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male} female {female} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male} female {female} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" @@ -3119,7 +3181,7 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}"); + $I18N_3$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}\`; } $I18N_3$ = $r3$.ɵɵi18nPostprocess($I18N_3$, { "VAR_SELECT": "\uFFFD0:1\uFFFD" @@ -3135,12 +3197,11 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_1194472282609532229$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$icu} {$startTagSpan} {$icu_1} {$closeTagSpan}", { - "startTagSpan": "\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD", - "closeTagSpan": "\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD", - "icu": $I18N_1$, - "icu_1": $I18N_3$ - }); + $I18N_0$ = $localize \` $` + + String.raw `{$I18N_1$}:icu: $` + + String.raw `{"\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD"}:startTagSpan: $` + + String.raw `{$I18N_3$}:icu_1: $` + + String.raw `{"\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD"}:closeTagSpan:\`; } function MyComponent_span_2_Template(rf, ctx) { if (rf & 1) { @@ -3194,7 +3255,7 @@ describe('i18n support in the view compiler', () => { $I18N_1$ = $MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$; } else { - $I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}"); + $I18N_1$ = $localize \`{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}\`; } $I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD", @@ -3202,15 +3263,15 @@ describe('i18n support in the view compiler', () => { "INTERPOLATION_1": "\uFFFD2\uFFFD" }); const $_c0$ = [${AttributeMarker.Template}, "ngIf"]; - var $I18N_3$; + var $I18N_4$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}"); - $I18N_3$ = $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$; + $I18N_4$ = $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$; } else { - $I18N_3$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}"); + $I18N_4$ = $localize \`{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}\`; } - $I18N_3$ = $r3$.ɵɵi18nPostprocess($I18N_3$, { + $I18N_4$ = $r3$.ɵɵi18nPostprocess($I18N_4$, { "VAR_SELECT": "\uFFFD0:1\uFFFD", "INTERPOLATION": "\uFFFD1:1\uFFFD" }); @@ -3220,17 +3281,16 @@ describe('i18n support in the view compiler', () => { "startTagSpan": "\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD", "closeTagSpan": "\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD", "icu": $I18N_1$, - "icu_1": $I18N_3$ + "icu_1": $I18N_4$ }); $I18N_0$ = $MSG_EXTERNAL_7186042105600518133$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize(" {$icu} {$startTagSpan} {$icu_1} {$closeTagSpan}", { - "startTagSpan": "\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD", - "closeTagSpan": "\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD", - "icu": $I18N_1$, - "icu_1": $I18N_3$ - }); + $I18N_0$ = $localize \` $` + + String.raw `{I18N_1}:icu: $` + + String.raw `{"\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD"}:startTagSpan: $` + + String.raw `{I18N_4}:icu_1: $` + + String.raw `{"\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD"}:closeTagSpan:\`; } function MyComponent_span_2_Template(rf, ctx) { if (rf & 1) { @@ -3285,7 +3345,7 @@ describe('i18n support in the view compiler', () => { $I18N_0$ = $MSG_EXTERNAL_6318060397235942326$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}"); + $I18N_0$ = $localize \`{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}\`; } $I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, { "VAR_SELECT": "\uFFFD0\uFFFD", diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index ff88cbca79..f5eea86e01 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -271,6 +271,7 @@ class KeyVisitor implements o.ExpressionVisitor { visitReadPropExpr = invalid; visitReadKeyExpr = invalid; visitCommaExpr = invalid; + visitLocalizedString = invalid; } function invalid(this: o.ExpressionVisitor, arg: o.Expression | o.Statement): never { diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index ac26cdba21..3d7a65275a 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -361,6 +361,19 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex return null; } + visitLocalizedString(ast: o.LocalizedString, ctx: EmitterVisitorContext): any { + ctx.print(ast, '$localize `' + ast.messageParts[0]); + for (let i = 1; i < ast.messageParts.length; i++) { + ctx.print(ast, '${'); + ast.expressions[i - 1].visitExpression(this, ctx); + // Add the placeholder name annotation to support runtime inlining + ctx.print(ast, `}:${ast.placeHolderNames[i - 1]}:`); + ctx.print(ast, ast.messageParts[i]); + } + ctx.print(ast, '`'); + return null; + } + abstract visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any; visitConditionalExpr(ast: o.ConditionalExpr, ctx: EmitterVisitorContext): any { diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 82d5addb91..3d4c203025 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -480,6 +480,26 @@ export class LiteralExpr extends Expression { } +export class LocalizedString extends Expression { + constructor( + public messageParts: string[], public placeHolderNames: string[], + public expressions: Expression[], sourceSpan?: ParseSourceSpan|null) { + super(STRING_TYPE, sourceSpan); + } + + isEquivalent(e: Expression): boolean { + // return e instanceof LocalizedString && this.message === e.message; + return false; + } + + isConstant() { return false; } + + visitExpression(visitor: ExpressionVisitor, context: any): any { + return visitor.visitLocalizedString(this, context); + } +} + + export class ExternalExpr extends Expression { constructor( public value: ExternalReference, type?: Type|null, public typeParams: Type[]|null = null, @@ -749,6 +769,7 @@ export interface ExpressionVisitor { visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any; visitInstantiateExpr(ast: InstantiateExpr, context: any): any; visitLiteralExpr(ast: LiteralExpr, context: any): any; + visitLocalizedString(ast: LocalizedString, context: any): any; visitExternalExpr(ast: ExternalExpr, context: any): any; visitConditionalExpr(ast: ConditionalExpr, context: any): any; visitNotExpr(ast: NotExpr, context: any): any; @@ -1074,6 +1095,14 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { visitLiteralExpr(ast: LiteralExpr, context: any): any { return this.transformExpr(ast, context); } + visitLocalizedString(ast: LocalizedString, context: any): any { + return this.transformExpr( + new LocalizedString( + ast.messageParts, ast.placeHolderNames, + this.visitAllExpressions(ast.expressions, context), ast.sourceSpan), + context); + } + visitExternalExpr(ast: ExternalExpr, context: any): any { return this.transformExpr(ast, context); } @@ -1291,6 +1320,9 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor visitLiteralExpr(ast: LiteralExpr, context: any): any { return this.visitExpression(ast, context); } + visitLocalizedString(ast: LocalizedString, context: any): any { + return this.visitExpression(ast, context); + } visitExternalExpr(ast: ExternalExpr, context: any): any { if (ast.typeParams) { ast.typeParams.forEach(type => type.visitType(this, context)); @@ -1551,6 +1583,12 @@ export function literal( return new LiteralExpr(value, type, sourceSpan); } +export function localizedString( + messageParts: string[], placeholderNames: string[], expressions: Expression[], + sourceSpan?: ParseSourceSpan | null): LocalizedString { + return new LocalizedString(messageParts, placeholderNames, expressions, sourceSpan); +} + export function isNull(exp: Expression): boolean { return exp instanceof LiteralExpr && exp.value === null; } diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts index 9f25155809..6fbde79917 100644 --- a/packages/compiler/src/output/output_interpreter.ts +++ b/packages/compiler/src/output/output_interpreter.ts @@ -5,11 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - - - import {CompileReflector} from '../compile_reflector'; - import * as o from './output_ast'; import {debugOutputAstAsTypeScript} from './ts_emitter'; @@ -239,6 +235,7 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { return new clazz(...args); } visitLiteralExpr(ast: o.LiteralExpr, ctx: _ExecutionContext): any { return ast.value; } + visitLocalizedString(ast: o.LocalizedString, context: any): any { return null; } visitExternalExpr(ast: o.ExternalExpr, ctx: _ExecutionContext): any { return this.reflector.resolveExternalReference(ast.value); } diff --git a/packages/compiler/src/render3/view/i18n/context.ts b/packages/compiler/src/render3/view/i18n/context.ts index 2c770813f3..40cd312ae4 100644 --- a/packages/compiler/src/render3/view/i18n/context.ts +++ b/packages/compiler/src/render3/view/i18n/context.ts @@ -10,7 +10,7 @@ import {AST} from '../../../expression_parser/ast'; import * as i18n from '../../../i18n/i18n_ast'; import * as o from '../../../output/output_ast'; -import {assembleBoundTextPlaceholders, findIndex, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util'; +import {assembleBoundTextPlaceholders, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util'; enum TagType { ELEMENT, @@ -142,7 +142,7 @@ export class I18nContext { return; } // try to find matching template... - const tmplIdx = findIndex(phs, findTemplateFn(context.id, context.templateIndex)); + const tmplIdx = phs.findIndex(findTemplateFn(context.id, context.templateIndex)); if (tmplIdx >= 0) { // ... if found - replace it with nested template content const isCloseTag = key.startsWith('CLOSE'); diff --git a/packages/compiler/src/render3/view/i18n/get_msg_utils.ts b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts new file mode 100644 index 0000000000..609bef106b --- /dev/null +++ b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as i18n from '../../../i18n/i18n_ast'; +import {mapLiteral} from '../../../output/map_util'; +import * as o from '../../../output/output_ast'; + +import {serializeIcuNode} from './icu_serializer'; +import {i18nMetaToDocStmt, metaFromI18nMessage} from './meta'; +import {formatI18nPlaceholderName} from './util'; + +/** Closure uses `goog.getMsg(message)` to lookup translations */ +const GOOG_GET_MSG = 'goog.getMsg'; + +export function createGoogleGetMsgStatements( + variable: o.ReadVarExpr, message: i18n.Message, closureVar: o.ReadVarExpr, + params: {[name: string]: o.Expression}): o.Statement[] { + const messageString = serializeI18nMessageForGetMsg(message); + const args = [o.literal(messageString) as o.Expression]; + if (Object.keys(params).length) { + args.push(mapLiteral(params, true)); + } + + // /** Description and meaning of message */ + // const MSG_... = goog.getMsg(..); + // I18N_X = MSG_...; + const statements = []; + const jsdocComment = i18nMetaToDocStmt(metaFromI18nMessage(message)); + if (jsdocComment !== null) { + statements.push(jsdocComment); + } + statements.push(closureVar.set(o.variable(GOOG_GET_MSG).callFn(args)).toConstDecl()); + statements.push(new o.ExpressionStatement(variable.set(closureVar))); + + return statements; +} + +/** + * This visitor walks over i18n tree and generates its string representation, including ICUs and + * placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format. + */ +class GetMsgSerializerVisitor implements i18n.Visitor { + private formatPh(value: string): string { return `{$${formatI18nPlaceholderName(value)}}`; } + + visitText(text: i18n.Text): any { return text.value; } + + visitContainer(container: i18n.Container): any { + return container.children.map(child => child.visit(this)).join(''); + } + + visitIcu(icu: i18n.Icu): any { return serializeIcuNode(icu); } + + visitTagPlaceholder(ph: i18n.TagPlaceholder): any { + return ph.isVoid ? + this.formatPh(ph.startName) : + `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`; + } + + visitPlaceholder(ph: i18n.Placeholder): any { return this.formatPh(ph.name); } + + visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { + return this.formatPh(ph.name); + } +} + +const serializerVisitor = new GetMsgSerializerVisitor(); + +export function serializeI18nMessageForGetMsg(message: i18n.Message): string { + return message.nodes.map(node => node.visit(serializerVisitor, null)).join(''); +} diff --git a/packages/compiler/src/render3/view/i18n/icu_serializer.ts b/packages/compiler/src/render3/view/i18n/icu_serializer.ts new file mode 100644 index 0000000000..8e5aa0b7ff --- /dev/null +++ b/packages/compiler/src/render3/view/i18n/icu_serializer.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as i18n from '../../../i18n/i18n_ast'; + +import {formatI18nPlaceholderName} from './util'; + +class IcuSerializerVisitor implements i18n.Visitor { + visitText(text: i18n.Text): any { return text.value; } + + visitContainer(container: i18n.Container): any { + return container.children.map(child => child.visit(this)).join(''); + } + + visitIcu(icu: i18n.Icu): any { + const strCases = + Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`); + const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`; + return result; + } + + visitTagPlaceholder(ph: i18n.TagPlaceholder): any { + return ph.isVoid ? + this.formatPh(ph.startName) : + `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`; + } + + visitPlaceholder(ph: i18n.Placeholder): any { return this.formatPh(ph.name); } + + visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { + return this.formatPh(ph.name); + } + + private formatPh(value: string): string { + return `{${formatI18nPlaceholderName(value, /* useCamelCase */ false)}}`; + } +} + +const serializer = new IcuSerializerVisitor(); +export function serializeIcuNode(icu: i18n.Icu): string { + return icu.visit(serializer); +} diff --git a/packages/compiler/src/render3/view/i18n/localize_utils.ts b/packages/compiler/src/render3/view/i18n/localize_utils.ts new file mode 100644 index 0000000000..a5ec3db8e6 --- /dev/null +++ b/packages/compiler/src/render3/view/i18n/localize_utils.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as i18n from '../../../i18n/i18n_ast'; +import * as o from '../../../output/output_ast'; + +import {serializeIcuNode} from './icu_serializer'; +import {i18nMetaToDocStmt, metaFromI18nMessage} from './meta'; +import {formatI18nPlaceholderName} from './util'; + +export function createLocalizeStatements( + variable: o.ReadVarExpr, message: i18n.Message, + params: {[name: string]: o.Expression}): o.Statement[] { + const statements = []; + + const jsdocComment = i18nMetaToDocStmt(metaFromI18nMessage(message)); + if (jsdocComment !== null) { + statements.push(jsdocComment); + } + + const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message); + statements.push(new o.ExpressionStatement(variable.set( + o.localizedString(messageParts, placeHolders, placeHolders.map(ph => params[ph]))))); + + return statements; +} + +class MessagePiece { + constructor(public text: string) {} +} +class LiteralPiece extends MessagePiece {} +class PlaceholderPiece extends MessagePiece { + constructor(name: string) { super(formatI18nPlaceholderName(name)); } +} + +/** + * This visitor walks over an i18n tree, capturing literal strings and placeholders. + * + * The result can be used for generating the `$localize` tagged template literals. + */ +class LocalizeSerializerVisitor implements i18n.Visitor { + visitText(text: i18n.Text, context: MessagePiece[]): any { + context.push(new LiteralPiece(text.value)); + } + + visitContainer(container: i18n.Container, context: MessagePiece[]): any { + container.children.forEach(child => child.visit(this, context)); + } + + visitIcu(icu: i18n.Icu, context: MessagePiece[]): any { + context.push(new LiteralPiece(serializeIcuNode(icu))); + } + + visitTagPlaceholder(ph: i18n.TagPlaceholder, context: MessagePiece[]): any { + context.push(new PlaceholderPiece(ph.startName)); + if (!ph.isVoid) { + ph.children.forEach(child => child.visit(this, context)); + context.push(new PlaceholderPiece(ph.closeName)); + } + } + + visitPlaceholder(ph: i18n.Placeholder, context: MessagePiece[]): any { + context.push(new PlaceholderPiece(ph.name)); + } + + visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { + context.push(new PlaceholderPiece(ph.name)); + } +} + +const serializerVisitor = new LocalizeSerializerVisitor(); + +/** + * Serialize an i18n message into two arrays: messageParts and placeholders. + * + * These arrays will be used to generate `$localize` tagged template literals. + * + * @param message The message to be serialized. + * @returns an object containing the messageParts and placeholders. + */ +export function serializeI18nMessageForLocalize(message: i18n.Message): + {messageParts: string[], placeHolders: string[]} { + const pieces: MessagePiece[] = []; + message.nodes.forEach(node => node.visit(serializerVisitor, pieces)); + return processMessagePieces(pieces); +} + +/** + * Convert the list of serialized MessagePieces into two arrays. + * + * One contains the literal string pieces and the other the placeholders that will be replaced by + * expressions when rendering `$localize` tagged template literals. + * + * @param pieces The pieces to process. + * @returns an object containing the messageParts and placeholders. + */ +function processMessagePieces(pieces: MessagePiece[]): + {messageParts: string[], placeHolders: string[]} { + const messageParts: string[] = []; + const placeHolders: string[] = []; + + if (pieces[0] instanceof PlaceholderPiece) { + // The first piece was a placeholder so we need to add an initial empty message part. + messageParts.push(''); + } + + for (let i = 0; i < pieces.length; i++) { + const part = pieces[i]; + if (part instanceof LiteralPiece) { + messageParts.push(part.text); + } else { + placeHolders.push(part.text); + if (pieces[i - 1] instanceof PlaceholderPiece) { + // There were two placeholders in a row, so we need to add an empty message part. + messageParts.push(''); + } + } + } + if (pieces[pieces.length - 1] instanceof PlaceholderPiece) { + // The last piece was a placeholder so we need to add a final empty message part. + messageParts.push(''); + } + return {messageParts, placeHolders}; +} \ No newline at end of file diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index e798d9a8b9..e75beed98c 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -12,8 +12,15 @@ import {createI18nMessageFactory} from '../../../i18n/i18n_parser'; import * as html from '../../../ml_parser/ast'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/interpolation_config'; import {ParseTreeResult} from '../../../ml_parser/parser'; +import * as o from '../../../output/output_ast'; -import {I18N_ATTR, I18N_ATTR_PREFIX, I18nMeta, hasI18nAttrs, icuFromI18nMessage, metaFromI18nMessage, parseI18nMeta} from './util'; +import {I18N_ATTR, I18N_ATTR_PREFIX, hasI18nAttrs, icuFromI18nMessage} from './util'; + +export type I18nMeta = { + id?: string, + description?: string, + meaning?: string +}; function setI18nRefs(html: html.Node & {i18n?: i18n.AST}, i18n: i18n.Node) { html.i18n = i18n; @@ -129,3 +136,57 @@ export function processI18nMeta( htmlAstWithErrors.rootNodes), htmlAstWithErrors.errors); } + +export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta { + return { + id: typeof id === 'string' ? id : message.id || '', + meaning: message.meaning || '', + description: message.description || '' + }; +} + +/** I18n separators for metadata **/ +const I18N_MEANING_SEPARATOR = '|'; +const I18N_ID_SEPARATOR = '@@'; + +/** + * Parses i18n metas like: + * - "@@id", + * - "description[@@id]", + * - "meaning|description[@@id]" + * and returns an object with parsed output. + * + * @param meta String that represents i18n meta + * @returns Object with id, meaning and description fields + */ +export function parseI18nMeta(meta?: string): I18nMeta { + let id: string|undefined; + let meaning: string|undefined; + let description: string|undefined; + + if (meta) { + const idIndex = meta.indexOf(I18N_ID_SEPARATOR); + const descIndex = meta.indexOf(I18N_MEANING_SEPARATOR); + let meaningAndDesc: string; + [meaningAndDesc, id] = + (idIndex > -1) ? [meta.slice(0, idIndex), meta.slice(idIndex + 2)] : [meta, '']; + [meaning, description] = (descIndex > -1) ? + [meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] : + ['', meaningAndDesc]; + } + + return {id, meaning, description}; +} + +// Converts i18n meta information for a message (id, description, meaning) +// to a JsDoc statement formatted as expected by the Closure compiler. +export function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null { + const tags: o.JSDocTag[] = []; + if (meta.description) { + tags.push({tagName: o.JSDocTagName.Desc, text: meta.description}); + } + if (meta.meaning) { + tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning}); + } + return tags.length == 0 ? null : new o.JSDocCommentStmt(tags); +} diff --git a/packages/compiler/src/render3/view/i18n/serializer.ts b/packages/compiler/src/render3/view/i18n/serializer.ts deleted file mode 100644 index e56ffef389..0000000000 --- a/packages/compiler/src/render3/view/i18n/serializer.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import * as i18n from '../../../i18n/i18n_ast'; - -import {formatI18nPlaceholderName} from './util'; - -/** - * This visitor walks over i18n tree and generates its string representation, including ICUs and - * placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format. - */ -class SerializerVisitor implements i18n.Visitor { - /** - * Keeps track of ICU nesting level, allowing to detect that we are processing elements of an ICU. - * - * This is needed due to the fact that placeholders in ICUs and in other messages are represented - * differently in Closure: - * - {$placeholder} in non-ICU case - * - {PLACEHOLDER} inside ICU - */ - private icuNestingLevel = 0; - - private formatPh(value: string): string { - const isInsideIcu = this.icuNestingLevel > 0; - const formatted = formatI18nPlaceholderName(value, /* useCamelCase */ !isInsideIcu); - return isInsideIcu ? `{${formatted}}` : `{$${formatted}}`; - } - - visitText(text: i18n.Text, context: any): any { return text.value; } - - visitContainer(container: i18n.Container, context: any): any { - return container.children.map(child => child.visit(this)).join(''); - } - - visitIcu(icu: i18n.Icu, context: any): any { - this.icuNestingLevel++; - const strCases = - Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`); - const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`; - this.icuNestingLevel--; - return result; - } - - visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any { - return ph.isVoid ? - this.formatPh(ph.startName) : - `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`; - } - - visitPlaceholder(ph: i18n.Placeholder, context: any): any { return this.formatPh(ph.name); } - - visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { - return this.formatPh(ph.name); - } -} - -const serializerVisitor = new SerializerVisitor(); - -export function getSerializedI18nContent(message: i18n.Message): string { - return message.nodes.map(node => node.visit(serializerVisitor, null)).join(''); -} \ No newline at end of file diff --git a/packages/compiler/src/render3/view/i18n/util.ts b/packages/compiler/src/render3/view/i18n/util.ts index 970c40be6d..8a2e3cc4c0 100644 --- a/packages/compiler/src/render3/view/i18n/util.ts +++ b/packages/compiler/src/render3/view/i18n/util.ts @@ -5,14 +5,10 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import * as i18n from '../../../i18n/i18n_ast'; import {toPublicName} from '../../../i18n/serializers/xmb'; import * as html from '../../../ml_parser/ast'; -import {mapLiteral} from '../../../output/map_util'; import * as o from '../../../output/output_ast'; -import {Identifiers as R3} from '../../r3_identifiers'; - /* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */ const CLOSURE_TRANSLATION_PREFIX = 'MSG_'; @@ -20,16 +16,6 @@ const CLOSURE_TRANSLATION_PREFIX = 'MSG_'; /* Prefix for non-`goog.getMsg` i18n-related vars */ export const TRANSLATION_PREFIX = 'I18N_'; -/** Closure uses `goog.getMsg(message)` to lookup translations */ -const GOOG_GET_MSG = 'goog.getMsg'; - -/** Name of the global variable that is used to determine if we use Closure translations or not */ -const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode'; - -/** I18n separators for metadata **/ -const I18N_MEANING_SEPARATOR = '|'; -const I18N_ID_SEPARATOR = '@@'; - /** Name of the i18n attributes **/ export const I18N_ATTR = 'i18n'; export const I18N_ATTR_PREFIX = 'i18n-'; @@ -43,55 +29,6 @@ export const I18N_ICU_MAPPING_PREFIX = 'I18N_EXP_'; /** Placeholder wrapper for i18n expressions **/ export const I18N_PLACEHOLDER_SYMBOL = '�'; -export type I18nMeta = { - id?: string, - description?: string, - meaning?: string -}; - -function i18nTranslationToDeclStmt( - variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta, - params?: {[name: string]: o.Expression}): o.Statement[] { - const statements: o.Statement[] = []; - // var I18N_X; - statements.push( - new o.DeclareVarStmt(variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan)); - - const args = [o.literal(message) as o.Expression]; - if (params && Object.keys(params).length) { - args.push(mapLiteral(params, true)); - } - - // Closure JSDoc comments - const docStatements = i18nMetaToDocStmt(meta); - const thenStatements: o.Statement[] = docStatements ? [docStatements] : []; - const googFnCall = o.variable(GOOG_GET_MSG).callFn(args); - // const MSG_... = goog.getMsg(..); - thenStatements.push(closureVar.set(googFnCall).toConstDecl()); - // I18N_X = MSG_...; - thenStatements.push(new o.ExpressionStatement(variable.set(closureVar))); - const localizeFnCall = o.importExpr(R3.i18nLocalize).callFn(args); - // I18N_X = i18nLocalize(...); - const elseStatements = [new o.ExpressionStatement(variable.set(localizeFnCall))]; - // if(ngI18nClosureMode) { ... } else { ... } - statements.push(o.ifStmt(o.variable(NG_I18N_CLOSURE_MODE), thenStatements, elseStatements)); - - return statements; -} - -// Converts i18n meta information for a message (id, description, meaning) -// to a JsDoc statement formatted as expected by the Closure compiler. -function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null { - const tags: o.JSDocTag[] = []; - if (meta.description) { - tags.push({tagName: o.JSDocTagName.Desc, text: meta.description}); - } - if (meta.meaning) { - tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning}); - } - return tags.length == 0 ? null : new o.JSDocCommentStmt(tags); -} - export function isI18nAttribute(name: string): boolean { return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX); } @@ -108,14 +45,6 @@ export function hasI18nAttrs(element: html.Element): boolean { return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name)); } -export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta { - return { - id: typeof id === 'string' ? id : message.id || '', - meaning: message.meaning || '', - description: message.description || '' - }; -} - export function icuFromI18nMessage(message: i18n.Message) { return message.nodes[0] as i18n.IcuPlaceholder; } @@ -143,8 +72,8 @@ export function getSeqNumberGenerator(startsAt: number = 0): () => number { } export function placeholdersToParams(placeholders: Map): - {[name: string]: o.Expression} { - const params: {[name: string]: o.Expression} = {}; + {[name: string]: o.LiteralExpr} { + const params: {[name: string]: o.LiteralExpr} = {}; placeholders.forEach((values: string[], key: string) => { params[key] = o.literal(values.length > 1 ? `[${values.join('|')}]` : values[0]); }); @@ -175,42 +104,24 @@ export function assembleBoundTextPlaceholders( return placeholders; } -export function findIndex(items: any[], callback: (item: any) => boolean): number { - for (let i = 0; i < items.length; i++) { - if (callback(items[i])) { - return i; - } - } - return -1; -} - /** - * Parses i18n metas like: - * - "@@id", - * - "description[@@id]", - * - "meaning|description[@@id]" - * and returns an object with parsed output. + * Format the placeholder names in a map of placeholders to expressions. * - * @param meta String that represents i18n meta - * @returns Object with id, meaning and description fields + * The placeholder names are converted from "internal" format (e.g. `START_TAG_DIV_1`) to "external" + * format (e.g. `startTagDiv_1`). + * + * @param params A map of placeholder names to expressions. + * @param useCamelCase whether to camelCase the placeholder name when formatting. + * @returns A new map of formatted placeholder names to expressions. */ -export function parseI18nMeta(meta?: string): I18nMeta { - let id: string|undefined; - let meaning: string|undefined; - let description: string|undefined; - - if (meta) { - const idIndex = meta.indexOf(I18N_ID_SEPARATOR); - const descIndex = meta.indexOf(I18N_MEANING_SEPARATOR); - let meaningAndDesc: string; - [meaningAndDesc, id] = - (idIndex > -1) ? [meta.slice(0, idIndex), meta.slice(idIndex + 2)] : [meta, '']; - [meaning, description] = (descIndex > -1) ? - [meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] : - ['', meaningAndDesc]; +export function i18nFormatPlaceholderNames( + params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) { + const _params: {[key: string]: o.Expression} = {}; + if (params && Object.keys(params).length) { + Object.keys(params).forEach( + key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]); } - - return {id, meaning, description}; + return _params; } /** @@ -254,27 +165,10 @@ export function getTranslationConstPrefix(extra: string): string { } /** - * Generates translation declaration statements. - * - * @param variable Translation value reference - * @param closureVar Variable for Closure `goog.getMsg` calls - * @param message Text message to be translated - * @param meta Object that contains meta information (id, meaning and description) - * @param params Object with placeholders key-value pairs - * @param transformFn Optional transformation (post processing) function reference - * @returns Array of Statements that represent a given translation + * Generate AST to declare a variable. E.g. `var I18N_1;`. + * @param variable the name of the variable to declare. */ -export function getTranslationDeclStmts( - variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta, - params: {[name: string]: o.Expression} = {}, - transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] { - const statements: o.Statement[] = []; - - statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params)); - - if (transformFn) { - statements.push(new o.ExpressionStatement(variable.set(transformFn(variable)))); - } - - return statements; +export function declareI18nVariable(variable: o.ReadVarExpr): o.Statement { + return new o.DeclareVarStmt( + variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan); } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 7d6d28e384..cf00bc8269 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -33,9 +33,10 @@ import {htmlAstToRender3Ast} from '../r3_template_transform'; import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util'; import {I18nContext} from './i18n/context'; +import {createGoogleGetMsgStatements} from './i18n/get_msg_utils'; +import {createLocalizeStatements} from './i18n/localize_utils'; import {I18nMetaVisitor} from './i18n/meta'; -import {getSerializedI18nContent} from './i18n/serializer'; -import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; +import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; import {StylingBuilder, StylingInstruction} from './styling_builder'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, getInterpolationArgsLength, invalid, trimTrailingNulls, unsupported} from './util'; @@ -187,27 +188,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } - registerContextVariables(variable: t.Variable) { - const scopedName = this._bindingScope.freshReferenceName(); - const retrievalLevel = this.level; - const lhs = o.variable(variable.name + scopedName); - this._bindingScope.set( - retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT, - (scope: BindingScope, relativeLevel: number) => { - let rhs: o.Expression; - if (scope.bindingLevel === retrievalLevel) { - // e.g. ctx - rhs = o.variable(CONTEXT_NAME); - } else { - const sharedCtxVar = scope.getSharedContextName(retrievalLevel); - // e.g. ctx_r0 OR x(2); - rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel); - } - // e.g. const $item$ = x(2).$implicit; - return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()]; - }); - } - buildTemplateFunction( nodes: t.Node[], variables: t.Variable[], ngContentSelectorsOffset: number = 0, i18n?: i18n.AST): o.FunctionExpr { @@ -317,38 +297,47 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // LocalResolver notifyImplicitReceiverUse(): void { this._bindingScope.notifyImplicitReceiverUse(); } - i18nTranslate( + private i18nTranslate( message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr, transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr { const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX)); // Closure Compiler requires const names to start with `MSG_` but disallows any other const to // start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call const closureVar = this.i18nGenerateClosureVar(message.id); - const formattedParams = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ true); - const meta = metaFromI18nMessage(message); - const content = getSerializedI18nContent(message); - const statements = - getTranslationDeclStmts(_ref, closureVar, content, meta, formattedParams, transformFn); + const statements = getTranslationDeclStmts(message, _ref, closureVar, params, transformFn); this.constantPool.statements.push(...statements); return _ref; } - i18nFormatPlaceholderNames(params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) { - const _params: {[key: string]: o.Expression} = {}; - if (params && Object.keys(params).length) { - Object.keys(params).forEach( - key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]); - } - return _params; + private registerContextVariables(variable: t.Variable) { + const scopedName = this._bindingScope.freshReferenceName(); + const retrievalLevel = this.level; + const lhs = o.variable(variable.name + scopedName); + this._bindingScope.set( + retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT, + (scope: BindingScope, relativeLevel: number) => { + let rhs: o.Expression; + if (scope.bindingLevel === retrievalLevel) { + // e.g. ctx + rhs = o.variable(CONTEXT_NAME); + } else { + const sharedCtxVar = scope.getSharedContextName(retrievalLevel); + // e.g. ctx_r0 OR x(2); + rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel); + } + // e.g. const $item$ = x(2).$implicit; + return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()]; + }); } - i18nAppendBindings(expressions: AST[]) { + private i18nAppendBindings(expressions: AST[]) { if (expressions.length > 0) { expressions.forEach(expression => this.i18n !.appendBinding(expression)); } } - i18nBindProps(props: {[key: string]: t.Text | t.BoundText}): {[key: string]: o.Expression} { + private i18nBindProps(props: {[key: string]: t.Text | t.BoundText}): + {[key: string]: o.Expression} { const bound: {[key: string]: o.Expression} = {}; Object.keys(props).forEach(key => { const prop = props[key]; @@ -369,7 +358,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return bound; } - i18nGenerateClosureVar(messageId: string): o.ReadVarExpr { + private i18nGenerateClosureVar(messageId: string): o.ReadVarExpr { let name: string; const suffix = this.fileBasedI18nSuffix.toUpperCase(); if (this.i18nUseExternalIds) { @@ -383,7 +372,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return o.variable(name); } - i18nUpdateRef(context: I18nContext): void { + private i18nUpdateRef(context: I18nContext): void { const {icus, meta, isRoot, isResolved, isEmitted} = context; if (isRoot && isResolved && !isEmitted && !isSingleI18nIcu(meta)) { context.isEmitted = true; @@ -428,7 +417,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - i18nStart(span: ParseSourceSpan|null = null, meta: i18n.AST, selfClosing?: boolean): void { + private i18nStart(span: ParseSourceSpan|null = null, meta: i18n.AST, selfClosing?: boolean): + void { const index = this.allocateDataSlot(); if (this.i18nContext) { this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta); @@ -448,7 +438,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.creationInstruction(span, selfClosing ? R3.i18n : R3.i18nStart, params); } - i18nEnd(span: ParseSourceSpan|null = null, selfClosing?: boolean): void { + private i18nEnd(span: ParseSourceSpan|null = null, selfClosing?: boolean): void { if (!this.i18n) { throw new Error('i18nEnd is executed with no i18n context present'); } @@ -476,6 +466,34 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.i18n = null; // reset local i18n context } + private getNamespaceInstruction(namespaceKey: string|null) { + switch (namespaceKey) { + case 'math': + return R3.namespaceMathML; + case 'svg': + return R3.namespaceSVG; + default: + return R3.namespaceHTML; + } + } + + private addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) { + this._namespace = nsInstruction; + this.creationInstruction(element.sourceSpan, nsInstruction); + } + + /** + * Adds an update instruction for an interpolated property or attribute, such as + * `prop="{{value}}"` or `attr.title="{{value}}"` + */ + private interpolatedUpdateInstruction( + instruction: o.ExternalReference, elementIndex: number, attrName: string, + input: t.BoundAttribute, value: any, params: any[]) { + this.updateInstruction( + elementIndex, input.sourceSpan, instruction, + () => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]); + } + visitContent(ngContent: t.Content) { const slot = this.allocateDataSlot(); const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; @@ -505,23 +523,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - - getNamespaceInstruction(namespaceKey: string|null) { - switch (namespaceKey) { - case 'math': - return R3.namespaceMathML; - case 'svg': - return R3.namespaceSVG; - default: - return R3.namespaceHTML; - } - } - - addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) { - this._namespace = nsInstruction; - this.creationInstruction(element.sourceSpan, nsInstruction); - } - visitElement(element: t.Element) { const elementIndex = this.allocateDataSlot(); const stylingBuilder = new StylingBuilder(o.literal(elementIndex), null); @@ -844,17 +845,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - /** - * Adds an update instruction for an interpolated property or attribute, such as - * `prop="{{value}}"` or `attr.title="{{value}}"` - */ - interpolatedUpdateInstruction( - instruction: o.ExternalReference, elementIndex: number, attrName: string, - input: t.BoundAttribute, value: any, params: any[]) { - this.updateInstruction( - elementIndex, input.sourceSpan, instruction, - () => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]); - } visitTemplate(template: t.Template) { const NG_TEMPLATE_TAG_NAME = 'ng-template'; @@ -1007,7 +997,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // - all ICU vars (such as `VAR_SELECT` or `VAR_PLURAL`) are replaced with correct values const transformFn = (raw: o.ReadVarExpr) => { const params = {...vars, ...placeholders}; - const formatted = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ false); + const formatted = i18nFormatPlaceholderNames(params, /* useCamelCase */ false); return instruction(null, R3.i18nPostprocess, [raw, mapLiteral(formatted, true)]); }; @@ -2004,3 +1994,52 @@ interface ChainableBindingInstruction { value: () => o.Expression; params?: any[]; } + +/** Name of the global variable that is used to determine if we use Closure translations or not */ +const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode'; + +/** + * Generate statements that define a given translation message. + * + * ``` + * var I18N_1; + * if (ngI18nClosureMode) { + * var MSG_EXTERNAL_XXX = goog.getMsg( + * "Some message with {$interpolation}!", + * { "interpolation": "\uFFFD0\uFFFD" } + * ); + * I18N_1 = MSG_EXTERNAL_XXX; + * } + * else { + * I18N_1 = $localize`Some message with ${'\uFFFD0\uFFFD'}!`; + * } + * ``` + * + * @param message The original i18n AST message node + * @param variable The variable that will be assigned the translation, e.g. `I18N_1`. + * @param closureVar The variable for Closure `goog.getMsg` calls, e.g. `MSG_EXTERNAL_XXX`. + * @param params Object mapping placeholder names to their values (e.g. + * `{ "interpolation": "\uFFFD0\uFFFD" }`). + * @param transformFn Optional transformation function that will be applied to the translation (e.g. + * post-processing). + * @returns An array of statements that defined a given translation. + */ +export function getTranslationDeclStmts( + message: i18n.Message, variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, + params: {[name: string]: o.Expression} = {}, + transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] { + const formattedParams = i18nFormatPlaceholderNames(params, /* useCamelCase */ true); + const statements: o.Statement[] = [ + declareI18nVariable(variable), + o.ifStmt( + o.variable(NG_I18N_CLOSURE_MODE), + createGoogleGetMsgStatements(variable, message, closureVar, formattedParams), + createLocalizeStatements(variable, message, formattedParams)), + ]; + + if (transformFn) { + statements.push(new o.ExpressionStatement(variable.set(transformFn(variable)))); + } + + return statements; +} \ No newline at end of file diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts index 1ce50ed7d0..2eb1a3dd19 100644 --- a/packages/compiler/test/render3/view/i18n_spec.ts +++ b/packages/compiler/test/render3/view/i18n_spec.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {I18nMeta, parseI18nMeta} from '@angular/compiler/src/render3/view/i18n/meta'; import {AST} from '../../../src/expression_parser/ast'; import {Lexer} from '../../../src/expression_parser/lexer'; @@ -13,8 +14,10 @@ import * as i18n from '../../../src/i18n/i18n_ast'; import * as o from '../../../src/output/output_ast'; import * as t from '../../../src/render3/r3_ast'; import {I18nContext} from '../../../src/render3/view/i18n/context'; -import {getSerializedI18nContent} from '../../../src/render3/view/i18n/serializer'; -import {I18nMeta, formatI18nPlaceholderName, parseI18nMeta} from '../../../src/render3/view/i18n/util'; +import {serializeI18nMessageForGetMsg} from '../../../src/render3/view/i18n/get_msg_utils'; +import {serializeIcuNode} from '../../../src/render3/view/i18n/icu_serializer'; +import {serializeI18nMessageForLocalize} from '../../../src/render3/view/i18n/localize_utils'; +import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util'; import {parseR3 as parse} from './util'; @@ -214,45 +217,162 @@ describe('Utils', () => { }); }); -describe('Serializer', () => { +describe('serializeI18nMessageForGetMsg', () => { const serialize = (input: string): string => { const tree = parse(`
${input}
`); const root = tree.nodes[0] as t.Element; - return getSerializedI18nContent(root.i18n as i18n.Message); + return serializeI18nMessageForGetMsg(root.i18n as i18n.Message); }; - it('should produce output for i18n content', () => { - const cases = [ - // plain text - ['Some text', 'Some text'], - // text with interpolation - [ - 'Some text {{ valueA }} and {{ valueB + valueC }}', - 'Some text {$interpolation} and {$interpolation_1}' - ], + it('should serialize plain text for `GetMsg()`', + () => { expect(serialize('Some text')).toEqual('Some text'); }); - // content with HTML tags - [ - 'A B
C
D', - 'A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D' - ], + it('should serialize text with interpolation for `GetMsg()`', () => { + expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')) + .toEqual('Some text {$interpolation} and {$interpolation_1}'); + }); - // simple ICU - ['{age, plural, 10 {ten} other {other}}', '{VAR_PLURAL, plural, 10 {ten} other {other}}'], + it('should serialize content with HTML tags for `GetMsg()`', () => { + expect(serialize('A B
C
D')) + .toEqual('A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D'); + }); - // nested ICUs - [ - '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}', - '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}' - ], + it('should serialize simple ICU for `GetMsg()`', () => { + expect(serialize('{age, plural, 10 {ten} other {other}}')) + .toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}'); + }); - // ICU with nested HTML - [ - '{age, plural, 10 {ten} other {
other
}}', - '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}' - ] - ]; + it('should serialize nested ICUs for `GetMsg()`', () => { + expect(serialize( + '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) + .toEqual( + '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'); + }); - cases.forEach(([input, output]) => { expect(serialize(input)).toEqual(output); }); + it('should serialize ICU with nested HTML for `GetMsg()`', () => { + expect(serialize('{age, plural, 10 {ten} other {
other
}}')) + .toEqual( + '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'); + }); + + it('should serialize ICU with nested HTML containing further ICUs for `GetMsg()`', () => { + expect( + serialize( + '{gender, select, male {male} female {female} other {other}}
{gender, select, male {male} female {female} other {other}}
')) + .toEqual('{$icu}{$startTagDiv}{$icu}{$closeTagDiv}'); + }); +}); + +describe('serializeI18nMessageForLocalize', () => { + const serialize = (input: string) => { + const tree = parse(`
${input}
`); + const root = tree.nodes[0] as t.Element; + return serializeI18nMessageForLocalize(root.i18n as i18n.Message); + }; + + it('should serialize plain text for `$localize()`', () => { + expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []}); + }); + + it('should serialize text with interpolation for `$localize()`', () => { + expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }} done')).toEqual({ + messageParts: ['Some text ', ' and ', ' done'], + placeHolders: ['interpolation', 'interpolation_1'] + }); + }); + + it('should serialize text with interpolation at start for `$localize()`', () => { + expect(serialize('{{ valueA }} and {{ valueB + valueC }} done')).toEqual({ + messageParts: ['', ' and ', ' done'], + placeHolders: ['interpolation', 'interpolation_1'] + }); + }); + + + it('should serialize text with interpolation at end for `$localize()`', () => { + expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')).toEqual({ + messageParts: ['Some text ', ' and ', ''], + placeHolders: ['interpolation', 'interpolation_1'] + }); + }); + + + it('should serialize only interpolation for `$localize()`', () => { + expect(serialize('{{ valueB + valueC }}')) + .toEqual({messageParts: ['', ''], placeHolders: ['interpolation']}); + }); + + + it('should serialize content with HTML tags for `$localize()`', () => { + expect(serialize('A B
C
D')).toEqual({ + messageParts: ['A ', 'B', 'C', '', ' D'], + placeHolders: ['startTagSpan', 'startTagDiv', 'closeTagDiv', 'closeTagSpan'] + }); + }); + + + it('should serialize simple ICU for `$localize()`', () => { + expect(serialize('{age, plural, 10 {ten} other {other}}')).toEqual({ + messageParts: ['{VAR_PLURAL, plural, 10 {ten} other {other}}'], + placeHolders: [] + }); + }); + + + it('should serialize nested ICUs for `$localize()`', () => { + expect(serialize( + '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) + .toEqual({ + messageParts: [ + '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}' + ], + placeHolders: [] + }); + }); + + + it('should serialize ICU with nested HTML for `$localize()`', () => { + expect(serialize('{age, plural, 10 {ten} other {
other
}}')).toEqual({ + messageParts: [ + '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}' + ], + placeHolders: [] + }); + }); + + it('should serialize ICU with nested HTML containing further ICUs for `$localize()`', () => { + expect( + serialize( + '{gender, select, male {male} female {female} other {other}}
{gender, select, male {male} female {female} other {other}}
')) + .toEqual({ + messageParts: ['', '', '', '', ''], + placeHolders: ['icu', 'startTagDiv', 'icu', 'closeTagDiv'] + }); + }); +}); + +describe('serializeIcuNode', () => { + const serialize = (input: string) => { + const tree = parse(`
${input}
`); + const rooti18n = (tree.nodes[0] as t.Element).i18n as i18n.Message; + return serializeIcuNode(rooti18n.nodes[0] as i18n.Icu); + }; + + it('should serialize a simple ICU', () => { + expect(serialize('{age, plural, 10 {ten} other {other}}')) + .toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}'); + }); + + it('should serialize a next ICU', () => { + expect(serialize( + '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) + .toEqual( + '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'); + }); + + it('should serialize ICU with nested HTML', () => { + expect(serialize('{age, plural, 10 {ten} other {
other
}}')) + .toEqual( + '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'); }); }); diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index c87a875d83..598d681e4b 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -37,3 +37,14 @@ export * from './core_render3_private_export'; export {SecurityContext} from './sanitization/security'; export {Sanitizer} from './sanitization/sanitizer'; export * from './codegen_private_exports'; + +import {global} from './util/global'; +if (ngDevMode) { + // This helper is to give a reasonable error message to people upgrading to v9 that have not yet + // installed `@angular/localize` in their app. + // tslint:disable-next-line: no-toplevel-property-access + global.$localize = global.$localize || function() { + throw new Error( + 'The global function `$localize` is missing. Please add `import \'@angular/localize\';` to your polyfills.ts file.'); + }; +} diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index b97cf55650..baea1b2d36 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -5,7 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import '../util/ng_i18n_closure_mode'; import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization'; import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; @@ -13,6 +12,7 @@ import {InertBodyHelper} from '../sanitization/inert_body'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {addAllToArray} from '../util/array_utils'; import {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; +import {global} from '../util/global'; import {attachPatchData} from './context_discovery'; import {bind, setDelayProjection} from './instructions/all'; import {attachI18nOpCodesDebug} from './instructions/lview_debug'; @@ -1317,40 +1317,86 @@ function replaceNgsp(value: string): string { return value.replace(NGSP_UNICODE_REGEXP, ' '); } -let TRANSLATIONS: {[key: string]: string} = {}; export interface I18nLocalizeOptions { translations: {[key: string]: string}; } /** - * Set the configuration for `i18nLocalize`. + * Provide translations for `$localize`. * * @deprecated this method is temporary & should not be used as it will be removed soon */ export function i18nConfigureLocalize(options: I18nLocalizeOptions = { translations: {} }) { - TRANSLATIONS = options.translations; -} + type TranslationInfo = {messageParts: TemplateStringsArray, placeholderNames: string[]}; + type MessageInfo = {translationKey: string, replacements: {[placeholderName: string]: any}}; + const PLACEHOLDER_MARKER = ':'; + const TRANSLATIONS: {[key: string]: TranslationInfo} = {}; -const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g; + Object.keys(options.translations).forEach(key => { + TRANSLATIONS[key] = splitMessage(options.translations[key]); + }); -/** - * A goog.getMsg-like function for users that do not use Closure. - * - * This method is required as a *temporary* measure to prevent i18n tests from being blocked while - * running outside of Closure Compiler. This method will not be needed once runtime translation - * service support is introduced. - * - * @codeGenApi - * @deprecated this method is temporary & should not be used as it will be removed soon - */ -export function ɵɵi18nLocalize(input: string, placeholders?: {[key: string]: string}) { - if (typeof TRANSLATIONS[input] !== 'undefined') { // to account for empty string - input = TRANSLATIONS[input]; + if (ngDevMode) { + if (global.$localize === undefined) { + throw new Error( + 'The global function `$localize` is missing. Please add `import \'@angular/localize\';` to your polyfills.ts file.'); + } } - if (placeholders !== undefined && Object.keys(placeholders).length) { - return input.replace(LOCALIZE_PH_REGEXP, (_, key) => placeholders[key] || ''); + $localize.translate = function(messageParts: TemplateStringsArray, expressions: readonly any[]): + [TemplateStringsArray, readonly any[]] { + const message = parseMessage(messageParts, expressions); + const translation = TRANSLATIONS[message.translationKey]; + const result: [TemplateStringsArray, readonly any[]] = + (translation === undefined ? [messageParts, expressions] : [ + translation.messageParts, + translation.placeholderNames.map(placeholder => message.replacements[placeholder]) + ]); + return result; + }; + + function splitMessage(message: string): TranslationInfo { + const parts = message.split(/{\$([^}]*)}/); + const messageParts = [parts[0]]; + const placeholderNames: string[] = []; + for (let i = 1; i < parts.length - 1; i += 2) { + placeholderNames.push(parts[i]); + messageParts.push(parts[i + 1]); + } + const rawMessageParts = + messageParts.map(part => part.charAt(0) === PLACEHOLDER_MARKER ? '\\' + part : part); + return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames}; + } + + function parseMessage( + messageParts: TemplateStringsArray, expressions: readonly any[]): MessageInfo { + const PLACEHOLDER_NAME_MARKER = ':'; + const replacements: {[placeholderName: string]: any} = {}; + let translationKey = messageParts[0]; + for (let i = 1; i < messageParts.length; i++) { + const messagePart = messageParts[i]; + const expression = expressions[i - 1]; + // There is a problem with synthesized template literals in TS where the raw version + // cannot be found, since there is no original source code to read it from. + // In that case we just fall back on the non-raw version. + // This should be OK because synthesized nodes (from the template compiler) will always have + // placeholder names provided. + if ((messageParts.raw[i] || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER) { + const endOfPlaceholderName = messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1); + const placeholderName = messagePart.substring(1, endOfPlaceholderName); + translationKey += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`; + replacements[placeholderName] = expression; + } else { + translationKey += messagePart; + replacements[`ph_${i}`] = expression; + } + } + return {translationKey, replacements}; + } + + function makeTemplateObject(cooked: string[], raw: string[]): TemplateStringsArray { + Object.defineProperty(cooked, 'raw', {value: raw}); + return cooked as any; } - return input; } /** diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 169a897007..d1752af3bb 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -149,7 +149,6 @@ export { ɵɵi18nApply, ɵɵi18nPostprocess, i18nConfigureLocalize, - ɵɵi18nLocalize, getLocaleId, setLocaleId, } from './i18n'; diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index 6356ef5d41..52f0a455c1 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -27,6 +27,7 @@ ts_library( "//packages/core/src/reflection", "//packages/core/src/util", "//packages/core/testing", + "//packages/localize", "//packages/platform-browser", "//packages/platform-browser-dynamic", "//packages/platform-browser/animations", diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index e7044a2a94..e0f5335bfc 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - +import '@angular/localize'; import {registerLocaleData} from '@angular/common'; import localeRo from '@angular/common/locales/ro'; import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize, Pipe, PipeTransform} from '@angular/core'; @@ -1130,6 +1130,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { TestBed.configureTestingModule({declarations: [ClsDir, MyApp]}); ɵi18nConfigureLocalize({ translations: { + // Not that this translation switches the order of the expressions! 'start {$interpolation} middle {$interpolation_1} end': 'début {$interpolation_1} milieu {$interpolation} fin', '{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} emails}}': diff --git a/packages/core/test/bundling/todo_i18n/BUILD.bazel b/packages/core/test/bundling/todo_i18n/BUILD.bazel index 11da031ee1..a45ef64f2f 100644 --- a/packages/core/test/bundling/todo_i18n/BUILD.bazel +++ b/packages/core/test/bundling/todo_i18n/BUILD.bazel @@ -17,6 +17,7 @@ ng_module( "//packages/common", "//packages/core", "//packages/core/test/bundling/util:reflect_metadata", + "//packages/localize", ], ) @@ -47,6 +48,7 @@ ts_library( "//packages/compiler", "//packages/core", "//packages/core/testing", + "//packages/localize", "//packages/private/testing", ], ) diff --git a/packages/core/test/bundling/todo_i18n/index.ts b/packages/core/test/bundling/todo_i18n/index.ts index 49337f9395..627e14961d 100644 --- a/packages/core/test/bundling/todo_i18n/index.ts +++ b/packages/core/test/bundling/todo_i18n/index.ts @@ -5,8 +5,10 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import '@angular/core/test/bundling/util/src/reflect_metadata'; +// Make the `$localize()` global function available to the compiled templates, and the direct calls +// below. This would normally be done inside the application `polyfills.ts` file. +import '@angular/localize'; /** * TODO(ocombe): replace this with the real runtime i18n service configuration * For now we define inline translations that are added with the function `ɵi18nConfigureLocalize`, @@ -16,31 +18,26 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; */ import './translations'; import {CommonModule} from '@angular/common'; -import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent, ɵɵi18nLocalize as localize} from '@angular/core'; +import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; class Todo { editing: boolean; - // TODO(issue/24571): remove '!'. - private _title !: string; get title() { return this._title; } set title(value: string) { this._title = value.trim(); } - constructor(title: string, public completed: boolean = false) { - this.editing = false; - this.title = title; - } + constructor(private _title: string, public completed: boolean = false) { this.editing = false; } } @Injectable({providedIn: 'root'}) class TodoStore { todos: Array = [ - new Todo(localize('Demonstrate Components')), - new Todo(localize('Demonstrate Structural Directives'), true), + new Todo($localize `Demonstrate Components`), + new Todo($localize `Demonstrate Structural Directives`, true), // Using a placeholder - new Todo(localize('Demonstrate {$value}', {value: 'NgModules'})), - new Todo(localize('Demonstrate zoneless change detection')), - new Todo(localize('Demonstrate internationalization')), + new Todo($localize `Demonstrate ${'NgModules'}:value:`), + new Todo($localize `Demonstrate zoneless change detection`), + new Todo($localize `Demonstrate internationalization`), ]; private getWithCompleted(completed: boolean) { diff --git a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts index 81d97c21ce..96adcdc609 100644 --- a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts @@ -19,6 +19,12 @@ describe('functional test for todo i18n', () => { BUNDLES.forEach(bundle => { describe(bundle, () => { it('should render todo i18n', withBody('', async() => { + // We need to delete the dummy `$localize` that was added because of the import of + // `@angular/core` at the top of this file. + // Also to clear out the translations from the previous test. + // This would not be needed in normal applications since the import of + // `@angular/localize` would be in polyfill.ts before any other import. + ($localize as any) = undefined; require(path.join(PACKAGE, bundle)); const toDoAppComponent = getComponent(document.querySelector('todo-app') !); expect(document.body.textContent).toContain('liste de tâches'); diff --git a/packages/core/test/linker/ng_container_integration_spec.ts b/packages/core/test/linker/ng_container_integration_spec.ts index 5339a4e0f0..70fbdb91c3 100644 --- a/packages/core/test/linker/ng_container_integration_spec.ts +++ b/packages/core/test/linker/ng_container_integration_spec.ts @@ -5,8 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - - +import '@angular/localize'; import {AfterContentInit, AfterViewInit, Component, ContentChildren, Directive, Input, QueryList, ViewChildren, ɵivyEnabled as ivyEnabled} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {isCommentNode} from '@angular/platform-browser/testing/src/browser_util'; diff --git a/packages/core/testing/BUILD.bazel b/packages/core/testing/BUILD.bazel index 8009d9f276..2b1dde511c 100644 --- a/packages/core/testing/BUILD.bazel +++ b/packages/core/testing/BUILD.bazel @@ -13,6 +13,7 @@ ng_module( "//packages:types", "//packages/compiler", "//packages/core", + "//packages/localize", "@npm//@types/jasmine", "@npm//zone.js", ], diff --git a/scripts/ci/run_angular_material_unit_tests.sh b/scripts/ci/run_angular_material_unit_tests.sh index 6f43e22b0f..c1ee7b81a6 100755 --- a/scripts/ci/run_angular_material_unit_tests.sh +++ b/scripts/ci/run_angular_material_unit_tests.sh @@ -25,6 +25,9 @@ node ${angular_dir}/scripts/ci/update-deps-to-dist-packages.js ${MATERIAL_REPO_T # repository automatically picks up the blocklist and disables the specified tests. cp ${angular_dir}/tools/material-ci/test-blocklist.ts ${MATERIAL_REPO_TMP_DIR}/test/ +# Ensure that the `@angular/localize` package is there. (It wasn't before v9.) +yarn --cwd ${MATERIAL_REPO_TMP_DIR} add ${angular_dir}/dist/packages-dist-ivy-aot/localize + # Create a symlink for the Bazel binary installed through NPM, as running through Yarn introduces OOM errors. ./scripts/circleci/setup_bazel_binary.sh diff --git a/test-main.js b/test-main.js index 6e8f44e400..1fea998c6a 100644 --- a/test-main.js +++ b/test-main.js @@ -51,6 +51,8 @@ System.config({ '@angular/router': {main: 'index.js', defaultExtension: 'js'}, '@angular/http/testing': {main: 'index.js', defaultExtension: 'js'}, '@angular/http': {main: 'index.js', defaultExtension: 'js'}, + '@angular/localize/run_time': {main: 'index.js', defaultExtension: 'js'}, + '@angular/localize': {main: 'index.js', defaultExtension: 'js'}, '@angular/upgrade/static/testing': {main: 'index.js', defaultExtension: 'js'}, '@angular/upgrade/static': {main: 'index.js', defaultExtension: 'js'}, '@angular/upgrade': {main: 'index.js', defaultExtension: 'js'},