/**
* @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
*/
// 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/init';
import {CommonModule, registerLocaleData} from '@angular/common';
import localeRo from '@angular/common/locales/ro';
import {Component, ContentChild, ElementRef, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, Pipe, PipeTransform, NO_ERRORS_SCHEMA} from '@angular/core';
import {setDelayProjection} from '@angular/core/src/render3/instructions/projection';
import {TestBed} from '@angular/core/testing';
import {loadTranslations, clearTranslations} from '@angular/localize';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
import {computeMsgId} from '@angular/compiler';
import {BehaviorSubject} from 'rxjs';
onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AppComp, DirectiveWithTplRef, UppercasePipe],
// In some of the tests we use made-up tag names for better readability, however they'll
// cause validation errors. Add the `NO_ERRORS_SCHEMA` so that we don't have to declare
// dummy components for each one of them.
schemas: [NO_ERRORS_SCHEMA],
});
});
afterEach(() => {
setDelayProjection(false);
clearTranslations();
});
it('should translate text', () => {
loadTranslations({[computeMsgId('text')]: 'texte'});
const fixture = initWithTemplate(AppComp, `
text
`);
expect(fixture.nativeElement.innerHTML).toEqual(`texte
`);
});
it('should support interpolations', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}!')]: 'Bonjour {$INTERPOLATION}!'});
const fixture = initWithTemplate(AppComp, `Hello {{name}}!
`);
expect(fixture.nativeElement.innerHTML).toEqual(`Bonjour Angular!
`);
fixture.componentRef.instance.name = `John`;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`Bonjour John!
`);
});
it('should support named interpolations', () => {
loadTranslations({
[computeMsgId(' Hello {$USER_NAME}! Emails: {$AMOUNT_OF_EMAILS_RECEIVED} ')]:
' Bonjour {$USER_NAME}! Emails: {$AMOUNT_OF_EMAILS_RECEIVED} '
});
const fixture = initWithTemplate(AppComp, `
Hello {{ name // i18n(ph="user_name") }}!
Emails: {{ count // i18n(ph="amount of emails received") }}
`);
expect(fixture.nativeElement.innerHTML).toEqual(` Bonjour Angular! Emails: 0
`);
fixture.componentRef.instance.name = `John`;
fixture.componentRef.instance.count = 5;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(` Bonjour John! Emails: 5
`);
});
it('should support interpolations with custom interpolation config', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
const interpolation = ['{%', '%}'] as[string, string];
TestBed.overrideComponent(AppComp, {set: {interpolation}});
const fixture = initWithTemplate(AppComp, `Hello {% name %}
`);
expect(fixture.nativeElement.innerHTML).toBe('Bonjour Angular
');
});
it('should support &ngsp; in translatable sections', () => {
// note: the `` unicode symbol represents the `&ngsp;` in translations
loadTranslations({[computeMsgId('text ||')]: 'texte ||'});
const fixture = initWithTemplate(AppCompWithWhitespaces, `text |&ngsp;|
`);
expect(fixture.nativeElement.innerHTML).toEqual(`texte | |
`);
});
it('should support interpolations with complex expressions', () => {
loadTranslations({
[computeMsgId(' {$INTERPOLATION} - {$INTERPOLATION_1} - {$INTERPOLATION_2} ')]:
' {$INTERPOLATION} - {$INTERPOLATION_1} - {$INTERPOLATION_2} (fr) '
});
const fixture = initWithTemplate(AppComp, `
{{ name | uppercase }} -
{{ obj?.a?.b }} -
{{ obj?.getA()?.b }}
`);
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty strings
expect(fixture.nativeElement.innerHTML).toEqual(` ANGULAR - - (fr)
`);
fixture.componentRef.instance.obj = {
a: {b: 'value 1'},
getA: () => ({b: 'value 2'}),
};
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(` ANGULAR - value 1 - value 2 (fr)
`);
});
it('should support elements', () => {
loadTranslations({
[computeMsgId(
'Hello {$START_TAG_SPAN}world{$CLOSE_TAG_SPAN} and {$START_TAG_DIV}universe{$CLOSE_TAG_DIV}!',
'')]:
'Bonjour {$START_TAG_SPAN}monde{$CLOSE_TAG_SPAN} et {$START_TAG_DIV}univers{$CLOSE_TAG_DIV}!'
});
const fixture = initWithTemplate(
AppComp, `Hello
world and
universe
!
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`Bonjour
monde et
univers
!
`);
});
it('should support removing elements', () => {
loadTranslations({
[computeMsgId(
'Hello {$START_BOLD_TEXT}my{$CLOSE_BOLD_TEXT}{$START_TAG_SPAN}world{$CLOSE_TAG_SPAN}',
'')]: 'Bonjour {$START_TAG_SPAN}monde{$CLOSE_TAG_SPAN}'
});
const fixture =
initWithTemplate(AppComp, `Hello my world
!
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`Bonjour monde
!
`);
});
it('should support moving elements', () => {
loadTranslations({
[computeMsgId(
'Hello {$START_TAG_SPAN}world{$CLOSE_TAG_SPAN} and {$START_TAG_DIV}universe{$CLOSE_TAG_DIV}!',
'')]:
'Bonjour {$START_TAG_DIV}univers{$CLOSE_TAG_DIV} et {$START_TAG_SPAN}monde{$CLOSE_TAG_SPAN}!'
});
const fixture = initWithTemplate(
AppComp, `Hello
world and
universe
!
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`Bonjour
univers
et
monde !
`);
});
it('should support template directives', () => {
loadTranslations({
[computeMsgId(
'Content: {$START_TAG_DIV}before{$START_TAG_SPAN}middle{$CLOSE_TAG_SPAN}after{$CLOSE_TAG_DIV}!',
'')]:
'Contenu: {$START_TAG_DIV}avant{$START_TAG_SPAN}milieu{$CLOSE_TAG_SPAN}après{$CLOSE_TAG_DIV}!'
});
const fixture = initWithTemplate(
AppComp,
`Content:
beforemiddle after
!
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`Contenu:
avantmilieu après
!
`);
fixture.componentRef.instance.visible = false;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`Contenu: !
`);
});
it('should support multiple i18n blocks', () => {
loadTranslations({
[computeMsgId('trad {$INTERPOLATION}')]: 'traduction {$INTERPOLATION}',
[computeMsgId('start {$INTERPOLATION} middle {$INTERPOLATION_1} end')]:
'start {$INTERPOLATION_1} middle {$INTERPOLATION} end',
[computeMsgId(
'{$START_TAG_C}trad{$CLOSE_TAG_C}{$START_TAG_D}{$CLOSE_TAG_D}{$START_TAG_E}{$CLOSE_TAG_E}',
'')]: '{$START_TAG_E}{$CLOSE_TAG_E}{$START_TAG_C}traduction{$CLOSE_TAG_C}'
});
const fixture = initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(
``);
});
it('should support multiple sibling i18n blocks', () => {
loadTranslations({
[computeMsgId('Section 1')]: 'Section un',
[computeMsgId('Section 2')]: 'Section deux',
[computeMsgId('Section 3')]: 'Section trois',
});
const fixture = initWithTemplate(AppComp, `
Section 1
Section 2
Section 3
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`Section un
Section deux
Section trois
`);
});
it('should support multiple sibling i18n blocks inside of a template directive', () => {
loadTranslations({
[computeMsgId('Section 1')]: 'Section un',
[computeMsgId('Section 2')]: 'Section deux',
[computeMsgId('Section 3')]: 'Section trois',
});
const fixture = initWithTemplate(AppComp, `
Section 1
Section 2
Section 3
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(
`Section un Section deux Section trois Section un Section deux Section trois Section un Section deux Section trois `);
});
it('should properly escape quotes in content', () => {
loadTranslations({
[computeMsgId('\'Single quotes\' and "Double quotes"')]:
'\'Guillemets simples\' et "Guillemets doubles"'
});
const fixture =
initWithTemplate(AppComp, `'Single quotes' and "Double quotes"
`);
expect(fixture.nativeElement.innerHTML)
.toEqual('\'Guillemets simples\' et "Guillemets doubles"
');
});
it('should correctly bind to context in nested template', () => {
loadTranslations({[computeMsgId('Item {$INTERPOLATION}')]: 'Article {$INTERPOLATION}'});
const fixture = initWithTemplate(AppComp, `
`);
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 ignore i18n attributes on self-closing tags', () => {
const fixture = initWithTemplate(AppComp, ' ');
expect(fixture.nativeElement.innerHTML).toBe(` `);
});
it('should handle i18n attribute with directives', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
const fixture = initWithTemplate(AppComp, `Hello {{ name }}
`);
expect(fixture.nativeElement.firstChild).toHaveText('Bonjour Angular');
});
it('should work correctly with event listeners', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
@Component(
{selector: 'app-comp', template: `Hello {{ name }}
`})
class ListenerComp {
name = `Angular`;
clicks = 0;
onClick() { this.clicks++; }
}
TestBed.configureTestingModule({declarations: [ListenerComp]});
const fixture = TestBed.createComponent(ListenerComp);
fixture.detectChanges();
const element = fixture.nativeElement.firstChild;
const instance = fixture.componentInstance;
expect(element).toHaveText('Bonjour Angular');
expect(instance.clicks).toBe(0);
element.click();
expect(instance.clicks).toBe(1);
});
it('should support local refs inside i18n block', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_CONTAINER} One {$CLOSE_TAG_NG_CONTAINER}' +
'{$START_TAG_DIV} Two {$CLOSE_TAG_DIV}' +
'{$START_TAG_SPAN} Three {$CLOSE_TAG_SPAN}')]:
'{$START_TAG_NG_CONTAINER} Une {$CLOSE_TAG_NG_CONTAINER}' +
'{$START_TAG_DIV} Deux {$CLOSE_TAG_DIV}' +
'{$START_TAG_SPAN} Trois {$CLOSE_TAG_SPAN}'
});
const fixture = initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.textContent).toBe(' Une Deux Trois ');
});
it('should handle local refs correctly in case an element is removed in translation', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_CONTAINER} One {$CLOSE_TAG_NG_CONTAINER}' +
'{$START_TAG_DIV} Two {$CLOSE_TAG_DIV}' +
'{$START_TAG_SPAN} Three {$CLOSE_TAG_SPAN}')]: '{$START_TAG_DIV} Deux {$CLOSE_TAG_DIV}'
});
const fixture = initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.textContent).toBe(' Deux ');
});
describe('ng-container and ng-template support', () => {
it('should support ng-container', () => {
loadTranslations({[computeMsgId('text')]: 'texte'});
const fixture = initWithTemplate(AppComp, `text `);
expect(fixture.nativeElement.innerHTML).toEqual(`texte`);
});
it('should handle single translation message within ng-template', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
const fixture =
initWithTemplate(AppComp, `Hello {{ name }} `);
const element = fixture.nativeElement;
expect(element).toHaveText('Bonjour Angular');
});
// Note: applying structural directives to is typically user error, but it
// is technically allowed, so we need to support it.
it('should handle structural directives on ng-template', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
const fixture = initWithTemplate(
AppComp, `Hello {{ name }} `);
const element = fixture.nativeElement;
expect(element).toHaveText('Bonjour Angular');
});
it('should be able to act as child elements inside i18n block (plain text content)', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_TEMPLATE} Hello {$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER} Bye {$CLOSE_TAG_NG_CONTAINER}',
'')]:
'{$START_TAG_NG_TEMPLATE} Bonjour {$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER} Au revoir {$CLOSE_TAG_NG_CONTAINER}'
});
const fixture = initWithTemplate(AppComp, `
Hello
Bye
`);
const element = fixture.nativeElement.firstChild;
expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir');
});
it('should be able to act as child elements inside i18n block (text + tags)', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_TEMPLATE}{$START_TAG_SPAN}Hello{$CLOSE_TAG_SPAN}{$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER}{$START_TAG_SPAN}Hello{$CLOSE_TAG_SPAN}{$CLOSE_TAG_NG_CONTAINER}',
'')]:
'{$START_TAG_NG_TEMPLATE}{$START_TAG_SPAN}Bonjour{$CLOSE_TAG_SPAN}{$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER}{$START_TAG_SPAN}Bonjour{$CLOSE_TAG_SPAN}{$CLOSE_TAG_NG_CONTAINER}'
});
const fixture = initWithTemplate(AppComp, `
Hello
Hello
`);
const element = fixture.nativeElement;
const spans = element.getElementsByTagName('span');
for (let i = 0; i < spans.length; i++) {
expect(spans[i]).toHaveText('Bonjour');
}
});
it('should be able to act as child elements inside i18n block (text + pipes)', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_TEMPLATE}Hello {$INTERPOLATION}{$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER}Bye {$INTERPOLATION}{$CLOSE_TAG_NG_CONTAINER}',
'')]:
'{$START_TAG_NG_TEMPLATE}Hej {$INTERPOLATION}{$CLOSE_TAG_NG_TEMPLATE}{$START_TAG_NG_CONTAINER}Vi ses {$INTERPOLATION}{$CLOSE_TAG_NG_CONTAINER}'
});
const fixture = initWithTemplate(AppComp, `
Hello {{name | uppercase}}
Bye {{name | uppercase}}
`);
const element = fixture.nativeElement.firstChild;
expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Hej ANGULARVi ses ANGULAR');
});
it('should be able to handle deep nested levels with templates', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_SPAN} Hello - 1 {$CLOSE_TAG_SPAN}{$START_TAG_SPAN_1} Hello - 2 {$START_TAG_SPAN_1} Hello - 3 {$START_TAG_SPAN_1} Hello - 4 {$CLOSE_TAG_SPAN}{$CLOSE_TAG_SPAN}{$CLOSE_TAG_SPAN}{$START_TAG_SPAN} Hello - 5 {$CLOSE_TAG_SPAN}',
'')]:
'{$START_TAG_SPAN} Bonjour - 1 {$CLOSE_TAG_SPAN}{$START_TAG_SPAN_1} Bonjour - 2 {$START_TAG_SPAN_1} Bonjour - 3 {$START_TAG_SPAN_1} Bonjour - 4 {$CLOSE_TAG_SPAN}{$CLOSE_TAG_SPAN}{$CLOSE_TAG_SPAN}{$START_TAG_SPAN} Bonjour - 5 {$CLOSE_TAG_SPAN}'
});
const fixture = initWithTemplate(AppComp, `
Hello - 1
Hello - 2
Hello - 3
Hello - 4
Hello - 5
`);
const element = fixture.nativeElement;
const spans = element.getElementsByTagName('span');
for (let i = 0; i < spans.length; i++) {
expect(spans[i].innerHTML).toContain(`Bonjour - ${i + 1}`);
}
});
it('should handle self-closing tags as content', () => {
loadTranslations({
[computeMsgId('{$START_TAG_SPAN}My logo{$TAG_IMG}{$CLOSE_TAG_SPAN}')]:
'{$START_TAG_SPAN}Mon logo{$TAG_IMG}{$CLOSE_TAG_SPAN}'
});
const content = `My logo `;
const fixture = initWithTemplate(AppComp, `
${content}
${content}
`);
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');
}
});
it('should correctly find context for an element inside i18n section in ', () => {
loadTranslations({
[computeMsgId('{$START_LINK}Not logged in{$CLOSE_LINK}')]:
'{$START_LINK}Not logged in{$CLOSE_LINK}'
});
@Directive({selector: '[myDir]'})
class Dir {
condition = true;
}
@Component({
selector: 'my-cmp',
template: `
Logged in
Not logged in
`,
})
class Cmp {
isLogged = false;
}
TestBed.configureTestingModule({
declarations: [Cmp, Dir],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const a = fixture.debugElement.query(By.css('a'));
const dir = a.injector.get(Dir);
expect(dir.condition).toEqual(true);
});
});
describe('should support ICU expressions', () => {
it('with no root node', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}')]:
'{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}'
});
const fixture =
initWithTemplate(AppComp, `{count, select, 10 {ten} 20 {twenty} other {other}}`);
const element = fixture.nativeElement;
expect(element).toHaveText('autre');
});
it('with no i18n tag', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}')]:
'{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}'
});
const fixture = initWithTemplate(
AppComp, `{count, select, 10 {ten} 20 {twenty} other {other}}
`);
const element = fixture.nativeElement;
expect(element).toHaveText('autre');
});
it('multiple', () => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
'')]:
'{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
[computeMsgId('{VAR_SELECT, select, other {({INTERPOLATION})}}')]:
'{VAR_SELECT, select, other {({INTERPOLATION})}}',
[computeMsgId('{$ICU} - {$ICU_1}')]: '{$ICU} - {$ICU_1}',
});
const fixture = initWithTemplate(AppComp, `{count, plural,
=0 {no emails !}
=1 {one email }
other {{{count}} emails }
} - {name, select,
other {({{name}})}
}
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`aucun email ! - (Angular)
`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`4 emails - (Angular)
`);
fixture.componentRef.instance.count = 0;
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`aucun email ! - (John)
`);
});
it('with custom interpolation config', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {ten} other {{INTERPOLATION}}}')]:
'{VAR_SELECT, select, 10 {dix} other {{INTERPOLATION}}}'
});
const interpolation = ['{%', '%}'] as[string, string];
TestBed.overrideComponent(AppComp, {set: {interpolation}});
const fixture =
initWithTemplate(AppComp, `{count, select, 10 {ten} other {{% name %}}}
`);
expect(fixture.nativeElement).toHaveText(`Angular`);
});
it('inside HTML elements', () => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
'')]:
'{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
[computeMsgId('{VAR_SELECT, select, other {({INTERPOLATION})}}')]:
'{VAR_SELECT, select, other {({INTERPOLATION})}}',
[computeMsgId(
'{$START_TAG_SPAN_1}{$ICU}{$CLOSE_TAG_SPAN} - ' +
'{$START_TAG_SPAN_1}{$ICU_1}{$CLOSE_TAG_SPAN}')]:
'{$START_TAG_SPAN_1}{$ICU}{$CLOSE_TAG_SPAN} - {$START_TAG_SPAN_1}{$ICU_1}{$CLOSE_TAG_SPAN}',
});
const fixture = initWithTemplate(AppComp, `{count, plural,
=0 {no emails !}
=1 {one email }
other {{{count}} emails }
} - {name, select,
other {({{name}})}
}
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(
`aucun email ! - (Angular)
`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`4 emails - (Angular)
`);
fixture.componentRef.instance.count = 0;
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`aucun email ! - (John)
`);
});
it('inside template directives', () => {
loadTranslations({
[computeMsgId('{$START_TAG_SPAN}{$ICU}{$CLOSE_TAG_SPAN}')]:
'{$START_TAG_SPAN}{$ICU}{$CLOSE_TAG_SPAN}',
[computeMsgId('{VAR_SELECT, select, other {({INTERPOLATION})}}')]:
'{VAR_SELECT, select, other {({INTERPOLATION})}}'
});
const fixture = initWithTemplate(AppComp, `{name, select,
other {({{name}})}
}
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`(Angular)
`);
fixture.componentRef.instance.visible = false;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`
`);
});
it('inside ng-container', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, other {({INTERPOLATION})}}')]:
'{VAR_SELECT, select, other {({INTERPOLATION})}}'
});
const fixture = initWithTemplate(AppComp, `{name, select,
other {({{name}})}
} `);
expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)`);
});
it('inside ', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}')]:
'{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}'
});
const fixture = initWithTemplate(
AppComp, `
` +
`{count, select, 10 {ten} 20 {twenty} other {other}}` +
`
`);
const element = fixture.nativeElement;
expect(element).toHaveText('autre');
});
it('nested', () => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =0 {zero} other {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}',
'')]:
'{VAR_PLURAL, plural, =0 {zero} other {{INTERPOLATION} {VAR_SELECT, select, cat {chats} dog {chiens} other {animaux}}!}}'
});
const fixture = initWithTemplate(AppComp, `{count, plural,
=0 {zero}
other {{{count}} {name, select,
cat {cats}
dog {dogs}
other {animals}
}!}
}
`);
expect(fixture.nativeElement.innerHTML).toEqual(`zero
`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`4 animaux!
`);
});
it('nested with interpolations in "other" blocks', () => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!} other {other - {INTERPOLATION}}}',
'')]:
'{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {chats} dog {chiens} other {animaux}}!} other {autre - {INTERPOLATION}}}'
});
const fixture = initWithTemplate(AppComp, `{count, plural,
=0 {zero}
=2 {{{count}} {name, select,
cat {cats}
dog {dogs}
other {animals}
}!}
other {other - {{count}}}
}
`);
expect(fixture.nativeElement.innerHTML).toEqual(`zero
`);
fixture.componentRef.instance.count = 2;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`2 animaux!
`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`autre - 4
`);
});
it('should return the correct plural form for ICU expressions when using a specific locale', () => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =0 {no email} =one {one email} =few {a few emails} =other {lots of emails}}')]:
'{VAR_PLURAL, plural, =0 {no email} =one {one email} =few {a few emails} =other {lots of emails}}'
});
registerLocaleData(localeRo);
TestBed.configureTestingModule({providers: [{provide: LOCALE_ID, useValue: 'ro'}]});
// We could also use `TestBed.overrideProvider(LOCALE_ID, {useValue: 'ro'});`
const fixture = initWithTemplate(AppComp, `
{count, plural,
=0 {no email}
=one {one email}
=few {a few emails}
=other {lots of emails}
}`);
expect(fixture.nativeElement.innerHTML).toEqual('no email');
// Change detection cycle, no model changes
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email');
fixture.componentInstance.count = 3;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails');
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email');
fixture.componentInstance.count = 10;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails');
fixture.componentInstance.count = 20;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails');
fixture.componentInstance.count = 0;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email');
});
it('projection', () => {
loadTranslations({
[computeMsgId('{VAR_PLURAL, plural, =1 {one} other {at least {INTERPOLATION} .}}')]:
'{VAR_PLURAL, plural, =1 {one} other {at least {INTERPOLATION} .}}'
});
@Component({selector: 'child', template: '
'})
class Child {
}
@Component({
selector: 'parent',
template: `
{
value // i18n(ph = "blah"),
plural,
=1 {one}
other {at least {{value}} .}
} `
})
class Parent {
value = 3;
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toContain('at least');
});
it('with empty values', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {} 20 {twenty} other {other}}')]:
'{VAR_SELECT, select, 10 {} 20 {twenty} other {other}}'
});
const fixture = initWithTemplate(AppComp, `{count, select, 10 {} 20 {twenty} other {other}}`);
const element = fixture.nativeElement;
expect(element).toHaveText('other');
});
it('inside a container when creating a view via vcr.createEmbeddedView', () => {
loadTranslations({
[computeMsgId('{VAR_PLURAL, plural, =1 {ONE} other {OTHER}}')]:
'{VAR_PLURAL, plural, =1 {ONE} other {OTHER}}'
});
@Directive({
selector: '[someDir]',
})
class Dir {
constructor(
private readonly viewContainerRef: ViewContainerRef,
private readonly templateRef: TemplateRef) {}
ngOnInit() { this.viewContainerRef.createEmbeddedView(this.templateRef); }
}
@Component({
selector: 'my-cmp',
template: `
`,
})
class Cmp {
}
@Component({
selector: 'my-app',
template: `
{
count,
plural,
=1 {ONE}
other {OTHER}
}
`,
})
class App {
count = 1;
condition = true;
}
TestBed.configureTestingModule({
declarations: [App, Cmp, Dir],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('ONE
');
fixture.componentRef.instance.count = 2;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('OTHER
');
// destroy component
fixture.componentInstance.condition = false;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.textContent).toBe('');
// render it again and also change ICU case
fixture.componentInstance.condition = true;
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('ONE
');
});
it('with nested ICU expression and inside a container when creating a view via vcr.createEmbeddedView',
() => {
loadTranslations({
[computeMsgId(
'{VAR_PLURAL, plural, =1 {ONE} other {{INTERPOLATION} ' +
'{VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}')]:
'{VAR_PLURAL, plural, =1 {ONE} other {{INTERPOLATION} ' +
'{VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}'
});
let dir: Dir|null = null;
@Directive({
selector: '[someDir]',
})
class Dir {
constructor(
private readonly viewContainerRef: ViewContainerRef,
private readonly templateRef: TemplateRef) {
dir = this;
}
attachEmbeddedView() { this.viewContainerRef.createEmbeddedView(this.templateRef); }
}
@Component({
selector: 'my-cmp',
template: `
`,
})
class Cmp {
}
@Component({
selector: 'my-app',
template: `
{
count,
plural,
=1 {ONE}
other {{{count}} {name, select,
cat {cats}
dog {dogs}
other {animals}
}!}
}
`,
})
class App {
count = 1;
}
TestBed.configureTestingModule({
declarations: [App, Cmp, Dir],
});
const fixture = TestBed.createComponent(App);
fixture.componentRef.instance.count = 2;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toBe(' ');
dir !.attachEmbeddedView();
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toBe(
'2 animals!
');
fixture.componentRef.instance.count = 1;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toBe('ONE
');
});
it('with nested containers', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, A {A } B {B } other {C }}')]:
'{VAR_SELECT, select, A {A } B {B } other {C }}',
[computeMsgId('{VAR_SELECT, select, A1 {A1 } B1 {B1 } other {C1 }}')]:
'{VAR_SELECT, select, A1 {A1 } B1 {B1 } other {C1 }}',
[computeMsgId(' {$ICU} ')]: ' {$ICU} ',
});
@Component({
selector: 'comp',
template: `
{type, select, A { A } B { B } other { C }}
{type, select, A1 { A1 } B1 { B1 } other { C1 }}
`,
})
class Comp {
type = 'A';
visible = true;
isVisible() { return true; }
}
TestBed.configureTestingModule({declarations: [Comp]});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('A');
fixture.componentInstance.visible = false;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('A');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('C1');
});
it('with named interpolations', () => {
loadTranslations({
[computeMsgId(
'{VAR_SELECT, select, A {A - {PH_A}} ' +
'B {B - {PH_B}} other {other - {PH_WITH_SPACES}}}')]:
'{VAR_SELECT, select, A {A (translated) - {PH_A}} ' +
'B {B (translated) - {PH_B}} other {other (translated) - {PH_WITH_SPACES}}}',
});
@Component({
selector: 'comp',
template: `
{
type,
select,
A {A - {{ typeA // i18n(ph="PH_A") }}}
B {B - {{ typeB // i18n(ph="PH_B") }}}
other {other - {{ typeC // i18n(ph="PH WITH SPACES") }}}
}
`,
})
class Comp {
type = 'A';
typeA = 'Type A';
typeB = 'Type B';
typeC = 'Type C';
}
TestBed.configureTestingModule({declarations: [Comp]});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('A (translated) - Type A');
fixture.componentInstance.type = 'C'; // trigger "other" case
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('A (translated) - Type A');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('other (translated) - Type C');
});
it('should work inside an ngTemplateOutlet inside an ngFor', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, A {A } B {B } other {other - {PH_WITH_SPACES}}}')]:
'{VAR_SELECT, select, A {A } B {B } other {other - {PH_WITH_SPACES}}}',
[computeMsgId('{$ICU} ')]: '{$ICU} '
});
@Component({
selector: 'app',
template: `
{
type,
select,
A {A }
B {B }
other {other - {{ typeC // i18n(ph="PH WITH SPACES") }}}
}
`
})
class AppComponent {
types = ['A', 'B', 'C'];
}
TestBed.configureTestingModule({declarations: [AppComponent]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('A');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('B');
});
it('should use metadata from container element if a message is a single ICU', () => {
loadTranslations({idA: '{VAR_SELECT, select, 1 {un} other {plus d\'un}}'});
@Component({
selector: 'app',
template: `
{count, select, 1 {one} other {more than one}}
`
})
class AppComponent {
count = 2;
}
TestBed.configureTestingModule({declarations: [AppComponent]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('plus d\'un');
});
it('should support ICUs without "other" cases', () => {
loadTranslations({
idA: '{VAR_SELECT, select, 1 {un (select)} 2 {deux (select)}}',
idB: '{VAR_PLURAL, plural, =1 {un (plural)} =2 {deux (plural)}}',
});
@Component({
selector: 'app',
template: `
{count, select, 1 {one (select)} 2 {two (select)}}
-
{count, plural, =1 {one (plural)} =2 {two (plural)}}
`
})
class AppComponent {
count = 1;
}
TestBed.configureTestingModule({declarations: [AppComponent]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('un (select) - un (plural)');
fixture.componentInstance.count = 3;
fixture.detectChanges();
// there is no ICU case for count=3
expect(fixture.nativeElement.textContent.trim()).toBe('-');
fixture.componentInstance.count = 4;
fixture.detectChanges();
// there is no ICU case for count=4, making sure content is still empty
expect(fixture.nativeElement.textContent.trim()).toBe('-');
fixture.componentInstance.count = 2;
fixture.detectChanges();
// check switching to an existing case after processing an ICU without matching case
expect(fixture.nativeElement.textContent.trim()).toBe('deux (select) - deux (plural)');
fixture.componentInstance.count = 1;
fixture.detectChanges();
// check that we can go back to the first ICU case
expect(fixture.nativeElement.textContent).toBe('un (select) - un (plural)');
});
it('should support nested ICUs without "other" cases', () => {
loadTranslations({
idA: '{VAR_SELECT_1, select, A {{VAR_SELECT, select, ' +
'1 {un (select)} 2 {deux (select)}}} other {}}',
idB: '{VAR_SELECT, select, A {{VAR_PLURAL, plural, ' +
'=1 {un (plural)} =2 {deux (plural)}}} other {}}',
});
@Component({
selector: 'app',
template: `
{
type, select,
A {{count, select, 1 {one (select)} 2 {two (select)}}}
other {}
}
-
{
type, select,
A {{count, plural, =1 {one (plural)} =2 {two (plural)}}}
other {}
}
`
})
class AppComponent {
type = 'A';
count = 1;
}
TestBed.configureTestingModule({declarations: [AppComponent]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('un (select) - un (plural)');
fixture.componentInstance.count = 3;
fixture.detectChanges();
// there is no case for count=3 in nested ICU
expect(fixture.nativeElement.textContent.trim()).toBe('-');
fixture.componentInstance.count = 4;
fixture.detectChanges();
// there is no case for count=4 in nested ICU, making sure content is still empty
expect(fixture.nativeElement.textContent.trim()).toBe('-');
fixture.componentInstance.count = 2;
fixture.detectChanges();
// check switching to an existing case after processing nested ICU without matching case
expect(fixture.nativeElement.textContent.trim()).toBe('deux (select) - deux (plural)');
fixture.componentInstance.count = 1;
fixture.detectChanges();
// check that we can go back to the first case in nested ICU
expect(fixture.nativeElement.textContent).toBe('un (select) - un (plural)');
fixture.componentInstance.type = 'B';
fixture.detectChanges();
// check that nested ICU is removed if root ICU case has changed
expect(fixture.nativeElement.textContent.trim()).toBe('-');
});
it('should support ICUs with pipes', () => {
loadTranslations({
idA: '{VAR_SELECT, select, 1 {{INTERPOLATION} article} 2 {deux articles}}',
});
@Component({
selector: 'app',
template: `
{count$ | async, select, 1 {{{count$ | async}} item} 2 {two items}}
`
})
class AppComponent {
count$ = new BehaviorSubject(1);
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [AppComponent],
});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('1 article');
fixture.componentInstance.count$.next(3);
fixture.detectChanges();
// there is no ICU case for count=3, expecting empty content
expect(fixture.nativeElement.textContent.trim()).toBe('');
fixture.componentInstance.count$.next(2);
fixture.detectChanges();
// checking the second ICU case
expect(fixture.nativeElement.textContent.trim()).toBe('deux articles');
});
});
describe('should support attributes', () => {
it('text', () => {
loadTranslations({[computeMsgId('text')]: 'texte'});
const fixture = initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.innerHTML).toEqual(`
`);
});
it('interpolations', () => {
loadTranslations({[computeMsgId('hello {$INTERPOLATION}')]: 'bonjour {$INTERPOLATION}'});
const fixture =
initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.innerHTML).toEqual(`
`);
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`
`);
});
it('with pipes', () => {
loadTranslations({[computeMsgId('hello {$INTERPOLATION}')]: 'bonjour {$INTERPOLATION}'});
const fixture = initWithTemplate(
AppComp, `
`);
expect(fixture.nativeElement.innerHTML).toEqual(`
`);
});
it('multiple attributes', () => {
loadTranslations({[computeMsgId('hello {$INTERPOLATION}')]: 'bonjour {$INTERPOLATION}'});
const fixture = initWithTemplate(
AppComp,
` `);
expect(fixture.nativeElement.innerHTML)
.toEqual(` `);
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(` `);
});
it('on removed elements', () => {
loadTranslations({
[computeMsgId('text')]: 'texte',
[computeMsgId('{$START_TAG_SPAN}content{$CLOSE_TAG_SPAN}')]: 'contenu',
});
const fixture =
initWithTemplate(AppComp, `content
`);
expect(fixture.nativeElement.innerHTML).toEqual(`contenu
`);
});
it('with custom interpolation config', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}', 'm')]: 'Bonjour {$INTERPOLATION}'});
const interpolation = ['{%', '%}'] as[string, string];
TestBed.overrideComponent(AppComp, {set: {interpolation}});
const fixture =
initWithTemplate(AppComp, `
`);
const element = fixture.nativeElement.firstChild;
expect(element.title).toBe('Bonjour Angular');
});
it('in nested template', () => {
loadTranslations({[computeMsgId('Item {$INTERPOLATION}', 'm')]: 'Article {$INTERPOLATION}'});
const fixture = initWithTemplate(AppComp, `
`);
const element = fixture.nativeElement;
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
expect((child as any).innerHTML).toBe(`
`);
}
});
it('should add i18n attributes on self-closing tags', () => {
loadTranslations({[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}'});
const fixture =
initWithTemplate(AppComp, ` `);
const element = fixture.nativeElement.firstChild;
expect(element.title).toBe('Bonjour Angular');
});
it('should apply i18n attributes during second template pass', () => {
loadTranslations({[computeMsgId('Set')]: 'Set'});
@Directive({
selector: '[test]',
inputs: ['test'],
exportAs: 'dir',
})
class Dir {
}
@Component({
selector: 'other',
template: `
`
})
class Other {
}
@Component({
selector: 'blah',
template: `
`
})
class Cmp {
}
TestBed.configureTestingModule({
declarations: [Dir, Cmp, Other],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
expect(fixture.debugElement.children[0].children[0].references.ref.test).toBe('Set');
expect(fixture.debugElement.children[1].children[0].references.ref.test).toBe('Set');
});
it('with complex expressions', () => {
loadTranslations({
[computeMsgId('{$INTERPOLATION} - {$INTERPOLATION_1} - {$INTERPOLATION_2}')]:
'{$INTERPOLATION} - {$INTERPOLATION_1} - {$INTERPOLATION_2} (fr)'
});
const fixture = initWithTemplate(AppComp, `
`);
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty strings
expect(fixture.nativeElement.firstChild.title).toEqual(`ANGULAR - - (fr)`);
fixture.componentRef.instance.obj = {
a: {b: 'value 1'},
getA: () => ({b: 'value 2'}),
};
fixture.detectChanges();
expect(fixture.nativeElement.firstChild.title).toEqual(`ANGULAR - value 1 - value 2 (fr)`);
});
it('should create corresponding ng-reflect properties', () => {
@Component({
selector: 'welcome',
template: '{{ messageText }}',
})
class WelcomeComp {
@Input() messageText !: string;
}
@Component({
template: `
`
})
class App {
}
TestBed.configureTestingModule({
declarations: [App, WelcomeComp],
});
loadTranslations({
[computeMsgId('Hello')]: 'Bonjour',
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const comp = fixture.debugElement.query(By.css('welcome'));
expect(comp.attributes['messagetext']).toBe('Bonjour');
expect(comp.attributes['ng-reflect-message-text']).toBe('Bonjour');
});
it('should support i18n attributes on elements', () => {
loadTranslations({[computeMsgId('Hello', 'meaning')]: 'Bonjour'});
@Directive({selector: '[mydir]'})
class Dir {
@Input() mydir: string = '';
}
@Component({
selector: 'my-cmp',
template: `
`,
})
class Cmp {
}
TestBed.configureTestingModule({
declarations: [Cmp, Dir],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const dir = fixture.debugElement.childNodes[0].injector.get(Dir);
expect(dir.mydir).toEqual('Bonjour');
});
});
it('should work with directives and host bindings', () => {
let directiveInstances: ClsDir[] = [];
@Directive({selector: '[test]'})
class ClsDir {
@HostBinding('className')
klass = 'foo';
constructor() { directiveInstances.push(this); }
}
@Component({
selector: `my-app`,
template: `
trad: {exp1, plural,
=0 {no emails !}
=1 {one email }
other {{{exp1}} emails}
}
`
})
class MyApp {
exp1 = 1;
exp2 = 2;
}
TestBed.configureTestingModule({declarations: [ClsDir, MyApp]});
loadTranslations({
// Note that this translation switches the order of the expressions!
[computeMsgId('start {$INTERPOLATION} middle {$INTERPOLATION_1} end')]:
'début {$INTERPOLATION_1} milieu {$INTERPOLATION} fin',
[computeMsgId(
'{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} emails}}')]:
'{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} emails}}',
[computeMsgId(' trad: {$ICU} ')]: ' traduction: {$ICU} '
});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const outerDiv: HTMLElement = fixture.nativeElement.querySelector('div[outer]');
const innerDiv: HTMLElement = fixture.nativeElement.querySelector('div[inner]');
// Note that ideally we'd just compare the innerHTML here, but different browsers return
// the order of attributes differently. E.g. most browsers preserve the declaration order,
// but IE does not.
expect(outerDiv.getAttribute('title')).toBe('début 2 milieu 1 fin');
expect(outerDiv.getAttribute('class')).toBe('foo');
expect(outerDiv.textContent !.trim()).toBe('traduction: un email');
expect(innerDiv.getAttribute('class')).toBe('foo');
directiveInstances.forEach(instance => instance.klass = 'bar');
fixture.componentRef.instance.exp1 = 2;
fixture.componentRef.instance.exp2 = 3;
fixture.detectChanges();
expect(outerDiv.getAttribute('title')).toBe('début 3 milieu 2 fin');
expect(outerDiv.getAttribute('class')).toBe('bar');
expect(outerDiv.textContent !.trim()).toBe('traduction: 2 emails');
expect(innerDiv.getAttribute('class')).toBe('bar');
});
it('should handle i18n attribute with directive inputs', () => {
let calledTitle = false;
let calledValue = false;
@Component({selector: 'my-comp', template: ''})
class MyComp {
t !: string;
@Input()
get title() { return this.t; }
set title(title) {
calledTitle = true;
this.t = title;
}
@Input()
get value() { return this.val; }
set value(value: string) {
calledValue = true;
this.val = value;
}
val !: string;
}
TestBed.configureTestingModule({declarations: [AppComp, MyComp]});
loadTranslations({
[computeMsgId('Hello {$INTERPOLATION}')]: 'Bonjour {$INTERPOLATION}',
[computeMsgId('works')]: 'fonctionne',
});
const fixture = initWithTemplate(
AppComp,
` `);
fixture.detectChanges();
const directive = fixture.debugElement.children[0].injector.get(MyComp);
expect(calledValue).toEqual(true);
expect(calledTitle).toEqual(true);
expect(directive.value).toEqual(`Bonjour Angular`);
expect(directive.title).toEqual(`fonctionne`);
});
it('should support adding/moving/removing nodes', () => {
loadTranslations({
[computeMsgId(
'{$START_TAG_DIV2}{$CLOSE_TAG_DIV2}' +
'{$START_TAG_DIV3}{$CLOSE_TAG_DIV3}' +
'{$START_TAG_DIV4}{$CLOSE_TAG_DIV4}' +
'{$START_TAG_DIV5}{$CLOSE_TAG_DIV5}' +
'{$START_TAG_DIV6}{$CLOSE_TAG_DIV6}' +
'{$START_TAG_DIV7}{$CLOSE_TAG_DIV7}' +
'{$START_TAG_DIV8}{$CLOSE_TAG_DIV8}')]: '{$START_TAG_DIV2}{$CLOSE_TAG_DIV2}' +
'{$START_TAG_DIV8}{$CLOSE_TAG_DIV8}' +
'{$START_TAG_DIV4}{$CLOSE_TAG_DIV4}' +
'{$START_TAG_DIV5}{$CLOSE_TAG_DIV5}Bonjour monde' +
'{$START_TAG_DIV3}{$CLOSE_TAG_DIV3}' +
'{$START_TAG_DIV7}{$CLOSE_TAG_DIV7}'
});
const fixture = initWithTemplate(AppComp, `
`);
expect(fixture.nativeElement.innerHTML)
.toEqual(
``);
});
describe('projection', () => {
it('should project the translations', () => {
@Component({selector: 'child', template: '
'})
class Child {
}
@Component({
selector: 'parent',
template: `
I am projected from
{{name}}
`
})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Child of {$INTERPOLATION}')]: 'Enfant de {$INTERPOLATION}',
[computeMsgId(
'{$START_TAG_CHILD}I am projected from' +
' {$START_BOLD_TEXT}{$INTERPOLATION}{$START_TAG_REMOVE_ME_1}{$CLOSE_TAG_REMOVE_ME_1}{$CLOSE_BOLD_TEXT}' +
'{$START_TAG_REMOVE_ME_2}{$CLOSE_TAG_REMOVE_ME_2}' +
'{$CLOSE_TAG_CHILD}' +
'{$START_TAG_REMOVE_ME_3}{$CLOSE_TAG_REMOVE_ME_3}')]:
'{$START_TAG_CHILD}Je suis projeté depuis {$START_BOLD_TEXT}{$INTERPOLATION}{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_CHILD}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`Je suis projeté depuis Parent
`);
});
it('should project a translated i18n block', () => {
@Component({selector: 'child', template: '
'})
class Child {
}
@Component({
selector: 'parent',
template: `
I am projected from {{name}}
`
})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Child of {$INTERPOLATION}')]: 'Enfant de {$INTERPOLATION}',
[computeMsgId('I am projected from {$INTERPOLATION}')]:
'Je suis projeté depuis {$INTERPOLATION}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`Je suis projeté depuis Parent
`);
// it should be able to render a new component with the same template code
const fixture2 = TestBed.createComponent(Parent);
fixture2.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(fixture2.nativeElement.innerHTML);
fixture2.componentRef.instance.name = 'Parent 2';
fixture2.detectChanges();
expect(fixture2.nativeElement.innerHTML)
.toEqual(
`Je suis projeté depuis Parent 2
`);
// The first fixture should not have changed
expect(fixture.nativeElement.innerHTML).not.toEqual(fixture2.nativeElement.innerHTML);
});
it('should re-project translations when multiple projections', () => {
@Component({selector: 'grand-child', template: '
'})
class GrandChild {
}
@Component(
{selector: 'child', template: ' '})
class Child {
}
@Component({selector: 'parent', template: `Hello World! `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]});
loadTranslations({
[computeMsgId('{$START_BOLD_TEXT}Hello{$CLOSE_BOLD_TEXT} World!')]:
'{$START_BOLD_TEXT}Bonjour{$CLOSE_BOLD_TEXT} monde!'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual('Bonjour monde!
');
});
it('should be able to remove projected placeholders', () => {
@Component({selector: 'grand-child', template: '
'})
class GrandChild {
}
@Component(
{selector: 'child', template: ' '})
class Child {
}
@Component({selector: 'parent', template: `Hello World! `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]});
loadTranslations(
{[computeMsgId('{$START_BOLD_TEXT}Hello{$CLOSE_BOLD_TEXT} World!')]: 'Bonjour monde!'});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual('Bonjour monde!
');
});
it('should project translations with selectors', () => {
@Component({selector: 'child', template: ` `})
class Child {
}
@Component({
selector: 'parent',
template: `
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('{$START_TAG_SPAN}{$CLOSE_TAG_SPAN}{$START_TAG_SPAN_1}{$CLOSE_TAG_SPAN}')]:
'{$START_TAG_SPAN}Contenu{$CLOSE_TAG_SPAN}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual('Contenu ');
});
it('should project content in i18n blocks', () => {
@Component({
selector: 'child',
template: `Content projected from
`
})
class Child {
}
@Component({selector: 'parent', template: `{{name}} `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Content projected from {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}')]:
'Contenu projeté depuis {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`Contenu projeté depuis Parent
`);
fixture.componentRef.instance.name = 'Parent component';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`Contenu projeté depuis Parent component
`);
});
it('should project content in i18n blocks with placeholders', () => {
@Component({
selector: 'child',
template: `Content projected from
`
})
class Child {
}
@Component({selector: 'parent', template: `{{name}} `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Content projected from {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}')]:
'{$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT} a projeté le contenu'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`Parent a projeté le contenu
`);
});
it('should project translated content in i18n blocks', () => {
@Component(
{selector: 'child', template: `Child content
`})
class Child {
}
@Component({selector: 'parent', template: `and projection from {{name}} `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Child content {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}')]:
'Contenu enfant {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}',
[computeMsgId('and projection from {$INTERPOLATION}')]:
'et projection depuis {$INTERPOLATION}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`Contenu enfant et projection depuis Parent
`);
});
it('should project bare ICU expressions', () => {
loadTranslations({
[computeMsgId('{VAR_PLURAL, plural, =1 {one} other {at least {INTERPOLATION} .}}')]:
'{VAR_PLURAL, plural, =1 {one} other {at least {INTERPOLATION} .}}'
});
@Component({selector: 'child', template: '
'})
class Child {
}
@Component({
selector: 'parent',
template: `
{
value // i18n(ph = "blah"),
plural,
=1 {one}
other {at least {{value}} .}
} `
})
class Parent {
value = 3;
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toContain('at least');
});
it('should project ICUs in i18n blocks', () => {
@Component(
{selector: 'child', template: `Child content
`})
class Child {
}
@Component({
selector: 'parent',
template:
`and projection from {name, select, angular {Angular} other {{{name}}}} `
})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('{VAR_SELECT, select, angular {Angular} other {{INTERPOLATION}}}')]:
'{VAR_SELECT, select, angular {Angular} other {{INTERPOLATION}}}',
[computeMsgId('Child content {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}')]:
'Contenu enfant {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}',
[computeMsgId('and projection from {$ICU}')]: 'et projection depuis {$ICU}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`Contenu enfant et projection depuis Parent
`);
fixture.componentRef.instance.name = 'angular';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`Contenu enfant et projection depuis Angular
`);
});
it(`shouldn't project deleted projections in i18n blocks`, () => {
@Component(
{selector: 'child', template: `Child content
`})
class Child {
}
@Component({selector: 'parent', template: `and projection from {{name}} `})
class Parent {
name: string = 'Parent';
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
loadTranslations({
[computeMsgId('Child content {$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT}')]:
'Contenu enfant',
[computeMsgId('and projection from {$INTERPOLATION}')]:
'et projection depuis {$INTERPOLATION}'
});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`Contenu enfant
`);
});
it('should display/destroy projected i18n content', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, A {A} B {B} other {other}}')]:
'{VAR_SELECT, select, A {A} B {B} other {other}}'
});
@Component({
selector: 'app',
template: `
( )
`
})
class MyContentApp {
}
@Component({
selector: 'my-app',
template: `
{type, select, A {A} B {B} other {other}}
`
})
class MyApp {
type = 'A';
condition = true;
}
TestBed.configureTestingModule({declarations: [MyApp, MyContentApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('(A)');
// change `condition` to remove
fixture.componentInstance.condition = false;
fixture.detectChanges();
// should not contain 'A'
expect(fixture.nativeElement.textContent).toBe('');
// display again
fixture.componentInstance.type = 'B';
fixture.componentInstance.condition = true;
fixture.detectChanges();
// expect that 'B' is now displayed
expect(fixture.nativeElement.textContent).toContain('(B)');
});
});
describe('queries', () => {
function toHtml(element: Element): string {
return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '')
.replace(//g, '');
}
it('detached nodes should still be part of query', () => {
@Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'})
class TextDirective {
// TODO(issue/24571): remove '!'.
text !: string;
constructor() {}
}
@Component({selector: 'div-query', template: ' '})
class DivQuery {
// TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef, {static: true}) template !: TemplateRef;
// TODO(issue/24571): remove '!'.
@ViewChild('vc', {read: ViewContainerRef, static: true})
vc !: ViewContainerRef;
// TODO(issue/24571): remove '!'.
@ContentChildren(TextDirective, {descendants: true})
query !: QueryList;
create() { this.vc.createEmbeddedView(this.template); }
destroy() { this.vc.clear(); }
}
TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]});
loadTranslations({
[computeMsgId(
'{$START_TAG_NG_TEMPLATE}{$START_TAG_DIV_1}' +
'{$START_TAG_DIV}' +
'{$START_TAG_SPAN}Content{$CLOSE_TAG_SPAN}' +
'{$CLOSE_TAG_DIV}' +
'{$CLOSE_TAG_DIV}{$CLOSE_TAG_NG_TEMPLATE}')]:
'{$START_TAG_NG_TEMPLATE}Contenu{$CLOSE_TAG_NG_TEMPLATE}'
});
const fixture = initWithTemplate(AppComp, `
`);
const q = fixture.debugElement.children[0].references.q;
expect(q.query.length).toEqual(0);
// Create embedded view
q.create();
fixture.detectChanges();
expect(q.query.length).toEqual(1);
expect(toHtml(fixture.nativeElement))
.toEqual(`Contenu `);
// Disable ng-if
fixture.componentInstance.visible = false;
fixture.detectChanges();
expect(q.query.length).toEqual(0);
expect(toHtml(fixture.nativeElement))
.toEqual(`Contenu `);
});
});
it('should reflect lifecycle hook changes in text interpolations in i18n block', () => {
@Directive({selector: 'input'})
class InputsDir {
constructor(private elementRef: ElementRef) {}
ngOnInit() { this.elementRef.nativeElement.value = 'value set in Directive.ngOnInit'; }
}
@Component({
template: `
{{myinput.value}}
`
})
class App {
}
TestBed.configureTestingModule({declarations: [App, InputsDir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('value set in Directive.ngOnInit');
});
it('should reflect lifecycle hook changes in text interpolations in i18n attributes', () => {
@Directive({selector: 'input'})
class InputsDir {
constructor(private elementRef: ElementRef) {}
ngOnInit() { this.elementRef.nativeElement.value = 'value set in Directive.ngOnInit'; }
}
@Component({
template: `
`
})
class App {
}
TestBed.configureTestingModule({declarations: [App, InputsDir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('div').title)
.toContain('value set in Directive.ngOnInit');
});
it('should not alloc expando slots when there is no new variable to create', () => {
loadTranslations({
[computeMsgId('{$START_TAG_DIV} Some content {$CLOSE_TAG_DIV}')]:
'{$START_TAG_DIV} Some content {$CLOSE_TAG_DIV}',
[computeMsgId(
'{$START_TAG_SPAN_1}{$ICU}{$CLOSE_TAG_SPAN} - {$START_TAG_SPAN_1}{$ICU_1}{$CLOSE_TAG_SPAN}')]:
'{$START_TAG_SPAN_1}{$ICU}{$CLOSE_TAG_SPAN} - {$START_TAG_SPAN_1}{$ICU_1}{$CLOSE_TAG_SPAN}',
});
@Component({
template: `
Button label
`
})
class ContentElementDialog {
data = false;
}
TestBed.configureTestingModule({declarations: [DialogDir, CloseBtn, ContentElementDialog]});
const fixture = TestBed.createComponent(ContentElementDialog);
fixture.detectChanges();
// Remove the reflect attribute, because the attribute order in innerHTML
// isn't guaranteed in different browsers so it could throw off our assertions.
const button = fixture.nativeElement.querySelector('button');
button.removeAttribute('ng-reflect-dialog-result');
expect(fixture.nativeElement.innerHTML).toEqual(`
Button label `);
});
});
function initWithTemplate(compType: Type, template: string) {
TestBed.overrideComponent(compType, {set: {template}});
const fixture = TestBed.createComponent(compType);
fixture.detectChanges();
return fixture;
}
@Component({selector: 'app-comp', template: ``})
class AppComp {
name = `Angular`;
visible = true;
count = 0;
}
@Component({
selector: 'app-comp-with-whitespaces',
template: ``,
preserveWhitespaces: true,
})
class AppCompWithWhitespaces {
}
@Directive({
selector: '[tplRef]',
})
class DirectiveWithTplRef {
constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {}
ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); }
}
@Pipe({name: 'uppercase'})
class UppercasePipe implements PipeTransform {
transform(value: string) { return value.toUpperCase(); }
}
@Directive({selector: `[dialog]`})
export class DialogDir {
}
@Directive({selector: `button[close]`, host: {'[title]': 'name'}})
export class CloseBtn {
@Input('close') dialogResult: any;
name: string = 'Close dialog';
}