refactor(ivy): update the compiler to emit `$localize` tags (#31609)

This commit changes the Angular compiler (ivy-only) to generate `$localize`
tagged strings for component templates that use `i18n` attributes.

BREAKING CHANGE

Since `$localize` is a global function, it must be included in any applications
that use i18n. This is achieved by importing the `@angular/localize` package
into an appropriate bundle, where it will be executed before the renderer
needs to call `$localize`. For CLI based projects, this is best done in
the `polyfills.ts` file.

```ts
import '@angular/localize';
```

For non-CLI applications this could be added as a script to the index.html
file or another suitable script file.

PR Close #31609
This commit is contained in:
Pete Bacon Darwin 2019-07-30 18:02:17 +01:00 committed by Misko Hevery
parent b21397bde9
commit fa79f51645
35 changed files with 1173 additions and 583 deletions

View File

@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 125448,
"main": 125882,
"polyfills": 45340
}
}

View File

@ -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.");
};

View File

@ -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.");
};

View File

@ -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',

View File

@ -14,6 +14,7 @@ ng_module(
"//packages:types",
"//packages/common",
"//packages/core",
"//packages/localize",
"//packages/platform-browser",
"@npm//rxjs",
],

View File

@ -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';

View File

@ -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}`;
}

View File

@ -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));
}

View File

@ -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}) {

View File

@ -271,6 +271,7 @@ class KeyVisitor implements o.ExpressionVisitor {
visitReadPropExpr = invalid;
visitReadKeyExpr = invalid;
visitCommaExpr = invalid;
visitLocalizedString = invalid;
}
function invalid<T>(this: o.ExpressionVisitor, arg: o.Expression | o.Statement): never {

View File

@ -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 {

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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');

View File

@ -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('');
}

View File

@ -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);
}

View File

@ -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};
}

View File

@ -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);
}

View File

@ -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('');
}

View File

@ -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 = '<27>';
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<string, string[]>):
{[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);
}

View File

@ -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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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;
}

View File

@ -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(`<div i18n>${input}</div>`);
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 <span>B<div>C</div></span> 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 <span>B<div>C</div></span> D'))
.toEqual('A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D');
});
// nested ICUs
[
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}',
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'
],
it('should serialize simple ICU for `GetMsg()`', () => {
expect(serialize('{age, plural, 10 {ten} other {other}}'))
.toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}');
});
// ICU with nested HTML
[
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'
]
];
it('should serialize nested ICUs for `GetMsg()`', () => {
expect(serialize(
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
.toEqual(
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}');
});
cases.forEach(([input, output]) => { expect(serialize(input)).toEqual(output); });
it('should serialize ICU with nested HTML for `GetMsg()`', () => {
expect(serialize('{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}'))
.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}}<div>{gender, select, male {male} female {female} other {other}}</div>'))
.toEqual('{$icu}{$startTagDiv}{$icu}{$closeTagDiv}');
});
});
describe('serializeI18nMessageForLocalize', () => {
const serialize = (input: string) => {
const tree = parse(`<div i18n>${input}</div>`);
const root = tree.nodes[0] as t.Element;
return serializeI18nMessageForLocalize(root.i18n as i18n.Message);
};
it('should serialize plain text for `$localize()`', () => {
expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []});
});
it('should serialize text with interpolation for `$localize()`', () => {
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }} done')).toEqual({
messageParts: ['Some text ', ' and ', ' done'],
placeHolders: ['interpolation', 'interpolation_1']
});
});
it('should serialize text with interpolation at start for `$localize()`', () => {
expect(serialize('{{ valueA }} and {{ valueB + valueC }} done')).toEqual({
messageParts: ['', ' and ', ' done'],
placeHolders: ['interpolation', 'interpolation_1']
});
});
it('should serialize text with interpolation at end for `$localize()`', () => {
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')).toEqual({
messageParts: ['Some text ', ' and ', ''],
placeHolders: ['interpolation', 'interpolation_1']
});
});
it('should serialize only interpolation for `$localize()`', () => {
expect(serialize('{{ valueB + valueC }}'))
.toEqual({messageParts: ['', ''], placeHolders: ['interpolation']});
});
it('should serialize content with HTML tags for `$localize()`', () => {
expect(serialize('A <span>B<div>C</div></span> 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 {<b>ten</b>} other {<div class="A">other</div>}}')).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}}<div>{gender, select, male {male} female {female} other {other}}</div>'))
.toEqual({
messageParts: ['', '', '', '', ''],
placeHolders: ['icu', 'startTagDiv', 'icu', 'closeTagDiv']
});
});
});
describe('serializeIcuNode', () => {
const serialize = (input: string) => {
const tree = parse(`<div i18n>${input}</div>`);
const rooti18n = (tree.nodes[0] as t.Element).i18n as i18n.Message;
return serializeIcuNode(rooti18n.nodes[0] as i18n.Icu);
};
it('should serialize a simple ICU', () => {
expect(serialize('{age, plural, 10 {ten} other {other}}'))
.toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}');
});
it('should serialize a next ICU', () => {
expect(serialize(
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
.toEqual(
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}');
});
it('should serialize ICU with nested HTML', () => {
expect(serialize('{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}'))
.toEqual(
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}');
});
});

View File

@ -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.');
};
}

View File

@ -5,7 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import '../util/ng_i18n_closure_mode';
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
@ -13,6 +12,7 @@ import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {global} from '../util/global';
import {attachPatchData} from './context_discovery';
import {bind, setDelayProjection} from './instructions/all';
import {attachI18nOpCodesDebug} from './instructions/lview_debug';
@ -1317,40 +1317,86 @@ function replaceNgsp(value: string): string {
return value.replace(NGSP_UNICODE_REGEXP, ' ');
}
let TRANSLATIONS: {[key: string]: string} = {};
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }
/**
* Set the configuration for `i18nLocalize`.
* Provide translations for `$localize`.
*
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
translations: {}
}) {
TRANSLATIONS = options.translations;
}
type TranslationInfo = {messageParts: TemplateStringsArray, placeholderNames: string[]};
type MessageInfo = {translationKey: string, replacements: {[placeholderName: string]: any}};
const PLACEHOLDER_MARKER = ':';
const TRANSLATIONS: {[key: string]: TranslationInfo} = {};
const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;
Object.keys(options.translations).forEach(key => {
TRANSLATIONS[key] = splitMessage(options.translations[key]);
});
/**
* A goog.getMsg-like function for users that do not use Closure.
*
* This method is required as a *temporary* measure to prevent i18n tests from being blocked while
* running outside of Closure Compiler. This method will not be needed once runtime translation
* service support is introduced.
*
* @codeGenApi
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function ɵɵi18nLocalize(input: string, placeholders?: {[key: string]: string}) {
if (typeof TRANSLATIONS[input] !== 'undefined') { // to account for empty string
input = TRANSLATIONS[input];
if (ngDevMode) {
if (global.$localize === undefined) {
throw new Error(
'The global function `$localize` is missing. Please add `import \'@angular/localize\';` to your polyfills.ts file.');
}
}
if (placeholders !== undefined && Object.keys(placeholders).length) {
return input.replace(LOCALIZE_PH_REGEXP, (_, key) => placeholders[key] || '');
$localize.translate = function(messageParts: TemplateStringsArray, expressions: readonly any[]):
[TemplateStringsArray, readonly any[]] {
const message = parseMessage(messageParts, expressions);
const translation = TRANSLATIONS[message.translationKey];
const result: [TemplateStringsArray, readonly any[]] =
(translation === undefined ? [messageParts, expressions] : [
translation.messageParts,
translation.placeholderNames.map(placeholder => message.replacements[placeholder])
]);
return result;
};
function splitMessage(message: string): TranslationInfo {
const parts = message.split(/{\$([^}]*)}/);
const messageParts = [parts[0]];
const placeholderNames: string[] = [];
for (let i = 1; i < parts.length - 1; i += 2) {
placeholderNames.push(parts[i]);
messageParts.push(parts[i + 1]);
}
const rawMessageParts =
messageParts.map(part => part.charAt(0) === PLACEHOLDER_MARKER ? '\\' + part : part);
return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames};
}
function parseMessage(
messageParts: TemplateStringsArray, expressions: readonly any[]): MessageInfo {
const PLACEHOLDER_NAME_MARKER = ':';
const replacements: {[placeholderName: string]: any} = {};
let translationKey = messageParts[0];
for (let i = 1; i < messageParts.length; i++) {
const messagePart = messageParts[i];
const expression = expressions[i - 1];
// There is a problem with synthesized template literals in TS where the raw version
// cannot be found, since there is no original source code to read it from.
// In that case we just fall back on the non-raw version.
// This should be OK because synthesized nodes (from the template compiler) will always have
// placeholder names provided.
if ((messageParts.raw[i] || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER) {
const endOfPlaceholderName = messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1);
const placeholderName = messagePart.substring(1, endOfPlaceholderName);
translationKey += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`;
replacements[placeholderName] = expression;
} else {
translationKey += messagePart;
replacements[`ph_${i}`] = expression;
}
}
return {translationKey, replacements};
}
function makeTemplateObject(cooked: string[], raw: string[]): TemplateStringsArray {
Object.defineProperty(cooked, 'raw', {value: raw});
return cooked as any;
}
return input;
}
/**

View File

@ -149,7 +149,6 @@ export {
ɵɵi18nApply,
ɵɵi18nPostprocess,
i18nConfigureLocalize,
ɵɵi18nLocalize,
getLocaleId,
setLocaleId,
} from './i18n';

View File

@ -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",

View File

@ -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}}':

View File

@ -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",
],
)

View File

@ -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<Todo> = [
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) {

View File

@ -19,6 +19,12 @@ describe('functional test for todo i18n', () => {
BUNDLES.forEach(bundle => {
describe(bundle, () => {
it('should render todo i18n', withBody('<todo-app></todo-app>', 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');

View File

@ -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';

View File

@ -13,6 +13,7 @@ ng_module(
"//packages:types",
"//packages/compiler",
"//packages/core",
"//packages/localize",
"@npm//@types/jasmine",
"@npm//zone.js",
],

View File

@ -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

View File

@ -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'},