From 7a30153aa1a2d0ac83991b293f414891c93df450 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Tue, 5 May 2020 17:52:49 -0700 Subject: [PATCH] test(core): verify that Ivy i18n works correctly with HTML namespaces (#36943) This commit adds several tests to verify that i18n logic in Ivy handles elements with HTML namespaces correctly. Related to #36941. PR Close #36943 --- .../compliance/r3_view_compiler_i18n_spec.ts | 98 +++++++++++++++++++ packages/core/test/acceptance/i18n_spec.ts | 75 +++++++++++++- 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index d7cd299ada..9b8cd9c8c2 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -3832,4 +3832,102 @@ $` + String.raw`{$I18N_4$}:ICU:\`; } }); }); + + describe('namespaces', () => { + it('should handle namespaces inside i18n blocks', () => { + const input = ` + + + + Count: 5 + + + + `; + + const output = String.raw` + var $I18N_0$; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + const $MSG_EXTERNAL_7128002169381370313$$APP_SPEC_TS_1$ = goog.getMsg("{$startTagXhtmlDiv} Count: {$startTagXhtmlSpan}5{$closeTagXhtmlSpan}{$closeTagXhtmlDiv}", { + "startTagXhtmlDiv": "\uFFFD#3\uFFFD", + "startTagXhtmlSpan": "\uFFFD#4\uFFFD", + "closeTagXhtmlSpan": "\uFFFD/#4\uFFFD", + "closeTagXhtmlDiv": "\uFFFD/#3\uFFFD" + }); + $I18N_0$ = $MSG_EXTERNAL_7128002169381370313$$APP_SPEC_TS_1$; + } + else { + $I18N_0$ = $localize \`$` + + String.raw`{"\uFFFD#3\uFFFD"}:START_TAG__XHTML_DIV: Count: $` + + String.raw`{"\uFFFD#4\uFFFD"}:START_TAG__XHTML_SPAN:5$` + + String.raw`{"\uFFFD/#4\uFFFD"}:CLOSE_TAG__XHTML_SPAN:$` + + String.raw`{"\uFFFD/#3\uFFFD"}:CLOSE_TAG__XHTML_DIV:\`; + } + … + function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵnamespaceSVG(); + $r3$.ɵɵelementStart(0, "svg", 0); + $r3$.ɵɵelementStart(1, "foreignObject"); + $r3$.ɵɵi18nStart(2, $I18N_0$); + $r3$.ɵɵnamespaceHTML(); + $r3$.ɵɵelementStart(3, "div", 1); + $r3$.ɵɵelement(4, "span"); + $r3$.ɵɵelementEnd(); + $r3$.ɵɵi18nEnd(); + $r3$.ɵɵelementEnd(); + $r3$.ɵɵelementEnd(); + } + } + `; + + verify(input, output); + }); + + it('should handle namespaces on i18n block containers', () => { + const input = ` + + + + Count: 5 + + + + `; + + const output = String.raw` + var $I18N_0$; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + const $MSG_EXTERNAL_7428861019045796010$$APP_SPEC_TS_1$ = goog.getMsg(" Count: {$startTagXhtmlSpan}5{$closeTagXhtmlSpan}", { + "startTagXhtmlSpan": "\uFFFD#4\uFFFD", + "closeTagXhtmlSpan": "\uFFFD/#4\uFFFD" + }); + $I18N_0$ = $MSG_EXTERNAL_7428861019045796010$$APP_SPEC_TS_1$; + } + else { + $I18N_0$ = $localize \` Count: $` + + String.raw`{"\uFFFD#4\uFFFD"}:START_TAG__XHTML_SPAN:5$` + + String.raw`{"\uFFFD/#4\uFFFD"}:CLOSE_TAG__XHTML_SPAN:\`; + } + … + function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵnamespaceSVG(); + $r3$.ɵɵelementStart(0, "svg", 0); + $r3$.ɵɵelementStart(1, "foreignObject"); + $r3$.ɵɵnamespaceHTML(); + $r3$.ɵɵelementStart(2, "div", 1); + $r3$.ɵɵi18nStart(3, $I18N_0$); + $r3$.ɵɵelement(4, "span"); + $r3$.ɵɵi18nEnd(); + $r3$.ɵɵelementEnd(); + $r3$.ɵɵelementEnd(); + $r3$.ɵɵelementEnd(); + } + } + `; + + verify(input, output, {verbose: true}); + }); + }); }); diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 6a272efaab..59c6f0e465 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -9,15 +9,15 @@ // below. This would normally be done inside the application `polyfills.ts` file. import '@angular/localize/init'; -import {CommonModule, registerLocaleData} from '@angular/common'; +import {CommonModule, DOCUMENT, registerLocaleData} from '@angular/common'; import localeEs from '@angular/common/locales/es'; import localeRo from '@angular/common/locales/ro'; import {computeMsgId} from '@angular/compiler'; -import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core'; import {setDelayProjection} from '@angular/core/src/render3/instructions/projection'; import {TestBed} from '@angular/core/testing'; import {clearTranslations, loadTranslations} from '@angular/localize'; -import {By} from '@angular/platform-browser'; +import {By, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; import {BehaviorSubject} from 'rxjs'; @@ -530,6 +530,75 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { }); }); + describe('should work correctly with namespaces', () => { + beforeEach(() => { + function _document(): any { + // Tell Ivy about the global document + ɵsetDocument(document); + return document; + } + + TestBed.configureTestingModule({ + providers: [ + {provide: DOCUMENT, useFactory: _document, deps: []}, + // TODO(FW-811): switch back to default server renderer (i.e. remove the line below) + // once it starts to support Ivy namespace format (URIs) correctly. For now, use + // `DomRenderer` that supports Ivy namespace format. + {provide: RendererFactory2, useClass: DomRendererFactory2} + ], + }); + }); + + it('should handle namespaces inside i18n blocks', () => { + loadTranslations({ + [computeMsgId( + '{$START_TAG__XHTML_DIV} Hello ' + + '{$START_TAG__XHTML_SPAN}world{$CLOSE_TAG__XHTML_SPAN}{$CLOSE_TAG__XHTML_DIV}')]: + '{$START_TAG__XHTML_DIV} Bonjour ' + + '{$START_TAG__XHTML_SPAN}monde{$CLOSE_TAG__XHTML_SPAN}{$CLOSE_TAG__XHTML_DIV}' + }); + + const fixture = initWithTemplate(AppComp, ` + + + + Hello world + + + + `); + + const element = fixture.nativeElement; + expect(element.textContent.trim()).toBe('Bonjour monde'); + expect(element.querySelector('svg').namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(element.querySelector('div').namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(element.querySelector('span').namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + }); + + it('should handle namespaces on i18n block containers', () => { + loadTranslations({ + [computeMsgId(' Hello {$START_TAG__XHTML_SPAN}world{$CLOSE_TAG__XHTML_SPAN}')]: + ' Bonjour {$START_TAG__XHTML_SPAN}monde{$CLOSE_TAG__XHTML_SPAN}' + }); + + const fixture = initWithTemplate(AppComp, ` + + + + Hello world + + + + `); + + const element = fixture.nativeElement; + expect(element.textContent.trim()).toBe('Bonjour monde'); + expect(element.querySelector('svg').namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(element.querySelector('div').namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(element.querySelector('span').namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + }); + }); + describe('should support ICU expressions', () => { it('with no root node', () => { loadTranslations({