Skip to content

Commit 73c6c64

Browse files
petebacondarwinmhevery
authored andcommitted
fix(core): handle multiple i18n attributes with expression bindings (#41882)
When there are multiple attributes that are marked for i18n translation, which contain expression bindings, we were generating i18n update op-codes that did not accurately map to the correct value to be bound in the lView. Each attribute's bindings were relative to the start of the lView first attributes values rather than to their own. This fix passes the current binding index to `generateBindingUpdateOpCodes()` when processing i18n attributes to account for this. Fixes #41869 PR Close #41882
1 parent 36a0358 commit 73c6c64

File tree

3 files changed

+57
-22
lines changed

3 files changed

+57
-22
lines changed

packages/core/src/render3/i18n/i18n_parse.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ function i18nStartFirstCreatePassProcessTextNode(
219219
const tNode = createTNodeAndAddOpCode(
220220
tView, rootTNode, existingTNodes, lView, createOpCodes, hasBinding ? null : text, false);
221221
if (hasBinding) {
222-
generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index);
222+
generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index, null, 0, null);
223223
}
224224
}
225225

@@ -251,7 +251,11 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str
251251

252252
// i18n attributes that hit this code path are guaranteed to have bindings, because
253253
// the compiler treats static i18n attributes as regular attribute bindings.
254-
generateBindingUpdateOpCodes(updateOpCodes, message, previousElementIndex, attrName);
254+
// Since this may not be the first i18n attribute on this element we need to pass in how
255+
// many previous bindings there have already been.
256+
generateBindingUpdateOpCodes(
257+
updateOpCodes, message, previousElementIndex, attrName, countBindings(updateOpCodes),
258+
null);
255259
}
256260
}
257261
tView.data[index] = updateOpCodes;
@@ -267,10 +271,12 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str
267271
* @param destinationNode Index of the destination node which will receive the binding.
268272
* @param attrName Name of the attribute, if the string belongs to an attribute.
269273
* @param sanitizeFn Sanitization function used to sanitize the string after update, if necessary.
274+
* @param bindingStart The lView index of the next expression that can be bound via an opCode.
275+
* @returns The mask value for these bindings
270276
*/
271-
export function generateBindingUpdateOpCodes(
272-
updateOpCodes: I18nUpdateOpCodes, str: string, destinationNode: number, attrName?: string,
273-
sanitizeFn: SanitizerFn|null = null): number {
277+
function generateBindingUpdateOpCodes(
278+
updateOpCodes: I18nUpdateOpCodes, str: string, destinationNode: number, attrName: string|null,
279+
bindingStart: number, sanitizeFn: SanitizerFn|null): number {
274280
ngDevMode &&
275281
assertGreaterThanOrEqual(
276282
destinationNode, HEADER_OFFSET, 'Index must be in absolute LView offset');
@@ -289,7 +295,7 @@ export function generateBindingUpdateOpCodes(
289295

290296
if (j & 1) {
291297
// Odd indexes are bindings
292-
const bindingIndex = parseInt(textValue, 10);
298+
const bindingIndex = bindingStart + parseInt(textValue, 10);
293299
updateOpCodes.push(-1 - bindingIndex);
294300
mask = mask | toMaskBit(bindingIndex);
295301
} else if (textValue !== '') {
@@ -309,6 +315,28 @@ export function generateBindingUpdateOpCodes(
309315
return mask;
310316
}
311317

318+
/**
319+
* Count the number of bindings in the given `opCodes`.
320+
*
321+
* It could be possible to speed this up, by passing the number of bindings found back from
322+
* `generateBindingUpdateOpCodes()` to `i18nAttributesFirstPass()` but this would then require more
323+
* complexity in the code and/or transient objects to be created.
324+
*
325+
* Since this function is only called once when the template is instantiated, is trivial in the
326+
* first instance (since `opCodes` will be an empty array), and it is not common for elements to
327+
* contain multiple i18n bound attributes, it seems like this is a reasonable compromise.
328+
*/
329+
function countBindings(opCodes: I18nUpdateOpCodes): number {
330+
let count = 0;
331+
for (let i = 0; i < opCodes.length; i++) {
332+
const opCode = opCodes[i];
333+
// Bindings are negative numbers.
334+
if (typeof opCode === 'number' && opCode < 0) {
335+
count++;
336+
}
337+
}
338+
return count;
339+
}
312340

313341
/**
314342
* Convert binding index to mask bit.
@@ -595,12 +623,12 @@ function walkIcuTree(
595623
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
596624
if (URI_ATTRS[lowerAttrName]) {
597625
generateBindingUpdateOpCodes(
598-
update, attr.value, newIndex, attr.name, _sanitizeUrl);
626+
update, attr.value, newIndex, attr.name, 0, _sanitizeUrl);
599627
} else if (SRCSET_ATTRS[lowerAttrName]) {
600628
generateBindingUpdateOpCodes(
601-
update, attr.value, newIndex, attr.name, sanitizeSrcset);
629+
update, attr.value, newIndex, attr.name, 0, sanitizeSrcset);
602630
} else {
603-
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name);
631+
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name, 0, null);
604632
}
605633
} else {
606634
ngDevMode &&
@@ -627,7 +655,8 @@ function walkIcuTree(
627655
addCreateNodeAndAppend(create, null, hasBinding ? '' : value, parentIdx, newIndex);
628656
addRemoveNode(remove, newIndex, depth);
629657
if (hasBinding) {
630-
bindingMask = generateBindingUpdateOpCodes(update, value, newIndex) | bindingMask;
658+
bindingMask =
659+
generateBindingUpdateOpCodes(update, value, newIndex, null, 0, null) | bindingMask;
631660
}
632661
break;
633662
case Node.COMMENT_NODE:

packages/core/test/acceptance/i18n_spec.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,17 +1624,23 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
16241624
});
16251625

16261626
it('multiple attributes', () => {
1627-
loadTranslations({[computeMsgId('hello {$INTERPOLATION}')]: 'bonjour {$INTERPOLATION}'});
1627+
loadTranslations({
1628+
[computeMsgId('hello {$INTERPOLATION} - {$INTERPOLATION_1}')]:
1629+
'bonjour {$INTERPOLATION} - {$INTERPOLATION_1}',
1630+
[computeMsgId('bye {$INTERPOLATION} - {$INTERPOLATION_1}')]:
1631+
'au revoir {$INTERPOLATION} - {$INTERPOLATION_1}',
1632+
});
16281633
const fixture = initWithTemplate(
16291634
AppComp,
1630-
`<input i18n i18n-title title="hello {{name}}" i18n-placeholder placeholder="hello {{name}}">`);
1635+
`<input i18n i18n-title title="hello {{name}} - {{count}}" i18n-placeholder placeholder="bye {{count}} - {{name}}">`);
16311636
expect(fixture.nativeElement.innerHTML)
1632-
.toEqual(`<input title="bonjour Angular" placeholder="bonjour Angular">`);
1637+
.toEqual(`<input title="bonjour Angular - 0" placeholder="au revoir 0 - Angular">`);
16331638

16341639
fixture.componentRef.instance.name = 'John';
1640+
fixture.componentRef.instance.count = 5;
16351641
fixture.detectChanges();
16361642
expect(fixture.nativeElement.innerHTML)
1637-
.toEqual(`<input title="bonjour John" placeholder="bonjour John">`);
1643+
.toEqual(`<input title="bonjour John - 5" placeholder="au revoir 5 - John">`);
16381644
});
16391645

16401646
it('on removed elements', () => {

packages/core/test/render3/i18n/i18n_spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,10 @@ describe('Runtime i18n', () => {
458458
});
459459

460460
it('for multiple attributes', () => {
461-
const message = `Hello �0�!`;
462-
const attrs = ['title', message, 'aria-label', message];
463-
const nbConsts = 2;
461+
const message1 = `Hello �0� - �1�!`;
462+
const message2 = `Bye �0� - �1�!`;
463+
const attrs = ['title', message1, 'aria-label', message2];
464+
const nbConsts = 4;
464465
const index = 1;
465466
const opCodes = getOpCodes(attrs, () => {
466467
ɵɵelementStart(0, 'div');
@@ -469,11 +470,10 @@ describe('Runtime i18n', () => {
469470
}, undefined, nbConsts, HEADER_OFFSET + index);
470471

471472
expect(opCodes).toEqual(matchDebug([
472-
`if (mask & 0b1) { (lView[${
473-
HEADER_OFFSET + 0}] as Element).setAttribute('title', \`Hello \$\{lView[i-1]}!\`); }`,
474-
`if (mask & 0b1) { (lView[${
475-
HEADER_OFFSET +
476-
0}] as Element).setAttribute('aria-label', \`Hello \$\{lView[i-1]}!\`); }`,
473+
`if (mask & 0b11) { (lView[${HEADER_OFFSET + 0}] as Element).` +
474+
'setAttribute(\'title\', `Hello ${lView[i-1]} - ${lView[i-2]}!`); }',
475+
`if (mask & 0b1100) { (lView[${HEADER_OFFSET + 0}] as Element).` +
476+
'setAttribute(\'aria-label\', `Bye ${lView[i-3]} - ${lView[i-4]}!`); }',
477477
]));
478478
});
479479
});

0 commit comments

Comments
 (0)