Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,686 changes: 1,243 additions & 443 deletions packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,7 @@ describe('ngtsc behavioral tests', () => {
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i18n(1, MSG_EXTERNAL_8321000940098097247$$TEST_TS_0);');
expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_1');
});

it('should take i18nUseExternalIds config option into account', () => {
Expand All @@ -1902,7 +1902,7 @@ describe('ngtsc behavioral tests', () => {
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i18n(1, MSG_TEST_TS_0);');
expect(jsContents).not.toContain('MSG_EXTERNAL_');
});

it('@Component\'s `interpolation` should override default interpolation config', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class Identifiers {
static i18nEnd: o.ExternalReference = {name: 'Δi18nEnd', moduleName: CORE};
static i18nApply: o.ExternalReference = {name: 'Δi18nApply', moduleName: CORE};
static i18nPostprocess: o.ExternalReference = {name: 'Δi18nPostprocess', moduleName: CORE};
static i18nLocalize: o.ExternalReference = {name: 'Δi18nLocalize', moduleName: CORE};

static load: o.ExternalReference = {name: 'Δload', moduleName: CORE};

Expand Down
63 changes: 37 additions & 26 deletions packages/compiler/src/render3/view/i18n/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ 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_';
const CLOSURE_TRANSLATION_MATCHER_REGEXP = new RegExp(`^${CLOSURE_TRANSLATION_PREFIX}`);

/* Prefix for non-`goog.getMsg` i18n-related vars */
const TRANSLATION_PREFIX = 'I18N_';

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 = '@@';
Expand All @@ -48,17 +50,36 @@ export type I18nMeta = {
};

function i18nTranslationToDeclStmt(
variable: o.ReadVarExpr, message: string,
params?: {[name: string]: o.Expression}): o.DeclareVarStmt {
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));
}
const fnCall = o.variable(GOOG_GET_MSG).callFn(args);
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);

// 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 informations for a message (id, description, meaning)
// 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[] = [];
Expand Down Expand Up @@ -231,34 +252,24 @@ 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
*/
export function getTranslationDeclStmts(
variable: o.ReadVarExpr, message: string, meta: I18nMeta,
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[] = [];
const docStatements = i18nMetaToDocStmt(meta);
if (docStatements) {
statements.push(docStatements);
}

statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params));

if (transformFn) {
statements.push(i18nTranslationToDeclStmt(variable, message, params));

// Closure Compiler doesn't allow non-goo.getMsg const names to start with `MSG_`,
// so we update variable name prefix in case post processing is required, so we can
// assign the result of post-processing function to the var that starts with `I18N_`
const raw = o.variable(variable.name !);
variable.name = variable.name !.replace(CLOSURE_TRANSLATION_MATCHER_REGEXP, TRANSLATION_PREFIX);

statements.push(
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
} else {
statements.push(i18nTranslationToDeclStmt(variable, message, params));
statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
}

return statements;
}
}
14 changes: 9 additions & 5 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prep
import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {Instruction, StylingBuilder} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';

Expand Down Expand Up @@ -322,14 +322,18 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
i18nTranslate(
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
const _ref = ref || this.i18nAllocateRef(message.id);
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 _params: {[key: string]: any} = {};
if (params && Object.keys(params).length) {
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
}
const meta = metaFromI18nMessage(message);
const content = getSerializedI18nContent(message);
const statements = getTranslationDeclStmts(_ref, content, meta, _params, transformFn);
const statements =
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
this.constantPool.statements.push(...statements);
return _ref;
}
Expand Down Expand Up @@ -361,7 +365,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
return bound;
}

