diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index 9c6bc08e90507..16edade3139ac 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -219,7 +219,7 @@ function i18nStartFirstCreatePassProcessTextNode( const tNode = createTNodeAndAddOpCode( tView, rootTNode, existingTNodes, lView, createOpCodes, hasBinding ? null : text, false); if (hasBinding) { - generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index); + generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index, null, 0, null); } } @@ -251,7 +251,11 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str // i18n attributes that hit this code path are guaranteed to have bindings, because // the compiler treats static i18n attributes as regular attribute bindings. - generateBindingUpdateOpCodes(updateOpCodes, message, previousElementIndex, attrName); + // Since this may not be the first i18n attribute on this element we need to pass in how + // many previous bindings there have already been. + generateBindingUpdateOpCodes( + updateOpCodes, message, previousElementIndex, attrName, countBindings(updateOpCodes), + null); } } tView.data[index] = updateOpCodes; @@ -267,10 +271,12 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str * @param destinationNode Index of the destination node which will receive the binding. * @param attrName Name of the attribute, if the string belongs to an attribute. * @param sanitizeFn Sanitization function used to sanitize the string after update, if necessary. + * @param bindingStart The lView index of the next expression that can be bound via an opCode. + * @returns The mask value for these bindings */ -export function generateBindingUpdateOpCodes( - updateOpCodes: I18nUpdateOpCodes, str: string, destinationNode: number, attrName?: string, - sanitizeFn: SanitizerFn|null = null): number { +function generateBindingUpdateOpCodes( + updateOpCodes: I18nUpdateOpCodes, str: string, destinationNode: number, attrName: string|null, + bindingStart: number, sanitizeFn: SanitizerFn|null): number { ngDevMode && assertGreaterThanOrEqual( destinationNode, HEADER_OFFSET, 'Index must be in absolute LView offset'); @@ -289,7 +295,7 @@ export function generateBindingUpdateOpCodes( if (j & 1) { // Odd indexes are bindings - const bindingIndex = parseInt(textValue, 10); + const bindingIndex = bindingStart + parseInt(textValue, 10); updateOpCodes.push(-1 - bindingIndex); mask = mask | toMaskBit(bindingIndex); } else if (textValue !== '') { @@ -309,6 +315,28 @@ export function generateBindingUpdateOpCodes( return mask; } +/** + * Count the number of bindings in the given `opCodes`. + * + * It could be possible to speed this up, by passing the number of bindings found back from + * `generateBindingUpdateOpCodes()` to `i18nAttributesFirstPass()` but this would then require more + * complexity in the code and/or transient objects to be created. + * + * Since this function is only called once when the template is instantiated, is trivial in the + * first instance (since `opCodes` will be an empty array), and it is not common for elements to + * contain multiple i18n bound attributes, it seems like this is a reasonable compromise. + */ +function countBindings(opCodes: I18nUpdateOpCodes): number { + let count = 0; + for (let i = 0; i < opCodes.length; i++) { + const opCode = opCodes[i]; + // Bindings are negative numbers. + if (typeof opCode === 'number' && opCode < 0) { + count++; + } + } + return count; +} /** * Convert binding index to mask bit. @@ -595,12 +623,12 @@ function walkIcuTree( if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) { if (URI_ATTRS[lowerAttrName]) { generateBindingUpdateOpCodes( - update, attr.value, newIndex, attr.name, _sanitizeUrl); + update, attr.value, newIndex, attr.name, 0, _sanitizeUrl); } else if (SRCSET_ATTRS[lowerAttrName]) { generateBindingUpdateOpCodes( - update, attr.value, newIndex, attr.name, sanitizeSrcset); + update, attr.value, newIndex, attr.name, 0, sanitizeSrcset); } else { - generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name); + generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name, 0, null); } } else { ngDevMode && @@ -627,7 +655,8 @@ function walkIcuTree( addCreateNodeAndAppend(create, null, hasBinding ? '' : value, parentIdx, newIndex); addRemoveNode(remove, newIndex, depth); if (hasBinding) { - bindingMask = generateBindingUpdateOpCodes(update, value, newIndex) | bindingMask; + bindingMask = + generateBindingUpdateOpCodes(update, value, newIndex, null, 0, null) | bindingMask; } break; case Node.COMMENT_NODE: diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index b0648ad5c2536..e7d53802f7bfe 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -1624,17 +1624,23 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { }); it('multiple attributes', () => { - loadTranslations({[computeMsgId('hello {$INTERPOLATION}')]: 'bonjour {$INTERPOLATION}'}); + loadTranslations({ + [computeMsgId('hello {$INTERPOLATION} - {$INTERPOLATION_1}')]: + 'bonjour {$INTERPOLATION} - {$INTERPOLATION_1}', + [computeMsgId('bye {$INTERPOLATION} - {$INTERPOLATION_1}')]: + 'au revoir {$INTERPOLATION} - {$INTERPOLATION_1}', + }); const fixture = initWithTemplate( AppComp, - ``); + ``); expect(fixture.nativeElement.innerHTML) - .toEqual(``); + .toEqual(``); fixture.componentRef.instance.name = 'John'; + fixture.componentRef.instance.count = 5; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) - .toEqual(``); + .toEqual(``); }); it('on removed elements', () => { diff --git a/packages/core/test/render3/i18n/i18n_spec.ts b/packages/core/test/render3/i18n/i18n_spec.ts index 2577ffa2883e3..572c5248a15dc 100644 --- a/packages/core/test/render3/i18n/i18n_spec.ts +++ b/packages/core/test/render3/i18n/i18n_spec.ts @@ -458,9 +458,10 @@ describe('Runtime i18n', () => { }); it('for multiple attributes', () => { - const message = `Hello �0�!`; - const attrs = ['title', message, 'aria-label', message]; - const nbConsts = 2; + const message1 = `Hello �0� - �1�!`; + const message2 = `Bye �0� - �1�!`; + const attrs = ['title', message1, 'aria-label', message2]; + const nbConsts = 4; const index = 1; const opCodes = getOpCodes(attrs, () => { ɵɵelementStart(0, 'div'); @@ -469,11 +470,10 @@ describe('Runtime i18n', () => { }, undefined, nbConsts, HEADER_OFFSET + index); expect(opCodes).toEqual(matchDebug([ - `if (mask & 0b1) { (lView[${ - HEADER_OFFSET + 0}] as Element).setAttribute('title', \`Hello \$\{lView[i-1]}!\`); }`, - `if (mask & 0b1) { (lView[${ - HEADER_OFFSET + - 0}] as Element).setAttribute('aria-label', \`Hello \$\{lView[i-1]}!\`); }`, + `if (mask & 0b11) { (lView[${HEADER_OFFSET + 0}] as Element).` + + 'setAttribute(\'title\', `Hello ${lView[i-1]} - ${lView[i-2]}!`); }', + `if (mask & 0b1100) { (lView[${HEADER_OFFSET + 0}] as Element).` + + 'setAttribute(\'aria-label\', `Bye ${lView[i-3]} - ${lView[i-4]}!`); }', ])); }); });