Skip to content

Commit fa79f51

Browse files
petebacondarwinmhevery
authored andcommitted
refactor(ivy): update the compiler to emit $localize tags (angular#31609)
This commit changes the Angular compiler (ivy-only) to generate `$localize` tagged strings for component templates that use `i18n` attributes. BREAKING CHANGE Since `$localize` is a global function, it must be included in any applications that use i18n. This is achieved by importing the `@angular/localize` package into an appropriate bundle, where it will be executed before the renderer needs to call `$localize`. For CLI based projects, this is best done in the `polyfills.ts` file. ```ts import '@angular/localize'; ``` For non-CLI applications this could be added as a script to the index.html file or another suitable script file. PR Close angular#31609
1 parent b21397b commit fa79f51

File tree

35 files changed

+1172
-582
lines changed

35 files changed

+1172
-582
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime": 1440,
24-
"main": 125448,
24+
"main": 125882,
2525
"polyfills": 45340
2626
}
2727
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
import "rxjs";
22

33
import "rxjs/operators";
4+
5+
const __globalThis = "undefined" !== typeof globalThis && globalThis;
6+
7+
const __window = "undefined" !== typeof window && window;
8+
9+
const __self = "undefined" !== typeof self && "undefined" !== typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && self;
10+
11+
const __global = "undefined" !== typeof global && global;
12+
13+
const _global = __globalThis || __global || __window || __self;
14+
15+
if (ngDevMode) _global.$localize = _global.$localize || function() {
16+
throw new Error("The global function `$localize` is missing. Please add `import '@angular/localize';` to your polyfills.ts file.");
17+
};

integration/side-effects/snapshots/core/esm5.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,17 @@ import "tslib";
33
import "rxjs";
44

55
import "rxjs/operators";
6+
7+
var __globalThis = "undefined" !== typeof globalThis && globalThis;
8+
9+
var __window = "undefined" !== typeof window && window;
10+
11+
var __self = "undefined" !== typeof self && "undefined" !== typeof WorkerGlobalScope && self instanceof WorkerGlobalScope && self;
12+
13+
var __global = "undefined" !== typeof global && global;
14+
15+
var _global = __globalThis || __global || __window || __self;
16+
17+
if (ngDevMode) _global.$localize = _global.$localize || function() {
18+
throw new Error("The global function `$localize` is missing. Please add `import '@angular/localize';` to your polyfills.ts file.");
19+
};

karma-js.conf.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ module.exports = function(config) {
8282
'dist/all/@angular/elements/schematics/**',
8383
'dist/all/@angular/examples/**/e2e_test/*',
8484
'dist/all/@angular/language-service/**',
85+
'dist/all/@angular/localize/**/test/**',
8586
'dist/all/@angular/router/**/test/**',
8687
'dist/all/@angular/platform-browser/testing/e2e_util.js',
8788
'dist/all/angular1_router.js',

modules/benchmarks/src/expanding_rows/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ng_module(
1414
"//packages:types",
1515
"//packages/common",
1616
"//packages/core",
17+
"//packages/localize",
1718
"//packages/platform-browser",
1819
"@npm//rxjs",
1920
],

modules/benchmarks/src/expanding_rows/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
// This benchmark uses i18n in its `ExpandingRowSummary` component so `$localize` must be loaded.
9+
import '@angular/localize';
810
import {enableProdMode} from '@angular/core';
911
import {platformBrowser} from '@angular/platform-browser';
1012

packages/compiler-cli/src/ngtsc/translator/src/translator.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
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';
10+
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
1011
import * as ts from 'typescript';
1112

1213
import {DefaultImportRecorder, ImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
@@ -249,6 +250,10 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
249250
return expr;
250251
}
251252

253+
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
254+
return visitLocalizedString(ast, context, this);
255+
}
256+
252257
visitExternalExpr(ast: ExternalExpr, context: Context): ts.PropertyAccessExpression
253258
|ts.Identifier {
254259
if (ast.value.moduleName === null || ast.value.name === null) {
@@ -435,6 +440,10 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
435440
return ts.createLiteral(ast.value as string);
436441
}
437442

443+
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
444+
return visitLocalizedString(ast, context, this);
445+
}
446+
438447
visitExternalExpr(ast: ExternalExpr, context: Context): ts.TypeNode {
439448
if (ast.value.moduleName === null || ast.value.name === null) {
440449
throw new Error(`Import unknown module or symbol`);
@@ -512,3 +521,44 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
512521
return ts.createTypeQueryNode(expr as ts.Identifier);
513522
}
514523
}
524+
525+
/**
526+
* A helper to reduce duplication, since this functionality is required in both
527+
* `ExpressionTranslatorVisitor` and `TypeTranslatorVisitor`.
528+
*/
529+
function visitLocalizedString(ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
530+
let template: ts.TemplateLiteral;
531+
if (ast.messageParts.length === 1) {
532+
template = ts.createNoSubstitutionTemplateLiteral(ast.messageParts[0]);
533+
} else {
534+
const head = ts.createTemplateHead(ast.messageParts[0]);
535+
const spans: ts.TemplateSpan[] = [];
536+
for (let i = 1; i < ast.messageParts.length; i++) {
537+
const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context);
538+
spans.push(ts.createTemplateSpan(
539+
resolvedExpression, ts.createTemplateMiddle(prefixWithPlaceholderMarker(
540+
ast.messageParts[i], ast.placeHolderNames[i - 1]))));
541+
}
542+
if (spans.length > 0) {
543+
// The last span is supposed to have a tail rather than a middle
544+
spans[spans.length - 1].literal.kind = ts.SyntaxKind.TemplateTail;
545+
}
546+
template = ts.createTemplateExpression(head, spans);
547+
}
548+
return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
549+
}
550+
551+
/**
552+
* We want our tagged literals to include placeholder name information to aid runtime translation.
553+
*
554+
* The expressions are marked with placeholder names by postfixing the expression with
555+
* `:placeHolderName:`. To achieve this, we actually "prefix" the message part that follows the
556+
* expression.
557+
*
558+
* @param messagePart the message part that follows the current expression.
559+
* @param placeHolderName the name of the placeholder for the current expression.
560+
* @returns the prefixed message part.
561+
*/
562+
function prefixWithPlaceholderMarker(messagePart: string, placeHolderName: string) {
563+
return `:${placeHolderName}:${messagePart}`;
564+
}