i18nAllocateRef(messageId: string): o.ReadVarExpr {
i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
let name: string;
const suffix = this.fileBasedI18nSuffix.toUpperCase();
if (this.i18nUseExternalIds) {
Expand Down Expand Up @@ -425,7 +429,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (this.i18nContext) {
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
} else {
const ref = this.i18nAllocateRef((meta as i18n.Message).id);
const ref = o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export {
Δi18nEnd,
Δi18nApply,
Δi18nPostprocess,
i18nConfigureLocalize as ɵi18nConfigureLocalize,
Δi18nLocalize,
setClassMetadata as ɵsetClassMetadata,
ΔresolveWindow,
ΔresolveDocument,
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/render3/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import '../util/ng_i18n_closure_mode';

import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
Expand Down Expand Up @@ -1604,3 +1606,38 @@ function parseNodes(
}
}
}

let TRANSLATIONS: {[key: string]: string} = {};
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }

/**
* Set the configuration for `i18nLocalize`.
*
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
Comment thread
AndrewKushnir marked this conversation as resolved.
Resolved
translations: {}
}) {
TRANSLATIONS = options.translations;
}

const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;

/**
* 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.
*
* @publicApi
* @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];
}
return Object.keys(placeholders).length ?
input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') :
input;
}
2 changes: 2 additions & 0 deletions packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export {
Δi18nEnd,
Δi18nApply,
Δi18nPostprocess,
i18nConfigureLocalize,
Δi18nLocalize,
} from './i18n';

export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/jit/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'Δi18nEnd': r3.Δi18nEnd,
'Δi18nApply': r3.Δi18nApply,
'Δi18nPostprocess': r3.Δi18nPostprocess,
'Δi18nLocalize': r3.Δi18nLocalize,
'ΔresolveWindow': r3.ΔresolveWindow,
'ΔresolveDocument': r3.ΔresolveDocument,
'ΔresolveBody': r3.ΔresolveBody,
Expand Down
25 changes: 7 additions & 18 deletions packages/core/src/util/ng_dev_mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {global} from './global';

declare global {
const ngDevMode: null|NgDevModePerfCounters;
interface NgDevModePerfCounters {
Expand Down Expand Up @@ -38,8 +40,6 @@ declare global {
}
}

declare let global: any;

export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
const newCounters: NgDevModePerfCounters = {
firstTemplatePass: 0,
Expand Down Expand Up @@ -69,31 +69,20 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
stylingApply: 0,
stylingApplyCacheMiss: 0,
};
// NOTE: Under Ivy we may have both window & global defined in the Node
// environment since ensureDocument() in render3.ts sets global.window.
if (typeof window != 'undefined') {
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(window as any)['ngDevMode'] = newCounters;
}
if (typeof global != 'undefined') {
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(global as any)['ngDevMode'] = newCounters;
}
if (typeof self != 'undefined') {
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(self as any)['ngDevMode'] = newCounters;
}

// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
global['ngDevMode'] = newCounters;
return newCounters;
}

/**
* This checks to see if the `ngDevMode` has been set. If yes,
* than we honor it, otherwise we default to dev mode with additional checks.
* then we honor it, otherwise we default to dev mode with additional checks.
*
* The idea is that unless we are doing production build where we explicitly
* set `ngDevMode == false` we should be helping the developer by providing
* as much early warning and errors as possible.
*/
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (typeof global['ngDevMode'] === 'undefined' || global['ngDevMode']) {
ngDevModeResetPerfCounters();
}
19 changes: 19 additions & 0 deletions packages/core/src/util/ng_i18n_closure_mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @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 {global} from './global';

Comment thread
ocombe marked this conversation as resolved.
Resolved
declare global {
const ngI18nClosureMode: boolean;
}

if (typeof global['ngI18nClosureMode'] === 'undefined') {
// Make sure to refer to ngI18nClosureMode as ['ngI18nClosureMode'] for closure.
global['ngI18nClosureMode'] =
typeof global['goog'] !== 'undefined' && typeof global['goog'].getMsg === 'function';
}
6 changes: 3 additions & 3 deletions packages/core/test/acceptance/view_container_ref_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {Component, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled, onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';

describe('ViewContainerRef', () => {

Expand All @@ -22,7 +22,7 @@ describe('ViewContainerRef', () => {
};

beforeEach(() => {
polyfillGoogGetMsg(TRANSLATIONS);
ɵi18nConfigureLocalize({translations: TRANSLATIONS});
TestBed.configureTestingModule(
{declarations: [StructDir, ViewContainerRefComp, ViewContainerRefApp, DestroyCasesComp]});
});
Expand Down
8 changes: 0 additions & 8 deletions packages/core/test/bundling/hello_world_i18n/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@
property renaming.
-->
<script>
// `goog.getMsg()` will be provided by Closure
const translations = {
'Hello World!': 'Bonjour Monde!',
'Hello Title!': 'Bonjour Titre!',
};
window.goog = window.goog || {};
window.goog.getMsg = (key) => translations[key] || key;

document.write('<script src="' +
(document.location.search.endsWith('debug') ? '/bundle.min_debug.js' : '/bundle.min.js') +
'"></' + 'script>');
Expand Down
9 changes: 8 additions & 1 deletion packages/core/test/bundling/hello_world_i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, NgModule, ɵrenderComponent as renderComponent} from '@angular/core';
import {Component, NgModule, ɵi18nConfigureLocalize, ɵrenderComponent as renderComponent} from '@angular/core';

const translations = {
'Hello World!': 'Bonjour Monde!',
'Hello Title!': 'Bonjour Titre!',
};

ɵi18nConfigureLocalize({translations});

@Component({
selector: 'hello-world',
Expand Down
Loading