`;
@@ -1347,7 +1347,7 @@ describe('i18n support in the view compiler', () => {
verify(input, output);
});
- it('should be able to be child elements inside i18n block', () => {
+ it('should be able to act as child elements inside i18n block', () => {
const input = `
Template content: {{ valueA | uppercase }}
diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts
index f0d6142ffd..386af20d0a 100644
--- a/packages/core/src/render3/i18n.ts
+++ b/packages/core/src/render3/i18n.ts
@@ -504,7 +504,7 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
* @publicAPI
*/
export function i18nPostprocess(
- message: string, replacements: {[key: string]: (string | string[])}): string {
+ message: string, replacements: {[key: string]: (string | string[])} = {}): string {
//
// Step 1: resolve all multi-value cases (like [�*1:1��#2:1�|�#4:1�|�5�])
//
diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts
new file mode 100644
index 0000000000..0c13466735
--- /dev/null
+++ b/packages/core/test/i18n_integration_spec.ts
@@ -0,0 +1,480 @@
+/**
+ * @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 {Component, Directive, TemplateRef, ViewContainerRef} from '@angular/core';
+import {TestBed} from '@angular/core/testing';
+import {expect} from '@angular/platform-browser/testing/src/matchers';
+import {fixmeIvy, onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing';
+
+@Directive({
+ selector: '[tplRef]',
+})
+class DirectiveWithTplRef {
+ constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {}
+ ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); }
+}
+
+@Component({selector: 'my-comp', template: ''})
+class MyComp {
+ name = 'John';
+ items = ['1', '2', '3'];
+ visible = true;
+ age = 20;
+ count = 2;
+ otherLabel = 'other label';
+}
+
+const TRANSLATIONS: any = {
+ 'one': 'un',
+ 'two': 'deux',
+ 'more than two': 'plus que deux',
+ 'ten': 'dix',
+ 'twenty': 'vingt',
+ 'other': 'autres',
+ 'Hello': 'Bonjour',
+ 'Hello {$interpolation}': 'Bonjour {$interpolation}',
+ 'Bye': 'Au revoir',
+ 'Item {$interpolation}': 'Article {$interpolation}',
+ '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"',
+ 'My logo': 'Mon logo',
+ '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}':
+ '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}',
+ '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}':
+ '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}',
+ '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Hello{$closeTagSpan}{$closeTagNgContainer}':
+ '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Bonjour{$closeTagSpan}{$closeTagNgContainer}',
+ '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}':
+ '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autres}}',
+ '{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}':
+ '{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}',
+ '{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}':
+ '{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}'
+};
+
+const getFixtureWithOverrides = (overrides = {}) => {
+ TestBed.overrideComponent(MyComp, {set: overrides});
+ const fixture = TestBed.createComponent(MyComp);
+ fixture.detectChanges();
+ return fixture;
+};
+
+onlyInIvy('Ivy i18n logic').describe('i18n', function() {
+
+ beforeEach(() => {
+ polyfillGoogGetMsg(TRANSLATIONS);
+ TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]});
+ });
+
+ describe('attributes', () => {
+ it('should translate static attributes', () => {
+ const title = 'Hello';
+ const template = `
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.title).toBe('Bonjour');
+ });
+
+ it('should support interpolation', () => {
+ const title = 'Hello {{ name }}';
+ const template = `
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.title).toBe('Bonjour John');
+ });
+
+ it('should support interpolation with custom interpolation config', () => {
+ const title = 'Hello {% name %}';
+ const template = `
`;
+ const interpolation = ['{%', '%}'] as[string, string];
+ const fixture = getFixtureWithOverrides({template, interpolation});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.title).toBe('Bonjour John');
+ });
+
+ fixmeIvy('FW-903: i18n attributes in nested templates throws at runtime')
+ .it('should correctly bind to context in nested template', () => {
+ const title = 'Item {{ id }}';
+ const template = `
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ for (let i = 0; i < element.children.length; i++) {
+ const child = element.children[i];
+ expect((child as any).innerHTML).toBe(`
`);
+ }
+ });
+
+ fixmeIvy('FW-904: i18n attributes placed on i18n root node don\'t work')
+ .it('should work correctly when placed on i18n root node', () => {
+ const title = 'Hello {{ name }}';
+ const content = 'Hello';
+ const template = `
+
${content}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.title).toBe('Bonjour John');
+ expect(element).toHaveText('Bonjour');
+ });
+
+ it('should add i18n attributes on self-closing tags', () => {
+ const title = 'Hello {{ name }}';
+ const template = `
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.title).toBe('Bonjour John');
+ });
+ });
+
+ describe('nested nodes', () => {
+ it('should handle static content', () => {
+ const content = 'Hello';
+ const template = `
${content}
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('Bonjour');
+ });
+
+ it('should support interpolation', () => {
+ const content = 'Hello {{ name }}';
+ const template = `
${content}
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('Bonjour John');
+ });
+
+ it('should support interpolation with custom interpolation config', () => {
+ const content = 'Hello {% name %}';
+ const template = `
${content}
`;
+ const interpolation = ['{%', '%}'] as[string, string];
+ const fixture = getFixtureWithOverrides({template, interpolation});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('Bonjour John');
+ });
+
+ it('should properly escape quotes in content', () => {
+ const content = `'Single quotes' and "Double quotes"`;
+ const template = `
${content}
`;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('\'Guillemets simples\' et "Guillemets doubles"');
+ });
+
+ it('should correctly bind to context in nested template', () => {
+ const content = 'Item {{ id }}';
+ const template = `
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ for (let i = 0; i < element.children.length; i++) {
+ const child = element.children[i];
+ expect(child).toHaveText(`Article ${i + 1}`);
+ }
+ });
+
+ it('should handle i18n attributes inside i18n section', () => {
+ const title = 'Hello {{ name }}';
+ const template = `
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ const content = `
`;
+ expect(element.innerHTML).toBe(content);
+ });
+
+ it('should handle i18n blocks in nested templates', () => {
+ const content = 'Hello {{ name }}';
+ const template = `
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.children[0]).toHaveText('Bonjour John');
+ });
+
+ it('should ignore i18n attributes on self-closing tags', () => {
+ const template = '
';
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element.innerHTML).toBe(template.replace(' i18n', ''));
+ });
+
+ it('should handle i18n attribute with directives', () => {
+ const content = 'Hello {{ name }}';
+ const template = `
+
${content}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('Bonjour John');
+ });
+ });
+
+ describe('ng-container and ng-template support', () => {
+ it('should handle single translation message within ng-container', () => {
+ const content = 'Hello {{ name }}';
+ const template = `
+
${content}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('Bonjour John');
+ });
+
+ it('should handle single translation message within ng-template', () => {
+ const content = 'Hello {{ name }}';
+ const template = `
+
${content}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element).toHaveText('Bonjour John');
+ });
+
+ it('should be able to act as child elements inside i18n block (plain text content)', () => {
+ const hello = 'Hello';
+ const bye = 'Bye';
+ const template = `
+
+
+ ${hello}
+
+
+ ${bye}
+
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir');
+ });
+
+ fixmeIvy(
+ 'FW-910: Invalid placeholder structure generated when using
with content that contains tags')
+ .it('should be able to act as child elements inside i18n block (text + tags)', () => {
+ const content = 'Hello';
+ const template = `
+
+
+ ${content}
+
+
+ ${content}
+
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ const spans = element.getElementsByTagName('span');
+ for (let i = 0; i < spans.length; i++) {
+ const child = spans[i];
+ expect((child as any).innerHTML).toBe('Bonjour');
+ }
+ });
+
+ it('should handle self-closing tags as content', () => {
+ const label = 'My logo';
+ const content = `${label}`;
+ const template = `
+
+ ${content}
+
+
+ ${content}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ const spans = element.getElementsByTagName('span');
+ for (let i = 0; i < spans.length; i++) {
+ const child = spans[i];
+ expect(child).toHaveText('Mon logo');
+ }
+ });
+ });
+
+ describe('ICU logic', () => {
+ it('should handle single ICUs', () => {
+ const template = `
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element).toHaveText('vingt');
+ });
+
+ it('should support ICUs generated outside of i18n blocks', () => {
+ const template = `
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element).toHaveText('vingt');
+ });
+
+ it('should support interpolation', () => {
+ const template = `
+ {age, select, 10 {ten} other {{{ otherLabel }}}}
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element).toHaveText(fixture.componentInstance.otherLabel);
+ });
+
+ it('should support interpolation with custom interpolation config', () => {
+ const template = `
+ {age, select, 10 {ten} other {{% otherLabel %}}}
+ `;
+ const interpolation = ['{%', '%}'] as[string, string];
+ const fixture = getFixtureWithOverrides({template, interpolation});
+
+ const element = fixture.nativeElement;
+ expect(element).toHaveText(fixture.componentInstance.otherLabel);
+ });
+
+ it('should handle ICUs with HTML tags inside', () => {
+ const template = `
+
+ {age, select, 10 {10 -
ten} 20 {20 -
twenty} other {
other
}}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ const italicTags = element.getElementsByTagName('i');
+ expect(italicTags.length).toBe(1);
+ expect(italicTags[0].innerHTML).toBe('vingt');
+ });
+
+ fixmeIvy('FW-905: Multiple ICUs in one i18n block are not processed')
+ .it('should handle multiple ICUs in one block', () => {
+ const template = `
+
+ {age, select, 10 {ten} 20 {twenty} other {other}} -
+ {count, select, 1 {one} 2 {two} other {more than two}}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('vingt - deux');
+ });
+
+ fixmeIvy('FW-906: Multiple ICUs wrapped in HTML tags in one i18n block throw an error')
+ .it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => {
+ const template = `
+
+
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+
+
+ {count, select, 1 {one} 2 {two} other {more than two}}
+
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ const spans = element.getElementsByTagName('span');
+ expect(spans.length).toBe(2);
+ expect(spans[0].innerHTML).toBe('vingt');
+ expect(spans[1].innerHTML).toBe('deux');
+ });
+
+ it('should handle ICUs inside a template in i18n block', () => {
+ const template = `
+
+
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ const spans = element.getElementsByTagName('span');
+ expect(spans.length).toBe(1);
+ expect(spans[0]).toHaveText('vingt');
+ });
+
+ it('should handle nested icus', () => {
+ const template = `
+
+ {age, select,
+ 10 {ten - {count, select, 1 {one} 2 {two} other {more than two}}}
+ 20 {twenty - {count, select, 1 {one} 2 {two} other {more than two}}}
+ other {other}}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement.firstChild;
+ expect(element).toHaveText('vingt - deux');
+ });
+
+ fixmeIvy('FW-908: ICUs inside s throw an error at runtime')
+ .it('should handle ICUs inside ', () => {
+ const template = `
+
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element.innerHTML).toBe('vingt');
+ });
+
+ fixmeIvy('FW-909: ICUs inside s throw errors at runtime')
+ .it('should handle ICUs inside ', () => {
+ const template = `
+
+ {age, select, 10 {ten} 20 {twenty} other {other}}
+
+ `;
+ const fixture = getFixtureWithOverrides({template});
+
+ const element = fixture.nativeElement;
+ expect(element.innerHTML).toBe('vingt');
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/private/testing/src/goog_get_msg.ts b/packages/private/testing/src/goog_get_msg.ts
index 902339e955..2547839cd8 100644
--- a/packages/private/testing/src/goog_get_msg.ts
+++ b/packages/private/testing/src/goog_get_msg.ts
@@ -16,13 +16,12 @@
export function polyfillGoogGetMsg(translations: {[key: string]: string} = {}): void {
const glob = (global as any);
glob.goog = glob.goog || {};
- glob.goog.getMsg =
- glob.goog.getMsg || function(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(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
- input;
- };
-}
+ glob.goog.getMsg = function(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(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
+ input;
+ };
+}
\ No newline at end of file