angular/packages/core/test/i18n_integration_spec.ts
Olivier Combe c61ea1d5bd fix(ivy): support for multiple ICU expressions in the same i18n block (#28083)
There were two issues with multiple ICU expressions in the same i18n block:
- the regexp that was used to parse the text wasn't able to handle multiple ICU expressions, I've replaced it with parsing the text and searching for brackets (which is what we ended up doing in the end anyway)
- we allocate node indexes for nodes generated by the ICU expressions which increases the expando value, but we would create the nodes for those cases during the update phase. In the mean time we would create some nodes during the creation phase (comment nodes for ICU expressions, text nodes, ...) with an auto increment index. This means that any node created after an ICU expression would get the following index value, but the ICU case nodes expected to use the same index as well... There was a mismatch between the auto generated index, and the expected index which was causing problems when we needed to select those nodes for updates later on. To fix it, I've added the expected node index to the list of mutate codes that we generate, and we do not use an auto increment value anymore.

FW-905 #resolve
PR Close #28083
2019-01-16 09:49:54 -08:00

478 lines
17 KiB
TypeScript

/**
* @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, 1 {one} 2 {two} other {more than two}}':
'{VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}',
'{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 = `<div i18n-title="m|d" title="${title}"></div>`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
expect(element.title).toBe('Bonjour');
});
it('should support interpolation', () => {
const title = 'Hello {{ name }}';
const template = `<div i18n-title="m|d" title="${title}"></div>`;
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 = `<div i18n-title="m|d" title="${title}"></div>`;
const interpolation = ['{%', '%}'] as[string, string];
const fixture = getFixtureWithOverrides({template, interpolation});
const element = fixture.nativeElement.firstChild;
expect(element.title).toBe('Bonjour John');
});
it('should correctly bind to context in nested template', () => {
const title = 'Item {{ id }}';
const template = `
<div *ngFor='let id of items'>
<div i18n-title='m|d' title='${title}'></div>
</div>
`;
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(`<div title="Article ${i + 1}"></div>`);
}
});
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 = `
<div i18n i18n-title="m|d" title="${title}">${content}</div>
`;
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 = `<img src="logo.png" i18n-title title="${title}">`;
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 = `<div i18n>${content}</div>`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
expect(element).toHaveText('Bonjour');
});
it('should support interpolation', () => {
const content = 'Hello {{ name }}';
const template = `<div i18n>${content}</div>`;
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 = `<div i18n>${content}</div>`;
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 = `<div i18n>${content}</div>`;
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 = `
<div *ngFor='let id of items'>
<div i18n>${content}</div>
</div>
`;
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 = `
<div i18n>
<div i18n-title="m|d" title="${title}"></div>
</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
const content = `<div title="Bonjour John"></div>`;
expect(element.innerHTML).toBe(content);
});
it('should handle i18n blocks in nested templates', () => {
const content = 'Hello {{ name }}';
const template = `
<div *ngIf="visible">
<div i18n>${content}</div>
</div>
`;
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 = '<img src="logo.png" i18n>';
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 = `
<div *ngIf="visible" i18n>${content}</div>
`;
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 = `
<ng-container i18n>${content}</ng-container>
`;
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 = `
<ng-template i18n tplRef>${content}</ng-template>
`;
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 = `
<div i18n>
<ng-template tplRef>
${hello}
</ng-template>
<ng-container>
${bye}
</ng-container>
</div>
`;
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 <ng-template> with content that contains tags')
.it('should be able to act as child elements inside i18n block (text + tags)', () => {
const content = 'Hello';
const template = `
<div i18n>
<ng-template tplRef>
<span>${content}</span>
</ng-template>
<ng-container>
<span>${content}</span>
</ng-container>
</div>
`;
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}<img src="logo.png" title="Logo">`;
const template = `
<ng-container i18n>
<span>${content}</span>
</ng-container>
<ng-template i18n tplRef>
<span>${content}</span>
</ng-template>
`;
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 = `
<div i18n>{age, select, 10 {ten} 20 {twenty} other {other}}</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement;
expect(element).toHaveText('vingt');
});
it('should support ICUs generated outside of i18n blocks', () => {
const template = `
<div>{age, select, 10 {ten} 20 {twenty} other {other}}</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement;
expect(element).toHaveText('vingt');
});
it('should support interpolation', () => {
const template = `
<div i18n>{age, select, 10 {ten} other {{{ otherLabel }}}}</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement;
expect(element).toHaveText(fixture.componentInstance.otherLabel);
});
it('should support interpolation with custom interpolation config', () => {
const template = `
<div i18n>{age, select, 10 {ten} other {{% otherLabel %}}}</div>
`;
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 = `
<div i18n>
{age, select, 10 {10 - <b>ten</b>} 20 {20 - <i>twenty</i>} other {<div class="other"><u>other</u></div>}}
</div>
`;
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');
});
it('should handle multiple ICUs in one block', () => {
const template = `
<div i18n>
{age, select, 10 {ten} 20 {twenty} other {other}} -
{count, select, 1 {one} 2 {two} other {more than two}}
</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
expect(element).toHaveText('vingt - deux');
});
it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => {
const template = `
<div i18n>
<span>
{age, select, 10 {ten} 20 {twenty} other {other}}
</span>
<span>
{count, select, 1 {one} 2 {two} other {more than two}}
</span>
</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
const spans = element.getElementsByTagName('span');
expect(spans.length).toBe(2);
expect(spans[0]).toHaveText('vingt');
expect(spans[1]).toHaveText('deux');
});
it('should handle ICUs inside a template in i18n block', () => {
const template = `
<div i18n>
<span *ngIf="visible">
{age, select, 10 {ten} 20 {twenty} other {other}}
</span>
</div>
`;
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 = `
<div i18n>
{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}}
</div>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement.firstChild;
expect(element).toHaveText('vingt - deux');
});
it('should handle ICUs inside <ng-container>', () => {
const template = `
<ng-container i18n>
{age, select, 10 {ten} 20 {twenty} other {other}}
</ng-container>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement;
expect(element).toHaveText('vingt');
});
it('should handle ICUs inside <ng-template>', () => {
const template = `
<ng-template i18n tplRef>
{age, select, 10 {ten} 20 {twenty} other {other}}
</ng-template>
`;
const fixture = getFixtureWithOverrides({template});
const element = fixture.nativeElement;
expect(element).toHaveText('vingt');
});
});
});