diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts new file mode 100644 index 0000000000..f63ed86800 --- /dev/null +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -0,0 +1,964 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, ContentChild, ContentChildren, Directive, HostBinding, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +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]}); + }); + + 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 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 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 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'); + } + }); + }); + + 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 {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{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 {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{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!
`); + }); + }); + + 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('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 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: { + 'start {$interpolation} middle {$interpolation_1} end': + 'début {$interpolation_1} milieu {$interpolation} fin', + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} 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 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!
'); + }); + + // FW-1319 Runtime i18n should be able to remove projected placeholders + xit('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!
'); + }); + + // FW-1312: Wrong i18n code generated by the compiler when the template has 2 empty `span` + xit('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'); + }); + }); + + 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) template !: TemplateRef; + + // TODO(issue/24571): remove '!'. + @ViewChild('vc', {read: ViewContainerRef}) + 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`); + }); + }); +}); + +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; +} + +@Directive({ + selector: '[tplRef]', +}) +class DirectiveWithTplRef { + constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} + ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } +} diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts deleted file mode 100644 index af09a356a7..0000000000 --- a/packages/core/test/i18n_integration_spec.ts +++ /dev/null @@ -1,640 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; -import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {onlyInIvy} from '@angular/private/testing'; - -@Directive({ - selector: '[tplRef]', -}) -class DirectiveWithTplRef { - constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} - ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } -} - -@Component({selector: 'my-comp', template: ''}) -class MyComp { - name = 'John'; - items = ['1', '2', '3']; - obj = {a: {b: 'value'}}; - visible = true; - age = 20; - count = 2; - otherLabel = 'other label'; - clicks = 0; - - onClick() { this.clicks++; } -} - -const TRANSLATIONS: any = { - 'one': 'un', - 'two': 'deux', - 'more than two': 'plus que deux', - 'ten': 'dix', - 'twenty': 'vingt', - 'other': 'autres', - 'Hello': 'Bonjour', - 'Hello {$interpolation}': 'Bonjour {$interpolation}', - 'Bye': 'Au revoir', - 'Item {$interpolation}': 'Article {$interpolation}', - '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"', - 'My logo': 'Mon logo', - '{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)', - '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': - '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}', - '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': - '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$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}', - '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': - '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autres}}', - '{VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}': - '{VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}', - '{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}': - '{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}', - '{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}': - '{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}', - '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': - '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' -}; - -const getFixtureWithOverrides = (overrides = {}) => { - TestBed.overrideComponent(MyComp, {set: overrides}); - const fixture = TestBed.createComponent(MyComp); - fixture.detectChanges(); - return fixture; -}; - -onlyInIvy('Ivy i18n logic').describe('i18n', function() { - - beforeEach(() => { - ɵi18nConfigureLocalize({translations: TRANSLATIONS}); - TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]}); - }); - - describe('attributes', () => { - it('should translate static attributes', () => { - const title = 'Hello'; - const template = `
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour'); - }); - - it('should support interpolation', () => { - const title = 'Hello {{ name }}'; - const template = `
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const title = 'Hello {% name %}'; - const template = `
`; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should correctly bind to context in nested template', () => { - const title = 'Item {{ id }}'; - const template = ` -
-
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect((child as any).innerHTML).toBe(`
`); - } - }); - - it('should work correctly when placed on i18n root node', () => { - const title = 'Hello {{ name }}'; - const content = 'Hello'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - expect(element).toHaveText('Bonjour'); - }); - - it('should add i18n attributes on self-closing tags', () => { - const title = 'Hello {{ name }}'; - const template = ``; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - }); - - describe('nested nodes', () => { - it('should handle static content', () => { - const content = 'Hello'; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour'); - }); - - it('should support interpolation', () => { - const content = 'Hello {{ name }}'; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const content = 'Hello {% name %}'; - const template = `
${content}
`; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolations with complex expressions', () => { - const template = `
{{ name | uppercase }} - {{ obj?.a?.b }}
`; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('JOHN - value (fr)'); - }); - - it('should properly escape quotes in content', () => { - const content = `'Single quotes' and "Double quotes"`; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('\'Guillemets simples\' et "Guillemets doubles"'); - }); - - it('should correctly bind to context in nested template', () => { - const content = 'Item {{ id }}'; - const template = ` -
-
${content}
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child).toHaveText(`Article ${i + 1}`); - } - }); - - it('should handle i18n attributes inside i18n section', () => { - const title = 'Hello {{ name }}'; - const template = ` -
-
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const content = `
`; - expect(element.innerHTML).toBe(content); - }); - - it('should handle i18n blocks in nested templates', () => { - const content = 'Hello {{ name }}'; - const template = ` -
-
${content}
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.children[0]).toHaveText('Bonjour John'); - }); - - it('should ignore i18n attributes on self-closing tags', () => { - const template = ''; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element.innerHTML).toBe(template.replace(' i18n', '')); - }); - - it('should handle i18n attribute with directives', () => { - const content = 'Hello {{ name }}'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should work correctly with event listeners', () => { - const content = 'Hello {{ name }}'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const instance = fixture.componentInstance; - - expect(element).toHaveText('Bonjour John'); - expect(instance.clicks).toBe(0); - - element.click(); - expect(instance.clicks).toBe(1); - }); - }); - - describe('ng-container and ng-template support', () => { - it('should handle single translation message within ng-container', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should handle single translation message within ng-template', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('Bonjour John'); - }); - - it('should be able to act as child elements inside i18n block (plain text content)', () => { - const hello = 'Hello'; - const bye = 'Bye'; - const template = ` -
- - ${hello} - - - ${bye} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - 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)', () => { - const content = 'Hello'; - const template = ` -
- - ${content} - - - ${content} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - 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 handle deep nested levels with templates', () => { - const content = 'Hello'; - const template = ` -
- - ${content} - 1 - - - ${content} - 2 - - ${content} - 3 - - ${content} - 4 - - - - - ${content} - 5 - -
- `; - const fixture = getFixtureWithOverrides({template}); - - 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', () => { - const label = 'My logo'; - const content = `${label}`; - const template = ` - - ${content} - - - ${content} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - const child = spans[i]; - expect(child).toHaveText('Mon logo'); - } - }); - }); - - describe('ICU logic', () => { - it('should handle single ICUs', () => { - const template = ` -
{age, select, 10 {ten} 20 {twenty} other {other}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICU-only templates', () => { - const template = ` - {age, select, 10 {ten} 20 {twenty} other {other}} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICUs generated outside of i18n blocks', () => { - const template = ` -
{age, select, 10 {ten} 20 {twenty} other {other}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support interpolation', () => { - const template = ` -
{age, select, 10 {ten} other {{{ otherLabel }}}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should support interpolation with custom interpolation config', () => { - const template = ` -
{age, select, 10 {ten} other {{% otherLabel %}}}
- `; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should handle ICUs with HTML tags inside', () => { - const template = ` -
- {age, select, 10 {10 - ten} 20 {20 - twenty} other {
other
}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const italicTags = element.getElementsByTagName('i'); - expect(italicTags.length).toBe(1); - expect(italicTags[0].innerHTML).toBe('vingt'); - }); - - it('should handle multiple ICUs in one block', () => { - const template = ` -
- {age, select, 10 {ten} 20 {twenty} other {other}} - - {count, select, 1 {one} 2 {two} other {more than two}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => { - const template = ` -
- - {age, select, 10 {ten} 20 {twenty} other {other}} - - - {count, select, 1 {one} 2 {two} other {more than two}} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(2); - expect(spans[0]).toHaveText('vingt'); - expect(spans[1]).toHaveText('deux'); - }); - - it('should handle ICUs inside a template in i18n block', () => { - const template = ` -
- - {age, select, 10 {ten} 20 {twenty} other {other}} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(1); - expect(spans[0]).toHaveText('vingt'); - }); - - it('should handle nested icus', () => { - const template = ` -
- {age, select, - 10 {ten - {count, select, 1 {one} 2 {two} other {more than two}}} - 20 {twenty - {count, select, 1 {one} 2 {two} other {more than two}}} - other {other}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - }); - - 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', () => { - const template = ` - - -
-
- Content -
-
-
-
- `; - - @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) template !: TemplateRef; - - // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) - 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]}); - const fixture = getFixtureWithOverrides({template}); - 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 handle multiple i18n sections', () => { - const template = ` -
Section 1
-
Section 2
-
Section 3
- `; - const fixture = getFixtureWithOverrides({template}); - expect(fixture.nativeElement.innerHTML) - .toBe('
Section 1
Section 2
Section 3
'); - }); - - it('should handle multiple i18n sections inside of *ngFor', () => { - const template = ` -
    -
  • Section 1
  • -
  • Section 2
  • -
  • Section 3
  • -
- `; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child.innerHTML).toBe(`
  • Section 1
  • Section 2
  • Section 3
  • `); - } - }); -}); diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 8f47b2509a..10df144f31 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,26 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; - import {noop} from '../../../compiler/src/render3/view/util'; -import {Component as _Component} from '../../src/core'; -import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/definition'; -import {getTranslationForTemplate, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵnextContext, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n} from '../../src/render3/interfaces/i18n'; -import {AttributeMarker} from '../../src/render3/interfaces/node'; +import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; -import {getNativeByIndex, getTNode} from '../../src/render3/util/view_utils'; - -import {NgForOf, NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture} from './render_util'; - -const Component: typeof _Component = function(...args: any[]): any { - // In test we use @Component for documentation only so it's safe to mock out the implementation. - return () => undefined; -} as any; +import {getNativeByIndex} from '../../src/render3/util/view_utils'; +import {TemplateFixture} from './render_util'; describe('Runtime i18n', () => { describe('getTranslationForTemplate', () => { @@ -83,7 +70,6 @@ describe('Runtime i18n', () => { const index = 0; const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); - // Check debug const debugOps = (opCodes as any).create.debug !.operations; expect(debugOps[0].__raw_opCode).toBe('simple text'); @@ -586,319 +572,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nEnd`, () => { - it('for text', () => { - const MSG_DIV = `simple text`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - expect(fixture.html).toEqual(`
    ${MSG_DIV}
    `); - }); - - it('for bindings', () => { - const MSG_DIV = `Hello �0�!`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - - // But it should have created an empty text node in `viewData` - const textTNode = fixture.hostView[HEADER_OFFSET + 2] as Node; - expect(textTNode.nodeType).toEqual(Node.TEXT_NODE); - }); - - it('for elements', () => { - const MSG_DIV = `Hello �#3�world�/#3� and �#2�universe�/#2�!`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'div'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - expect(fixture.html).toEqual('
    Hello world and
    universe
    !
    '); - }); - - it('for translations without top level element', () => { - // When it's the first node - let MSG_DIV = `Hello world`; - let fixture = prepareFixture(() => { ɵɵi18n(0, MSG_DIV); }, null, 1); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is a text node - MSG_DIV = ` world`; - fixture = prepareFixture(() => { - ɵɵtext(0, 'Hello'); - ɵɵi18n(1, MSG_DIV); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is an element - fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵtext(1, 'Hello'); - ɵɵelementEnd(); - ɵɵi18n(2, MSG_DIV); - }, null, 3); - - expect(fixture.html).toEqual('
    Hello
    world'); - - // When there is a node after - MSG_DIV = `Hello `; - fixture = prepareFixture(() => { - ɵɵi18n(0, MSG_DIV); - ɵɵtext(1, 'world'); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - }); - - it('for deleted placeholders', () => { - const MSG_DIV = `Hello �#3�world�/#3�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div'); // Will be removed - ɵɵelement(3, 'span'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelementStart(4, 'div'); - { ɵɵtext(5, '!'); } - ɵɵelementEnd(); - }, null, 6); - - expect(fixture.html).toEqual('
    Hello world
    !
    '); - }); - - it('for sub-templates', () => { - // Template: `
    Content:
    beforemiddleafter
    !
    `; - const MSG_DIV = - `Content: �*2:1��#1:1�before�*2:2��#1:2�middle�/#1:2��/*2:2�after�/#1:1��/*2:1�!`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelementStart(1, 'div'); - ɵɵtemplate(2, subTemplate_2, 2, 0, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵelementEnd(); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', ɵɵbind(true)); - } - } - - function subTemplate_2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 2); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - } - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 3, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
    Content:
    beforemiddleafter
    !
    '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    -
    '); - }); - - it('for multiple ICU expressions inside html', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - - it('for ICU expressions inside templates', () => { - const MSG_DIV = `�*2:1��#1:1�{�0:1�, plural, - =0 {no emails!} - =1 {one email} - other {�0:1� emails} - }�/#1:1��/*2:1�`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - const ctx = ɵɵnextContext(); - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(0); - } - } - - class MyApp { - value0 = 0; - value1 = 'emails label'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 2, 2, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
    no emails!
    '); - - // Update the value - fixture.component.value0 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    3 emails
    '); - }); - - it('for ICU expressions inside ', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1); - { ɵɵi18n(2, MSG_DIV); } - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(0)); - ɵɵi18nExp(ɵɵbind('more than one')); - ɵɵi18nApply(2); - }, - 3, 2); - - expect(fixture.html).toEqual('
    no emails!
    '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - }); - describe(`i18nAttribute`, () => { it('for text', () => { const MSG_title = `Hello world!`; @@ -975,1140 +648,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nExp & i18nApply`, () => { - it('for text bindings', () => { - const MSG_DIV = `Hello �0�!`; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    Hello world!
    '); - }); - - it('for attribute bindings', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html).toEqual('
    '); - }); - - it('for attributes with no bindings', () => { - const MSG_title = `Hello world!`; - const MSG_div_attr = ['title', MSG_title]; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { ɵɵi18nApply(1); }, 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - }); - - it('for multiple attribute bindings', () => { - const MSG_title = `Hello �0� and �1�, again �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value0: 'world', value1: 'universe'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value0 = 'earth'; - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value0 = 'earthlings'; - ctx.value1 = 'martians'; - fixture.update(); - expect(fixture.html) - .toEqual('
    '); - }); - - it('for bindings of multiple attributes', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html) - .toEqual('
    '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html).toEqual('
    no emails!
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    no emails!
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
    one email
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual('
    10 emails
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual('
    10 emails
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
    no emails!
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
    one email - (1)
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails - (10)
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails - (10)
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 4, 2); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    one email(1)
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails(10)
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails(10)
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const ctx = {value0: 0, value1: 'cat'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
    zero
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    zero
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html).toEqual('
    10 cats!
    '); - - ctx.value1 = 'squirrel'; - fixture.update(); - expect(fixture.html).toEqual('
    10 animals!
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
    zero
    '); - }); - }); - - describe('integration', () => { - it('should support multiple i18n blocks', () => { - // Translated template: - //
    - // - // trad {{exp1}} - // - // hello - // - // - // trad - // - //
    - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_2_ATTR = ['title', `start �1� middle �0� end`]; - const MSG_DIV_2 = `�#9��/#9��#7�trad�/#7�`; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 10, - vars: 2, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'a'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵtext(3, 'hello'); - ɵɵelementStart(4, 'b'); - { - ɵɵi18nAttributes(5, MSG_DIV_2_ATTR); - ɵɵi18nStart(6, MSG_DIV_2); - { - ɵɵelement(7, 'c'); - ɵɵelement(8, 'd'); // will be removed - ɵɵelement(9, 'e'); // will be moved before `c` - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
    trad 1hellotrad
    `); - }); - - it('should support multiple sibling i18n blocks', () => { - // Translated template: - //
    - //
    Section 1
    - //
    Section 2
    - //
    Section 3
    - //
    - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 7, - vars: 0, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'div'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'div'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'div'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual(`
    Section 1
    Section 2
    Section 3
    `); - }); - - it('should support multiple sibling i18n blocks inside of *ngFor', () => { - // Translated template: - //
      - //
    • Section 1
    • - //
    • Section 2
    • - //
    • Section 3
    • - //
    - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - ɵɵelementStart(1, 'li'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'li'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'li'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - - class MyApp { - items: string[] = ['1', '2', '3']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, liTemplate, 7, 0, 'ul', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
    • Section 1
    • Section 2
    • Section 3
    • Section 1
    • Section 2
    • Section 3
    • Section 1
    • Section 2
    • Section 3
    `); - }); - - it('should support attribute translations on removed elements', () => { - // Translated template: - //
    - // trad {{exp1}} - //
    - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 5, - vars: 5, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b'); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(`
    trad 1
    `); - }); - - it('should work with directives and host bindings', () => { - let directiveInstances: Directive[] = []; - - class Directive { - // @HostBinding('className') - klass = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => { - const instance = new Directive(); - directiveInstances.push(instance); - return instance; - }, - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'className', ɵɵbind(ctx.klass), null, true); - } - } - }); - } - - // Translated template: - //
    - // trad {�0�, plural, - // =0 {no emails!} - // =1 {one email} - // other {�0� emails} - // } - //
    - - const MSG_DIV_1 = `trad {�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = 1; - exp2 = 2; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 6, - vars: 5, - directives: [Directive], - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [AttributeMarker.Bindings, 'dir']); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b', [AttributeMarker.Bindings, 'dir']); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelement(5, 'div', [AttributeMarker.Bindings, 'dir']); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html) - .toEqual( - `
    trad one email
    `); - - directiveInstances.forEach(instance => instance.klass = 'bar'); - fixture.component.exp1 = 2; - fixture.component.exp2 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - `
    trad 2 emails
    `); - }); - - it('should fix the links when adding/moving/removing nodes', () => { - const MSG_DIV = `�#2��/#2��#8��/#8��#4��/#4��#5��/#5�Hello World�#3��/#3��#7��/#7�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div2'); - ɵɵelement(3, 'div3'); - ɵɵelement(4, 'div4'); - ɵɵelement(5, 'div5'); - ɵɵelement(6, 'div6'); - ɵɵelement(7, 'div7'); - ɵɵelement(8, 'div8'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - }, null, 9); - - expect(fixture.html) - .toEqual( - '
    Hello World
    '); - - const div0 = getTNode(0, fixture.hostView); - const div2 = getTNode(2, fixture.hostView); - const div3 = getTNode(3, fixture.hostView); - const div4 = getTNode(4, fixture.hostView); - const div5 = getTNode(5, fixture.hostView); - const div7 = getTNode(7, fixture.hostView); - const div8 = getTNode(8, fixture.hostView); - const text = getTNode(9, fixture.hostView); - expect(div0.child).toEqual(div2); - expect(div0.next).toBeNull(); - expect(div2.next).toEqual(div8); - expect(div8.next).toEqual(div4); - expect(div4.next).toEqual(div5); - expect(div5.next).toEqual(text); - expect(text.next).toEqual(div3); - expect(div3.next).toEqual(div7); - expect(div7.next).toBeNull(); - }); - - describe('projection', () => { - it('should project the translations', () => { - @Component({selector: 'child', template: '

    '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Je suis projeté depuis �#3��0��/#3��/#2�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
    - - I am projected from - {{name}} - - - - - -
    ` - // Translated to: - //
    - // - //

    - // Je suis projeté depuis {{name}} - //

    - //
    - //
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 8, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelementStart(2, 'child'); - { - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵelement(5, 'remove-me-1'); - } - ɵɵelementEnd(); - ɵɵelement(6, 'remove-me-2'); - } - ɵɵelementEnd(); - ɵɵelement(7, 'remove-me-3'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - //

    Parent

    - //

    Je suis projeté depuis Parent

    - }); - - it('should project a translated i18n block', () => { - @Component({selector: 'child', template: '

    '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Je suis projeté depuis �0�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
    - - - I am projected from {{name}} - - -
    ` - // Translated to: - //
    - // - // - // Je suis projeté depuis {{name}} - // - // - //
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'child'); - { - ɵɵelement(2, 'any'); - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵi18n(5, MSG_DIV_SECTION_1); - } - ɵɵelementEnd(); - ɵɵelement(6, 'any'); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - - // it should be able to render a new component with the same template code - const fixture2 = new ComponentFixture(Parent); - expect(fixture2.html).toEqual(fixture.html); - - // Updating the fixture should work - fixture2.component.name = 'Parent 2'; - fixture.update(); - fixture2.update(); - expect(fixture2.html) - .toEqual( - '

    Je suis projeté depuis Parent 2

    '); - - // The first fixture should not have changed - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - }); - - it('should re-project translations when multiple projections', () => { - @Component({selector: 'grand-child', template: '
    '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Bonjour�/#2� Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
    Bonjour Monde!
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { ɵɵelement(2, 'b'); } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
    Bonjour Monde!
    '); - //
    Bonjour
    - //
    Bonjour Monde!
    - }); - - xit('should re-project translations when removed placeholders', () => { - @Component({selector: 'grand-child', template: '
    '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Bonjour Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
    Bonjour Monde!
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'b'); // will be removed - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
    Bonjour Monde!
    '); - }); - - it('should project translations with selectors', () => { - @Component({ - selector: 'child', - template: ` - - ` - }) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span']]]); - ɵɵprojection(0, 1); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`; - - @Component({ - selector: 'parent', - template: ` - - - - - ` - // Translated to: - // Contenu - }) - class Parent { - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'span', ['title', 'keepMe']); - ɵɵelement(3, 'span', ['title', 'deleteMe']); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('Contenu'); - }); - }); - }); - describe('i18nPostprocess', () => { it('should handle valid cases', () => { const arr = ['�*1:1��#2:1�', '�#4:1�', '�6:1�', '�/#2:1��/*1:1�'];