/**
* @license
* Copyright Google LLC 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, DOCUMENT, registerLocaleData} from '@angular/common';
import localeEs from '@angular/common/locales/es';
import localeRo from '@angular/common/locales/ro';
import {computeMsgId} from '@angular/compiler';
import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core';
import {setDelayProjection} from '@angular/core/src/render3/instructions/projection';
import {TestBed} from '@angular/core/testing';
import {clearTranslations, loadTranslations} from '@angular/localize';
import {By, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
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, `
`})
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_TEMPLATE} Four {$CLOSE_TAG_NG_TEMPLATE}' +
'{$START_TAG_NG_CONTAINER_1}{$CLOSE_TAG_NG_CONTAINER}')]:
'{$START_TAG_NG_CONTAINER} Une {$CLOSE_TAG_NG_CONTAINER}' +
'{$START_TAG_DIV} Deux {$CLOSE_TAG_DIV}' +
'{$START_TAG_SPAN} Trois {$CLOSE_TAG_SPAN}' +
'{$START_TAG_NG_TEMPLATE} Quatre {$CLOSE_TAG_NG_TEMPLATE}' +
'{$START_TAG_NG_CONTAINER_1}{$CLOSE_TAG_NG_CONTAINER}'
});
const fixture = initWithTemplate(AppComp, `
One
Two
Three Four
`);
expect(fixture.nativeElement.textContent).toBe(' Une Deux Trois Quatre ');
});
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, `
One
Two
Three
`);
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, `
HelloHello
`);
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, `
`);
});
it('should return the correct plural form for ICU expressions when using "ro" locale', () => {
// The "ro" locale has a complex plural function that can handle muliple options
// (and string inputs)
//
// function plural(n: number): number {
// let i = Math.floor(Math.abs(n)), v = n.toString().replace(/^[^.]*\.?/, '').length;
// if (i === 1 && v === 0) return 1;
// if (!(v === 0) || n === 0 ||
// !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19)
// return 3;
// return 5;
// }
//
// Compare this to the "es" locale in the next test
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(`should return the correct plural form for ICU expressions when using "es" locale`, () => {
// The "es" locale has a simple plural function that can only handle a few options
// (and not string inputs)
//
// function plural(n: number): number {
// if (n === 1) return 1;
// return 5;
// }
//
// Compare this to the "ro" locale in the previous test
const icuMessage = '{VAR_PLURAL, plural, =0 {no email} =one ' +
'{one email} =few {a few emails} =other {lots of emails}}';
loadTranslations({[computeMsgId(icuMessage)]: icuMessage});
registerLocaleData(localeEs);
TestBed.configureTestingModule({providers: [{provide: LOCALE_ID, useValue: 'es'}]});
// We could also use `TestBed.overrideProvider(LOCALE_ID, {useValue: 'es'});`
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('lots of 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('lots of 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: `
');
});
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: `
`
})
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: `
'})
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 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, `
Content
`);
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`);
});
});
describe('invalid translations handling', () => {
it('should throw in case invalid ICU is present in a template', () => {
// Error message is produced by Compiler.
expect(() => initWithTemplate(AppComp, '{count, select, 10 {ten} other {other}'))
.toThrowError(
/Invalid ICU message. Missing '}'. \("{count, select, 10 {ten} other {other}\[ERROR ->\]"\)/);
});
it('should throw in case invalid ICU is present in translation', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {ten} other {other}}')]:
// Missing "}" at the end of translation.
'{VAR_SELECT, select, 10 {dix} other {autre}'
});
// Error message is produced at runtime.
expect(() => initWithTemplate(AppComp, '{count, select, 10 {ten} other {other}}'))
.toThrowError(
/Unable to parse ICU expression in "{�0�, select, 10 {dix} other {autre}" message./);
});
it('should throw in case unescaped curly braces are present in a template', () => {
// Error message is produced by Compiler.
expect(() => initWithTemplate(AppComp, 'Text { count }'))
.toThrowError(
/Do you have an unescaped "{" in your template\? Use "{{ '{' }}"\) to escape it/);
});
it('should throw in case curly braces are added into translation', () => {
loadTranslations({
// Curly braces which were not present in a template were added into translation.
[computeMsgId('Text')]: 'Text { count }',
});
expect(() => initWithTemplate(AppComp, '
Text
'))
.toThrowError(/Unable to parse ICU expression in "Text { count }" message./);
});
});
it('should handle extra HTML in translation as plain text', () => {
loadTranslations({
// Translation contains HTML tags that were not present in original message.
[computeMsgId('Text')]: 'Text