packages/compiler-cli/src/transformers/node_emitter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
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';
10+
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
1011
import * as ts from 'typescript';
1112

1213
import {error} from './util';
@@ -535,6 +536,10 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
535536

536537
visitLiteralExpr(expr: LiteralExpr) { return this.record(expr, createLiteral(expr.value)); }
537538

539+
visitLocalizedString(expr: LocalizedString, context: any) {
540+
throw new Error('localized strings are not supported in pre-ivy mode.');
541+
}
542+
538543
visitExternalExpr(expr: ExternalExpr) {
539544
return this.record(expr, this._visitIdentifier(expr.value));
540545
}

packages/compiler-cli/test/compliance/mock_compile.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import {NgtscProgram} from '../../src/ngtsc/program';
1515
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
1616
const OPERATOR =
1717
/!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./;
18-
const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"|`(\\`[\s\S])*?`/;
18+
const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"/;
19+
const BACKTICK_STRING = /\\`(([\s\S]*?)(\$\{[^}]*?\})?)*?\\`/;
20+
const BACKTICK_INTERPOLATION = /(\$\{[^}]*\})/;
1921
const NUMBER = /\d+/;
2022

2123
const ELLIPSIS = '…';
2224
const TOKEN = new RegExp(
23-
`\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`,
25+
`\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|(${BACKTICK_STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`,
2426
'y');
2527

2628
type Piece = string | RegExp;
@@ -30,6 +32,8 @@ const SKIP = /(?:.|\n|\r)*/;
3032
const ERROR_CONTEXT_WIDTH = 30;
3133
// Transform the expected output to set of tokens
3234
function tokenize(text: string): Piece[] {
35+
// TOKEN.lastIndex is stateful so we cache the `lastIndex` and restore it at the end of the call.
36+
const lastIndex = TOKEN.lastIndex;
3337
TOKEN.lastIndex = 0;
3438

3539
let match: RegExpMatchArray|null;
@@ -42,6 +46,8 @@ function tokenize(text: string): Piece[] {
4246
pieces.push(IDENTIFIER);
4347
} else if (token === ELLIPSIS) {
4448
pieces.push(SKIP);
49+
} else if (match = BACKTICK_STRING.exec(token)) {
50+
pieces.push(...tokenizeBackTickString(token));
4551
} else {
4652
pieces.push(token);
4753
}
@@ -57,10 +63,33 @@ function tokenize(text: string): Piece[] {
5763
`Invalid test, no token found for "${text[tokenizedTextEnd]}" ` +
5864
`(context = '${text.substr(from, to)}...'`);
5965
}
66+
// Reset the lastIndex in case we are in a recursive `tokenize()` call.
67+
TOKEN.lastIndex = lastIndex;
6068

6169
return pieces;
6270
}
6371

72+
/**
73+
* Back-ticks are escaped as "\`" so we must strip the backslashes.
74+
* Also the string will likely contain interpolations and if an interpolation holds an
75+
* identifier we will need to match that later. So tokenize the interpolation too!
76+
*/
77+
function tokenizeBackTickString(str: string): Piece[] {
78+
const pieces: Piece[] = ['`'];
79+
const backTickPieces = str.slice(2, -2).split(BACKTICK_INTERPOLATION);
80+
backTickPieces.forEach((backTickPiece) => {
81+
if (BACKTICK_INTERPOLATION.test(backTickPiece)) {
82+
// An interpolation so tokenize this expression
83+
pieces.push(...tokenize(backTickPiece));
84+
} else {
85+
// Not an interpolation so just add it as a piece
86+
pieces.push(backTickPiece);
87+
}
88+
});
89+
pieces.push('`');
90+
return pieces;
91+
}
92+
6493
export function expectEmit(
6594
source: string, expected: string, description: string,
6695
assertIdentifiers?: {[name: string]: RegExp}) {

0 commit comments

Comments
 (0)