/** * @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 myworld
!
`); 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:
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', () => { 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, `
trad {{name}} hello trad
`); expect(fixture.nativeElement.innerHTML) .toEqual( `
traduction Angular hello traduction
`); }); 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, ` `); expect(fixture.nativeElement.innerHTML) .toEqual( ``); }); 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, `
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', () => { 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, `
One
Two
Three
`); 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, `
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, `
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( `
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]}); 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, `
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 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: `
Some content
` }) 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(`
`); }); }); 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'; }