diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 453498d1f4..3812841552 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -395,21 +395,19 @@ function i18nStartFirstPass( } } else { // Even indexes are text (including bindings & ICU expressions) - const parts = value.split(ICU_REGEXP); + const parts = extractParts(value); for (let j = 0; j < parts.length; j++) { - value = parts[j]; - if (j & 1) { // Odd indexes are ICU expressions // Create the comment node that will anchor the ICU expression allocExpando(viewData); const icuNodeIndex = tView.blueprint.length - 1 - HEADER_OFFSET; createOpCodes.push( - COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', + COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); // Update codes for the ICU expression - const icuExpression = parseICUBlock(value.substr(1, value.length - 2)); + const icuExpression = parts[j] as IcuExpression; const mask = getBindingMask(icuExpression); icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex); // Since this is recursive, the last TIcu that was pushed is the one we want @@ -422,19 +420,21 @@ function i18nStartFirstPass( mask, // mask of all the bindings of this ICU expression 2, // skip 2 opCodes if not changed icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex); - } else if (value !== '') { + } else if (parts[j] !== '') { + const text = parts[j] as string; // Even indexes are text (including bindings) - const hasBinding = value.match(BINDING_REGEXP); + const hasBinding = text.match(BINDING_REGEXP); // Create text nodes allocExpando(viewData); + const textNodeIndex = tView.blueprint.length - 1 - HEADER_OFFSET; createOpCodes.push( // If there is a binding, the value will be set during update - hasBinding ? '' : value, + hasBinding ? '' : text, textNodeIndex, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); if (hasBinding) { addAllToArray( - generateBindingUpdateOpCodes(value, tView.blueprint.length - 1 - HEADER_OFFSET), + generateBindingUpdateOpCodes(text, tView.blueprint.length - 1 - HEADER_OFFSET), updateOpCodes); } } @@ -580,8 +580,7 @@ function i18nEndFirstPass(tView: TView) { // The last placeholder that was added before `i18nEnd` const previousOrParentTNode = getPreviousOrParentTNode(); - const visitedPlaceholders = - readCreateOpCodes(rootIndex, tI18n.create, tI18n.expandoStartIndex, viewData); + const visitedPlaceholders = readCreateOpCodes(rootIndex, tI18n.create, tI18n.icus, viewData); // Remove deleted placeholders // The last placeholder that was added before `i18nEnd` is `previousOrParentTNode` @@ -593,7 +592,7 @@ function i18nEndFirstPass(tView: TView) { } function readCreateOpCodes( - index: number, createOpCodes: I18nMutateOpCodes, expandoStartIndex: number, + index: number, createOpCodes: I18nMutateOpCodes, icus: TIcu[] | null, viewData: LView): number[] { const renderer = getLView()[RENDERER]; let currentTNode: TNode|null = null; @@ -603,10 +602,10 @@ function readCreateOpCodes( const opCode = createOpCodes[i]; if (typeof opCode == 'string') { const textRNode = createTextNode(opCode, renderer); + const textNodeIndex = createOpCodes[++i] as number; ngDevMode && ngDevMode.rendererCreateTextNode++; previousTNode = currentTNode; - currentTNode = - createNodeAtIndex(expandoStartIndex++, TNodeType.Element, textRNode, null, null); + currentTNode = createNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null, null); setIsParent(false); } else if (typeof opCode == 'number') { switch (opCode & I18nMutateOpCode.MASK_OPCODE) { @@ -658,14 +657,15 @@ function readCreateOpCodes( switch (opCode) { case COMMENT_MARKER: const commentValue = createOpCodes[++i] as string; + const commentNodeIndex = createOpCodes[++i] as number; ngDevMode && assertEqual( typeof commentValue, 'string', `Expected "${commentValue}" to be a comment node value`); const commentRNode = renderer.createComment(commentValue); ngDevMode && ngDevMode.rendererCreateComment++; previousTNode = currentTNode; - currentTNode = createNodeAtIndex( - expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null); + currentTNode = + createNodeAtIndex(commentNodeIndex, TNodeType.IcuContainer, commentRNode, null, null); attachPatchData(commentRNode, viewData); (currentTNode as TIcuContainerNode).activeCaseIndex = null; // We will add the case nodes later, during the update phase @@ -673,6 +673,7 @@ function readCreateOpCodes( break; case ELEMENT_MARKER: const tagNameValue = createOpCodes[++i] as string; + const elementNodeIndex = createOpCodes[++i] as number; ngDevMode && assertEqual( typeof tagNameValue, 'string', `Expected "${tagNameValue}" to be an element node tag name`); @@ -680,7 +681,7 @@ function readCreateOpCodes( ngDevMode && ngDevMode.rendererCreateElement++; previousTNode = currentTNode; currentTNode = createNodeAtIndex( - expandoStartIndex++, TNodeType.Element, elementRNode, tagNameValue, null); + elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue, null); break; default: throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); @@ -759,7 +760,7 @@ function readUpdateOpCodes( icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null; // Add the nodes for the new case - readCreateOpCodes(-1, tIcu.create[caseIndex], tIcu.expandoStartIndex, viewData); + readCreateOpCodes(-1, tIcu.create[caseIndex], icus, viewData); caseCreated = true; break; case I18nUpdateOpCode.IcuUpdate: @@ -1400,7 +1401,7 @@ function parseNodes( icuCase.vars--; } else { icuCase.create.push( - ELEMENT_MARKER, tagName, + ELEMENT_MARKER, tagName, newIndex, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); const elAttrs = element.attributes; for (let i = 0; i < elAttrs.length; i++) { @@ -1446,7 +1447,7 @@ function parseNodes( const value = currentNode.textContent || ''; const hasBinding = value.match(BINDING_REGEXP); icuCase.create.push( - hasBinding ? '' : value, + hasBinding ? '' : value, newIndex, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove); if (hasBinding) { @@ -1461,7 +1462,7 @@ function parseNodes( const newLocal = ngDevMode ? `nested ICU ${nestedIcuIndex}` : ''; // Create the comment node that will anchor the ICU expression icuCase.create.push( - COMMENT_MARKER, newLocal, + COMMENT_MARKER, newLocal, newIndex, parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); const nestedIcu = nestedIcus[nestedIcuIndex]; nestedIcusToCreate.push([nestedIcu, newIndex]); diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts index a0babb5886..e4d2f3bbf0 100644 --- a/packages/core/test/i18n_integration_spec.ts +++ b/packages/core/test/i18n_integration_spec.ts @@ -50,6 +50,8 @@ const TRANSLATIONS: any = { '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Bonjour{$closeTagSpan}{$closeTagNgContainer}', '{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}}': @@ -384,23 +386,21 @@ onlyInIvy('Ivy i18n logic').describe('i18n', function() { expect(italicTags[0].innerHTML).toBe('vingt'); }); - fixmeIvy('FW-905: Multiple ICUs in one i18n block are not processed') - .it('should handle multiple ICUs in one block', () => { - const template = ` + 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 fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); + const element = fixture.nativeElement.firstChild; + expect(element).toHaveText('vingt - deux'); + }); - fixmeIvy('FW-906: Multiple ICUs wrapped in HTML tags in one i18n block throw an error') - .it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => { - const template = ` + it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => { + const template = `
{age, select, 10 {ten} 20 {twenty} other {other}} @@ -410,14 +410,14 @@ onlyInIvy('Ivy i18n logic').describe('i18n', function() {
`; - const fixture = getFixtureWithOverrides({template}); + const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(2); - expect(spans[0].innerHTML).toBe('vingt'); - expect(spans[1].innerHTML).toBe('deux'); - }); + 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 = ` diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 0db5621dc7..f4ad2fe136 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -84,8 +84,10 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 1, expandoStartIndex: nbConsts, - create: - ['simple text', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], + create: [ + 'simple text', nbConsts, + index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild + ], update: [], icus: null }); @@ -106,20 +108,25 @@ describe('Runtime i18n', () => { expandoStartIndex: nbConsts, create: [ 'Hello ', + nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'world', + nbConsts + 1, elementIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, ' and ', + nbConsts + 2, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'universe', + nbConsts + 3, elementIndex2 << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, '!', + nbConsts + 4, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, ], update: [], @@ -136,7 +143,8 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 1, expandoStartIndex: nbConsts, - create: ['', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], + create: + ['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], update: [ 0b1, // bindings mask 4, // if no update, skip 4 @@ -157,7 +165,8 @@ describe('Runtime i18n', () => { expect(opCodes).toEqual({ vars: 1, expandoStartIndex: nbConsts, - create: ['', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], + create: + ['', nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild], update: [ 0b11, // bindings mask 8, // if no update, skip 8 @@ -192,10 +201,12 @@ describe('Runtime i18n', () => { expandoStartIndex: nbConsts, create: [ '', + nbConsts, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, '!', + nbConsts + 1, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, ], update: [ @@ -223,10 +234,12 @@ describe('Runtime i18n', () => { spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'before', + nbConsts, spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'after', + nbConsts + 1, spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, ], @@ -249,6 +262,7 @@ describe('Runtime i18n', () => { bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, 'middle', + nbConsts, bElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd, ], @@ -268,7 +282,7 @@ describe('Runtime i18n', () => { const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index); const tIcuIndex = 0; const icuCommentNodeIndex = index + 1; - const firstTextNode = index + 2; + const firstTextNodeIndex = index + 2; const bElementNodeIndex = index + 3; const iElementNodeIndex = index + 3; const spanElementNodeIndex = index + 3; @@ -279,7 +293,7 @@ describe('Runtime i18n', () => { vars: 5, expandoStartIndex: nbConsts, create: [ - COMMENT_MARKER, 'ICU 1', + COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], update: [ @@ -300,49 +314,53 @@ describe('Runtime i18n', () => { create: [ [ 'no ', + firstTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, ELEMENT_MARKER, 'b', + bElementNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, 'title', 'none', 'emails', + innerTextNode, bElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, '!', + lastTextNode, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, ], [ - 'one ', + 'one ', firstTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ELEMENT_MARKER, 'i', + ELEMENT_MARKER, 'i', iElementNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'email', + 'email', innerTextNode, iElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], [ - '', + '', firstTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - ELEMENT_MARKER, 'span', + ELEMENT_MARKER, 'span', spanElementNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - 'emails', + 'emails', innerTextNode, spanElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ] ], remove: [ [ - firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, + firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, ], [ - firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, + firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, iElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, ], [ - firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, + firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, spanElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, ] @@ -354,7 +372,7 @@ describe('Runtime i18n', () => { 3, // skip 3 if not changed -1, // binding index ' ', // text string to concatenate to the binding value - firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, + firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, 0b10, // mask for the title attribute binding 4, // skip 4 if not changed -2, // binding index @@ -380,10 +398,10 @@ describe('Runtime i18n', () => { const index = 0; const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index); const icuCommentNodeIndex = index + 1; - const firstTextNode = index + 2; + const firstTextNodeIndex = index + 2; const nestedIcuCommentNodeIndex = index + 3; - const lastTextNode = index + 4; - const nestedTextNode = index + 5; + const lastTextNodeIndex = index + 4; + const nestedTextNodeIndex = index + 5; const tIcuIndex = 1; const nestedTIcuIndex = 0; @@ -391,7 +409,7 @@ describe('Runtime i18n', () => { vars: 6, expandoStartIndex: nbConsts, create: [ - COMMENT_MARKER, 'ICU 1', + COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex, index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], update: [ @@ -407,27 +425,30 @@ describe('Runtime i18n', () => { { type: 0, vars: [1, 1, 1], - expandoStartIndex: lastTextNode + 1, + expandoStartIndex: lastTextNodeIndex + 1, childIcus: [[], [], []], cases: ['cat', 'dog', 'other'], create: [ [ - 'cats', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | + 'cats', nestedTextNodeIndex, nestedIcuCommentNodeIndex + << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], [ - 'dogs', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | + 'dogs', nestedTextNodeIndex, nestedIcuCommentNodeIndex + << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], [ - 'animals', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | + 'animals', nestedTextNodeIndex, nestedIcuCommentNodeIndex + << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ] ], remove: [ - [nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], - [nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], - [nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove] + [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], + [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], + [nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove] ], update: [[], [], []] }, @@ -439,23 +460,23 @@ describe('Runtime i18n', () => { cases: ['0', 'other'], create: [ [ - 'zero', + 'zero', firstTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ], [ - '', + '', firstTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - COMMENT_MARKER, 'nested ICU 0', + COMMENT_MARKER, 'nested ICU 0', nestedIcuCommentNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild, - '!', + '!', lastTextNodeIndex, icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild ] ], remove: [ - [firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], + [firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove], [ - firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, - lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, + firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, + lastTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, 0 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu, nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove, ] @@ -467,7 +488,7 @@ describe('Runtime i18n', () => { 3, // skip 3 if not changed -1, // binding index ' ', // text string to concatenate to the binding value - firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, + firstTextNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text, 0b10, // mask for inner ICU main binding 3, // skip 3 if not changed -2, // inner ICU main binding @@ -656,6 +677,45 @@ describe('Runtime i18n', () => { 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!} @@ -1014,6 +1074,118 @@ describe('Runtime i18n', () => { 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}