diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts
index 9c6bc08e90..16edade313 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 b0648ad5c2..e7d53802f7 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 2577ffa288..572c5248a1 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]}!`); }',
]));
});
});