diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index 9c6bc08e90507..ce7d844e8d984 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -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, null, + countBindings(updateOpCodes)); } } 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 { + sanitizeFn: SanitizerFn|null = null, bindingStart = 0): 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. diff --git a/packages/core/test/render3/i18n/i18n_spec.ts b/packages/core/test/render3/i18n/i18n_spec.ts index 70666f4257a83..5c9136545e847 100644 --- a/packages/core/test/render3/i18n/i18n_spec.ts +++ b/packages/core/test/render3/i18n/i18n_spec.ts @@ -449,9 +449,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'); @@ -460,8 +461,8 @@ describe('Runtime i18n', () => { }, undefined, nbConsts, HEADER_OFFSET + index); expect(opCodes).toEqual(matchDebug([ - 'if (mask & 0b1) { (lView[21] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }', - 'if (mask & 0b1) { (lView[21] as Element).setAttribute(\'aria-label\', `Hello ${lView[i-1]}!`); }', + 'if (mask & 0b11) { (lView[21] as Element).setAttribute(\'title\', `Hello ${lView[i-1]} - ${lView[i-2]}!`); }', + 'if (mask & 0b1100) { (lView[21] as Element).setAttribute(\'aria-label\', `Bye ${lView[i-3]} - ${lView[i-4]}!`); }', ])); }); });