/** * @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 '@angular/localize'; import {registerLocaleData} from '@angular/common'; import localeRo from '@angular/common/locales/ro'; import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize, Pipe, PipeTransform} from '@angular/core'; import {setDelayProjection} from '@angular/core/src/render3/instructions/projection'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { beforeEach(() => { TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef, UppercasePipe]}); }); afterEach(() => { setDelayProjection(false); }); it('should translate text', () => { ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); const fixture = initWithTemplate(AppComp, `
text
`); expect(fixture.nativeElement.innerHTML).toEqual(`
texte
`); }); it('should support interpolations', () => { ɵi18nConfigureLocalize( {translations: {'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', () => { ɵi18nConfigureLocalize({ translations: { ' Hello {$userName}! Emails: {$amountOfEmailsReceived} ': ' Bonjour {$userName}! Emails: {$amountOfEmailsReceived} ' } }); 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', () => { ɵi18nConfigureLocalize({translations: {'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 ɵi18nConfigureLocalize({translations: {'text ||': 'texte ||'}}); const fixture = initWithTemplate(AppCompWithWhitespaces, `
text |&ngsp;|
`); expect(fixture.nativeElement.innerHTML).toEqual(`
texte | |
`); }); it('should support interpolations with complex expressions', () => { ɵi18nConfigureLocalize({ translations: {'{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)'} }); const fixture = initWithTemplate(AppComp, `
{{ name | uppercase }} - {{ obj?.a?.b }}
`); expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - (fr)
`); fixture.componentRef.instance.obj = {a: {b: 'value'}}; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - value (fr)
`); }); it('should support elements', () => { ɵi18nConfigureLocalize({ translations: { 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': 'Bonjour {$startTagSpan}monde{$closeTagSpan} et {$startTagDiv}univers{$closeTagDiv}!' } }); const fixture = initWithTemplate( AppComp, `
Hello world and
universe
!
`); expect(fixture.nativeElement.innerHTML) .toEqual(`
Bonjour monde et
univers
!
`); }); it('should support removing elements', () => { ɵi18nConfigureLocalize({ translations: { 'Hello {$startBoldText}my{$closeBoldText}{$startTagSpan}world{$closeTagSpan}': 'Bonjour {$startTagSpan}monde{$closeTagSpan}' } }); const fixture = initWithTemplate(AppComp, `
Hello myworld
!
`); expect(fixture.nativeElement.innerHTML) .toEqual(`
Bonjour monde
!
`); }); it('should support moving elements', () => { ɵi18nConfigureLocalize({ translations: { 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': 'Bonjour {$startTagDiv}univers{$closeTagDiv} et {$startTagSpan}monde{$closeTagSpan}!' } }); const fixture = initWithTemplate( AppComp, `
Hello world and
universe
!
`); expect(fixture.nativeElement.innerHTML) .toEqual(`
Bonjour
univers
et monde!
`); }); it('should support template directives', () => { ɵi18nConfigureLocalize({ translations: { 'Content: {$startTagDiv}before{$startTagSpan}middle{$closeTagSpan}after{$closeTagDiv}!': 'Contenu: {$startTagDiv}avant{$startTagSpan}milieu{$closeTagSpan}après{$closeTagDiv}!' } }); const fixture = initWithTemplate( AppComp, `
Content:
beforemiddleafter
!
`); expect(fixture.nativeElement.innerHTML) .toEqual(`
Contenu:
avantmilieuaprès
!
`); fixture.componentRef.instance.visible = false; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(`
Contenu: !
`); }); it('should support multiple i18n blocks', () => { ɵi18nConfigureLocalize({ translations: { 'trad {$interpolation}': 'traduction {$interpolation}', 'start {$interpolation} middle {$interpolation_1} end': 'start {$interpolation_1} middle {$interpolation} end', '{$startTagC}trad{$closeTagC}{$startTagD}{$closeTagD}{$startTagE}{$closeTagE}': '{$startTagE}{$closeTagE}{$startTagC}traduction{$closeTagC}' } }); const fixture = initWithTemplate(AppComp, `
trad {{name}} hello trad
`); expect(fixture.nativeElement.innerHTML) .toEqual( `
traduction Angular hello traduction
`); }); it('should support multiple sibling i18n blocks', () => { ɵi18nConfigureLocalize({ translations: { 'Section 1': 'Section un', 'Section 2': 'Section deux', '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', () => { ɵi18nConfigureLocalize({ translations: { 'Section 1': 'Section un', 'Section 2': 'Section deux', 'Section 3': 'Section trois', } }); const fixture = initWithTemplate(AppComp, ` `); expect(fixture.nativeElement.innerHTML) .toEqual( ``); }); it('should properly escape quotes in content', () => { ɵi18nConfigureLocalize({ translations: { '\'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', () => { ɵi18nConfigureLocalize({translations: {'Item {$interpolation}': 'Article {$interpolation}'}}); const fixture = initWithTemplate(AppComp, `
Item {{ id }}
`); 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', () => { ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); const fixture = initWithTemplate(AppComp, `
Hello {{ name }}
`); expect(fixture.nativeElement.firstChild).toHaveText('Bonjour Angular'); }); it('should work correctly with event listeners', () => { ɵi18nConfigureLocalize({translations: {'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); }); describe('ng-container and ng-template support', () => { it('should support ng-container', () => { ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); const fixture = initWithTemplate(AppComp, `text`); expect(fixture.nativeElement.innerHTML).toEqual(`texte`); }); it('should handle single translation message within ng-template', () => { ɵi18nConfigureLocalize( {translations: {'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)', () => { ɵi18nConfigureLocalize({ translations: { '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}' } }); 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)', () => { ɵi18nConfigureLocalize({ translations: { '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}' } }); 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)', () => { // Note: for some reason keeping this key inline causes clang to reformat the entire file // in a very weird way. Keeping it separated like this seems to make it happy. const key = '{$startTagNgTemplate}Hello {$interpolation}{$closeTagNgTemplate}' + '{$startTagNgContainer}Bye {$interpolation}{$closeTagNgContainer}'; ɵi18nConfigureLocalize({ translations: { [key]: '{$startTagNgTemplate}Hej {$interpolation}{$closeTagNgTemplate}{$startTagNgContainer}Vi ses {$interpolation}{$closeTagNgContainer}' } }); 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', () => { ɵi18nConfigureLocalize({ translations: { '{$startTagSpan} Hello - 1 {$closeTagSpan}{$startTagSpan_1} Hello - 2 {$startTagSpan_1} Hello - 3 {$startTagSpan_1} Hello - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Hello - 5 {$closeTagSpan}': '{$startTagSpan} Bonjour - 1 {$closeTagSpan}{$startTagSpan_1} Bonjour - 2 {$startTagSpan_1} Bonjour - 3 {$startTagSpan_1} Bonjour - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Bonjour - 5 {$closeTagSpan}' } }); 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', () => { ɵi18nConfigureLocalize({ translations: { '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}' } }); 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 ', () => { @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', () => { ɵi18nConfigureLocalize({ translations: { '{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', () => { ɵi18nConfigureLocalize({ translations: { '{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', () => { ɵi18nConfigureLocalize({ translations: { '{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}}}', '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' } }); 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', () => { ɵi18nConfigureLocalize({ translations: { '{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', () => { ɵi18nConfigureLocalize({ translations: { '{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}}}', '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' } }); 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', () => { ɵi18nConfigureLocalize({ translations: { '{VAR_SELECT, select, other {(name)}}': '{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', () => { ɵi18nConfigureLocalize({ translations: { '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' } }); const fixture = initWithTemplate(AppComp, `{name, select, other {({{name}})} }`); expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)`); }); it('inside ', () => { ɵi18nConfigureLocalize({ translations: { '{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', () => { ɵi18nConfigureLocalize({ translations: { '{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 {chients} 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', () => { // Note: for some reason long string causing clang to reformat the entire file. const key = '{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, ' + 'cat {cats} dog {dogs} other {animals}}!} other {other - {INTERPOLATION}}}'; const translation = '{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, ' + 'cat {chats} dog {chients} other {animaux}}!} other {other - {INTERPOLATION}}}'; ɵi18nConfigureLocalize({translations: {[key]: translation}}); 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(`
other - 4
`); }); it('should return the correct plural form for ICU expressions when using a specific locale', () => { 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', () => { @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]}); ɵi18nConfigureLocalize({translations: {}}); const fixture = TestBed.createComponent(Parent); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toContain('at least'); }); it('with empty values', () => { 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', () => { @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', () => { 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', () => { @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', () => { @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 - Type A'); fixture.componentInstance.type = 'C'; // trigger "other" case fixture.detectChanges(); expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('A - Type A'); expect(fixture.debugElement.nativeElement.innerHTML).toContain('other - Type C'); }); it('should work inside an ngTemplateOutlet inside an ngFor', () => { @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'); }); }); describe('should support attributes', () => { it('text', () => { ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); const fixture = initWithTemplate(AppComp, `
`); expect(fixture.nativeElement.innerHTML).toEqual(`
`); }); it('interpolations', () => { ɵi18nConfigureLocalize( {translations: {'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', () => { ɵi18nConfigureLocalize( {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); const fixture = initWithTemplate( AppComp, `
`); expect(fixture.nativeElement.innerHTML).toEqual(`
`); }); it('multiple attributes', () => { ɵi18nConfigureLocalize( {translations: {'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', () => { ɵi18nConfigureLocalize( {translations: {'text': 'texte', '{$startTagSpan}content{$closeTagSpan}': 'contenu'}}); const fixture = initWithTemplate(AppComp, `
content
`); expect(fixture.nativeElement.innerHTML).toEqual(`
contenu
`); }); it('with custom interpolation config', () => { ɵi18nConfigureLocalize( {translations: {'Hello {$interpolation}': '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', () => { ɵi18nConfigureLocalize({translations: {'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 as any).innerHTML).toBe(`
`); } }); it('should add i18n attributes on self-closing tags', () => { ɵi18nConfigureLocalize( {translations: {'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', () => { @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('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]}); ɵi18nConfigureLocalize({ translations: { // Not that this translation switches the order of the expressions! 'start {$interpolation} middle {$interpolation_1} end': 'début {$interpolation_1} milieu {$interpolation} fin', '{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}}', ' trad: {$icu} ': ' traduction: {$icu} ' } }); const fixture = TestBed.createComponent(MyApp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual( `
traduction: un email ` + `
`); directiveInstances.forEach(instance => instance.klass = 'bar'); fixture.componentRef.instance.exp1 = 2; fixture.componentRef.instance.exp2 = 3; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual( `
traduction: 2 emails ` + `
`); }); 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]}); ɵi18nConfigureLocalize({ translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}', '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', () => { ɵi18nConfigureLocalize({ translations: { '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv3}{$closeTagDiv3}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}{$startTagDiv6}{$closeTagDiv6}{$startTagDiv7}{$closeTagDiv7}{$startTagDiv8}{$closeTagDiv8}': '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv8}{$closeTagDiv8}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}Bonjour monde{$startTagDiv3}{$closeTagDiv3}{$startTagDiv7}{$closeTagDiv7}' } }); const fixture = initWithTemplate(AppComp, `
`); expect(fixture.nativeElement.innerHTML) .toEqual( `
Bonjour monde
`); }); 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]}); ɵi18nConfigureLocalize({ translations: { 'Child of {$interpolation}': 'Enfant de {$interpolation}', '{$startTagChild}I am projected from {$startBoldText}{$interpolation}{$startTagRemoveMe_1}{$closeTagRemoveMe_1}{$closeBoldText}{$startTagRemoveMe_2}{$closeTagRemoveMe_2}{$closeTagChild}{$startTagRemoveMe_3}{$closeTagRemoveMe_3}': '{$startTagChild}Je suis projeté depuis {$startBoldText}{$interpolation}{$closeBoldText}{$closeTagChild}' } }); 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]}); ɵi18nConfigureLocalize({ translations: { 'Child of {$interpolation}': 'Enfant de {$interpolation}', '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]}); ɵi18nConfigureLocalize({ translations: { '{$startBoldText}Hello{$closeBoldText} World!': '{$startBoldText}Bonjour{$closeBoldText} 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]}); ɵi18nConfigureLocalize( {translations: {'{$startBoldText}Hello{$closeBoldText} 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]}); ɵi18nConfigureLocalize({ translations: { '{$startTagSpan}{$closeTagSpan}{$startTagSpan_1}{$closeTagSpan}': '{$startTagSpan}Contenu{$closeTagSpan}' } }); 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]}); ɵi18nConfigureLocalize({ translations: { 'Content projected from {$startTagNgContent}{$closeTagNgContent}': 'Contenu projeté depuis {$startTagNgContent}{$closeTagNgContent}' } }); 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]}); ɵi18nConfigureLocalize({ translations: { 'Content projected from {$startTagNgContent}{$closeTagNgContent}': '{$startTagNgContent}{$closeTagNgContent} 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]}); ɵi18nConfigureLocalize({ translations: { 'Child content {$startTagNgContent}{$closeTagNgContent}': 'Contenu enfant {$startTagNgContent}{$closeTagNgContent}', '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', () => { @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]}); ɵi18nConfigureLocalize({translations: {}}); 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]}); ɵi18nConfigureLocalize({ translations: { 'Child content {$startTagNgContent}{$closeTagNgContent}': 'Contenu enfant {$startTagNgContent}{$closeTagNgContent}', '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]}); ɵi18nConfigureLocalize({ translations: { 'Child content {$startTagNgContent}{$closeTagNgContent}': 'Contenu enfant', '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', () => { @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]}); ɵi18nConfigureLocalize({ translations: { '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' } }); 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`); }); }); it('should not alloc expando slots when there is no new variable to create', () => { @Component({ template: `
Some content
` }) class ContentElementDialog { data = false; } TestBed.configureTestingModule({declarations: [DialogDir, CloseBtn, ContentElementDialog]}); const fixture = TestBed.createComponent(ContentElementDialog); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(`
`); }); }); 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'; }