diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index a679190614..205433cf13 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -390,11 +390,19 @@ function i18nStartFirstPass( parentIndexStack[parentIndexPointer] = parentIndex; const createOpCodes: I18nMutateOpCodes = []; // If the previous node wasn't the direct parent then we have a translation without top level - // element and we need to keep a reference of the previous element if there is one + // element and we need to keep a reference of the previous element if there is one. We should also + // keep track whether an element was a parent node or not, so that the logic that consumes + // the generated `I18nMutateOpCode`s can leverage this information to properly set TNode state + // (whether it's a parent or sibling). if (index > 0 && previousOrParentTNode !== parentTNode) { + let previousTNodeIndex = previousOrParentTNode.index - HEADER_OFFSET; + // If current TNode is a sibling node, encode it using a negative index. This information is + // required when the `Select` action is processed (see the `readCreateOpCodes` function). + if (!getIsParent()) { + previousTNodeIndex = ~previousTNodeIndex; + } // Create an OpCode to select the previous TNode - createOpCodes.push( - previousOrParentTNode.index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select); + createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select); } const updateOpCodes: I18nUpdateOpCodes = []; const icuExpressions: TIcu[] = []; @@ -414,12 +422,16 @@ function i18nStartFirstPass( } } else { const phIndex = parseInt(value.substr(1), 10); - // The value represents a placeholder that we move to the designated index + const isElement = value.charAt(0) === TagType.ELEMENT; + // The value represents a placeholder that we move to the designated index. + // Note: positive indicies indicate that a TNode with a given index should also be marked as + // parent while executing `Select` instruction. createOpCodes.push( - phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, + (isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF | + I18nMutateOpCode.Select, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); - if (value.charAt(0) === TagType.ELEMENT) { + if (isElement) { parentIndexStack[++parentIndexPointer] = parentIndex = phIndex; } } @@ -757,12 +769,15 @@ function readCreateOpCodes( appendI18nNode(tView, currentTNode !, destinationTNode, previousTNode, lView); break; case I18nMutateOpCode.Select: - const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + // Negative indicies indicate that a given TNode is a sibling node, not a parent node + // (see `i18nStartFirstPass` for additional information). + const isParent = opCode >= 0; + const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF; visitedNodes.push(nodeIndex); previousTNode = currentTNode; currentTNode = getTNode(tView, nodeIndex); if (currentTNode) { - setPreviousOrParentTNode(currentTNode, currentTNode.type === TNodeType.Element); + setPreviousOrParentTNode(currentTNode, isParent); } break; case I18nMutateOpCode.ElementEnd: diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 8c1bfa479d..432f355173 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -270,9 +270,9 @@ export function getPreviousOrParentTNode(): TNode { return instructionState.lFrame.previousOrParentTNode; } -export function setPreviousOrParentTNode(tNode: TNode, _isParent: boolean) { +export function setPreviousOrParentTNode(tNode: TNode, isParent: boolean) { instructionState.lFrame.previousOrParentTNode = tNode; - instructionState.lFrame.isParent = _isParent; + instructionState.lFrame.isParent = isParent; } export function getIsParent(): boolean { diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 8b5c83e323..7e52d460be 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -530,6 +530,36 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { expect(element).toHaveText('autre'); }); + it('with no root node and text surrounding ICU', () => { + loadTranslations({ + [computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]: + '{VAR_SELECT, select, 10 {Dix} 20 {Vingt} other {Autre}}' + }); + const fixture = initWithTemplate(AppComp, ` + ICU start --> + {count, select, 10 {Ten} 20 {Twenty} other {Other}} + <-- ICU end + `); + + const element = fixture.nativeElement; + expect(element.textContent).toContain('ICU start --> Autre <-- ICU end'); + }); + + it('with no root node and text and DOM nodes surrounding ICU', () => { + loadTranslations({ + [computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]: + '{VAR_SELECT, select, 10 {Dix} 20 {Vingt} other {Autre}}' + }); + const fixture = initWithTemplate(AppComp, ` + ICU start --> + {count, select, 10 {Ten} 20 {Twenty} other {Other}} + <-- ICU end + `); + + const element = fixture.nativeElement; + expect(element.textContent).toContain('ICU start --> Autre <-- ICU end'); + }); + it('with no i18n tag', () => { loadTranslations({ [computeMsgId('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}')]: @@ -2100,6 +2130,73 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { "ng-reflect-ng-if": "false" }-->`); }); + + describe('ngTemplateOutlet', () => { + it('should work with i18n content that includes elements', () => { + loadTranslations({ + [computeMsgId('{$START_TAG_SPAN}A{$CLOSE_TAG_SPAN} B ')]: + '{$START_TAG_SPAN}a{$CLOSE_TAG_SPAN} b', + }); + + const fixture = initWithTemplate(AppComp, ` + + + A B + + `); + expect(fixture.nativeElement.textContent).toContain('a b'); + }); + + it('should work with i18n content that includes other templates (*ngIf)', () => { + loadTranslations({ + [computeMsgId('{$START_TAG_SPAN}A{$CLOSE_TAG_SPAN} B ')]: + '{$START_TAG_SPAN}a{$CLOSE_TAG_SPAN} b', + }); + + const fixture = initWithTemplate(AppComp, ` + + + A B + + `); + expect(fixture.nativeElement.textContent).toContain('a b'); + }); + + it('should work with i18n content that includes projection', () => { + loadTranslations({ + [computeMsgId('{$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT} B ')]: + '{$START_TAG_NG_CONTENT}{$CLOSE_TAG_NG_CONTENT} b', + }); + + @Component({ + selector: 'projector', + template: ` + + + B + + ` + }) + class Projector { + } + + @Component({ + selector: 'app', + template: ` + a + ` + }) + class AppComponent { + } + + TestBed.configureTestingModule({declarations: [AppComponent, Projector]}); + + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('a b'); + }); + }); }); function initWithTemplate(compType: Type, template: string) { diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 0da67146a4..0535ce454b 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -194,6 +194,7 @@ describe('Runtime i18n', () => { let nbConsts = 3; let index = 1; const firstTextNode = 3; + const rootTemplate = 2; let opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); expect(opCodes).toEqual({ @@ -202,7 +203,7 @@ describe('Runtime i18n', () => { '', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, + ~rootTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, '!', nbConsts + 1, @@ -234,7 +235,7 @@ describe('Runtime i18n', () => { 'before', nbConsts, spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, + ~bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'after', nbConsts + 1,