diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json
index fb86788fb6ab87..9773f8a944fd5f 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 5f8109856a868d..47de5e87133b38 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 09cc6db2003f08..d5fd28b2d86ecb 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 b5c6ea800f8a74..7eacd89f86e156 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 b3d6925e74bc8d..08362e9e72642a 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 743f0ee323df3a..f46ffce3187ad7 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 4cef26deac169b..58422df55c47b1 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 66c31a14582fae..1cd37b45e8e9d8 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 47d2c1f171bd98..7fc3fc6b879e3a 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 f0390bf501a678..720e49b14b6877 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 ff88cbca79133b..f5eea86e017595 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 ac26cdba21b615..3d7a65275acc3c 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 82d5addb9102ef..3d4c2030256997 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 9f25155809c20f..6fbde79917f9d6 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 2c770813f30db0..40cd312ae47e1d 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 00000000000000..609bef106bf68a
--- /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 00000000000000..8e5aa0b7ffd669
--- /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 00000000000000..a5ec3db8e61505
--- /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 e798d9a8b98986..e75beed98c7144 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 e56ffef38911ff..00000000000000
--- 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 970c40be6dbd3f..8a2e3cc4c0af6d 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 7d6d28e3842598..cf00bc82693c8d 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 1ce50ed7d04609..2eb1a3dd196ab2 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 BC
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 BC
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}}');
+ });
+
+ 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}}');
+ });
+
+ 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']
+ });
+ });
- // ICU with nested HTML
- [
- '{age, plural, 10 {ten} other {other
}}',
+
+ 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 BC
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}}');
+ });
- cases.forEach(([input, output]) => { expect(serialize(input)).toEqual(output); });
+ 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 c87a875d8329d4..598d681e4b7287 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 b97cf55650da1d..baea1b2d36bced 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.');
+ }
+ }
+ $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};
}
- if (placeholders !== undefined && Object.keys(placeholders).length) {
- return input.replace(LOCALIZE_PH_REGEXP, (_, key) => placeholders[key] || '');
+
+ 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 169a89700776c6..d1752af3bb913e 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 6356ef5d4157ab..52f0a455c13459 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 e7044a2a943326..e0f5335bfca06a 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 11da031ee12839..a45ef64f2fbc47 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 49337f93958cfc..627e14961d1172 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 81d97c21ce3673..96adcdc6092856 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 5339a4e0f0fc4c..70fbdb91c37611 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 8009d9f2768395..2b1dde511cc4d5 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 6f43e22b0fdea1..c1ee7b81a61785 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 6e8f44e40069c2..1fea998c6a4079 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'},