fix(ivy): process property bindings in i18n blocks similar to non-i18n bindings (#28969)
Prior to this change i18n block bindings were converted to Expressions right away (once we first access them), when in non-i18n cases we processed them differently: the actual conversion happens at instructions generation. Because of this discrepancy, the output for bindings in i18n blocks was generated incorrectly (with invalid indicies in pipeBindN fns and invalid references to non-existent local variables). Now the bindings processing is unified and i18nExp instructions should contain right bind expressions. PR Close #28969
This commit is contained in:
parent
34bdebcdd2
commit
40833ba54b
|
@ -764,6 +764,37 @@ describe('i18n support in the view compiler', () => {
|
||||||
verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}});
|
verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support interpolations with complex expressions', () => {
|
||||||
|
const input = `
|
||||||
|
<div i18n>
|
||||||
|
{{ valueA | async }}
|
||||||
|
{{ valueA?.a?.b }}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
const $MSG_EXTERNAL_1482713963707913023$$APP_SPEC_TS_0$ = goog.getMsg(" {$interpolation} {$interpolation_1} ", {
|
||||||
|
"interpolation": "\uFFFD0\uFFFD",
|
||||||
|
"interpolation_1": "\uFFFD1\uFFFD"
|
||||||
|
});
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18n(1, $MSG_EXTERNAL_1482713963707913023$$APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵpipe(2, "async");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(2, 2, ctx.valueA)));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(((ctx.valueA == null) ? null : ((ctx.valueA.a == null) ? null : ctx.valueA.a.b))));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle i18n attributes with bindings in content', () => {
|
it('should handle i18n attributes with bindings in content', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div i18n>My i18n block #{{ one }}</div>
|
<div i18n>My i18n block #{{ one }}</div>
|
||||||
|
@ -800,7 +831,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
||||||
$r3$.ɵi18nApply(1);
|
$r3$.ɵi18nApply(1);
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 0, ctx.two)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 3, ctx.two)));
|
||||||
$r3$.ɵi18nApply(3);
|
$r3$.ɵi18nApply(3);
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(((ctx.three + ctx.four) + ctx.five)));
|
$r3$.ɵi18nExp($r3$.ɵbind(((ctx.three + ctx.four) + ctx.five)));
|
||||||
$r3$.ɵi18nApply(6);
|
$r3$.ɵi18nApply(6);
|
||||||
|
@ -868,7 +899,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
||||||
$r3$.ɵi18nApply(1);
|
$r3$.ɵi18nApply(1);
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(5, 0, ctx.two)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(5, 3, ctx.two)));
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(ctx.nestedInBlockTwo));
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.nestedInBlockTwo));
|
||||||
$r3$.ɵi18nApply(4);
|
$r3$.ɵi18nApply(4);
|
||||||
}
|
}
|
||||||
|
@ -941,7 +972,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
$r3$.ɵi18nApply(1);
|
$r3$.ɵi18nApply(1);
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueE));
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueE));
|
||||||
$r3$.ɵi18nApply(8);
|
$r3$.ɵi18nApply(8);
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(6, 0, ctx.valueD)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(6, 5, ctx.valueD)));
|
||||||
$r3$.ɵi18nApply(5);
|
$r3$.ɵi18nApply(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -988,7 +1019,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
const $ctx_r0$ = $r3$.ɵnextContext();
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r0$.valueA));
|
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r0$.valueA));
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 0, $ctx_r0$.valueB)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 2, $ctx_r0$.valueB)));
|
||||||
$r3$.ɵi18nApply(2);
|
$r3$.ɵi18nApply(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1118,7 +1149,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
const $ctx_r0$ = $r3$.ɵnextContext();
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵelementProperty(4, "ngIf", $r3$.ɵbind($ctx_r0$.exists));
|
$r3$.ɵelementProperty(4, "ngIf", $r3$.ɵbind($ctx_r0$.exists));
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r0$.valueA));
|
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r0$.valueA));
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 0, $ctx_r0$.valueB)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 3, $ctx_r0$.valueB)));
|
||||||
$r3$.ɵi18nApply(0);
|
$r3$.ɵi18nApply(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1148,7 +1179,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
const $ctx_r1$ = $r3$.ɵnextContext();
|
const $ctx_r1$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind(($ctx_r1$.valueE + $ctx_r1$.valueF)));
|
$r3$.ɵi18nExp($r3$.ɵbind(($ctx_r1$.valueE + $ctx_r1$.valueF)));
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 0, $ctx_r1$.valueG)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 2, $ctx_r1$.valueG)));
|
||||||
$r3$.ɵi18nApply(0);
|
$r3$.ɵi18nApply(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1347,7 +1378,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
$r3$.ɵelementContainerEnd();
|
$r3$.ɵelementContainerEnd();
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(2, 0, ctx.valueA)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(2, 1, ctx.valueA)));
|
||||||
$r3$.ɵi18nApply(1);
|
$r3$.ɵi18nApply(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1371,7 +1402,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
$r3$.ɵpipe(1, "uppercase");
|
$r3$.ɵpipe(1, "uppercase");
|
||||||
} if (rf & 2) {
|
} if (rf & 2) {
|
||||||
const $ctx_r0$ = $r3$.ɵnextContext();
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 0, $ctx_r0$.valueA)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 1, $ctx_r0$.valueA)));
|
||||||
$r3$.ɵi18nApply(0);
|
$r3$.ɵi18nApply(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1412,7 +1443,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
const $ctx_r0$ = $r3$.ɵnextContext();
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 0, $ctx_r0$.valueA)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 1, $ctx_r0$.valueA)));
|
||||||
$r3$.ɵi18nApply(0);
|
$r3$.ɵi18nApply(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1431,7 +1462,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
$r3$.ɵelementEnd();
|
$r3$.ɵelementEnd();
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 0, ctx.valueB)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 1, ctx.valueB)));
|
||||||
$r3$.ɵi18nApply(1);
|
$r3$.ɵi18nApply(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1540,7 +1571,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
const $ctx_r0$ = $r3$.ɵnextContext();
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 0, $ctx_r0$.valueA)));
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 1, $ctx_r0$.valueA)));
|
||||||
$r3$.ɵi18nApply(0);
|
$r3$.ɵi18nApply(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AST} from '../../../expression_parser/ast';
|
||||||
import * as i18n from '../../../i18n/i18n_ast';
|
import * as i18n from '../../../i18n/i18n_ast';
|
||||||
import * as o from '../../../output/output_ast';
|
import * as o from '../../../output/output_ast';
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ function setupRegistry() {
|
||||||
*/
|
*/
|
||||||
export class I18nContext {
|
export class I18nContext {
|
||||||
public readonly id: number;
|
public readonly id: number;
|
||||||
public bindings = new Set<o.Expression>();
|
public bindings = new Set<AST>();
|
||||||
public placeholders = new Map<string, any[]>();
|
public placeholders = new Map<string, any[]>();
|
||||||
public isEmitted: boolean = false;
|
public isEmitted: boolean = false;
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ export class I18nContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
// public API to accumulate i18n-related content
|
// public API to accumulate i18n-related content
|
||||||
appendBinding(binding: o.Expression) { this.bindings.add(binding); }
|
appendBinding(binding: AST) { this.bindings.add(binding); }
|
||||||
appendIcu(name: string, ref: o.Expression) {
|
appendIcu(name: string, ref: o.Expression) {
|
||||||
updatePlaceholderMap(this._registry.icus, name, ref);
|
updatePlaceholderMap(this._registry.icus, name, ref);
|
||||||
}
|
}
|
||||||
|
|
|
@ -329,12 +329,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
i18nAppendBindings(expressions: AST[]) {
|
i18nAppendBindings(expressions: AST[]) {
|
||||||
if (!this.i18n || !expressions.length) return;
|
if (expressions.length > 0) {
|
||||||
const implicit = o.variable(CONTEXT_NAME);
|
expressions.forEach(expression => this.i18n !.appendBinding(expression));
|
||||||
expressions.forEach(expression => {
|
}
|
||||||
const binding = this.convertExpressionBinding(implicit, expression);
|
|
||||||
this.i18n !.appendBinding(binding);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i18nBindProps(props: {[key: string]: t.Text | t.BoundText}): {[key: string]: o.Expression} {
|
i18nBindProps(props: {[key: string]: t.Text | t.BoundText}): {[key: string]: o.Expression} {
|
||||||
|
@ -452,7 +449,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
// setup accumulated bindings
|
// setup accumulated bindings
|
||||||
const {index, bindings} = this.i18n;
|
const {index, bindings} = this.i18n;
|
||||||
if (bindings.size) {
|
if (bindings.size) {
|
||||||
bindings.forEach(binding => this.updateInstruction(span, R3.i18nExp, [binding]));
|
bindings.forEach(binding => {
|
||||||
|
this.updateInstruction(
|
||||||
|
span, R3.i18nExp,
|
||||||
|
() => [this.convertPropertyBinding(o.variable(CONTEXT_NAME), binding)]);
|
||||||
|
});
|
||||||
this.updateInstruction(span, R3.i18nApply, [o.literal(index)]);
|
this.updateInstruction(span, R3.i18nApply, [o.literal(index)]);
|
||||||
}
|
}
|
||||||
if (!selfClosing) {
|
if (!selfClosing) {
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AST} from '../../../src/expression_parser/ast';
|
||||||
|
import {Lexer} from '../../../src/expression_parser/lexer';
|
||||||
|
import {Parser} from '../../../src/expression_parser/parser';
|
||||||
import * as i18n from '../../../src/i18n/i18n_ast';
|
import * as i18n from '../../../src/i18n/i18n_ast';
|
||||||
import * as o from '../../../src/output/output_ast';
|
import * as o from '../../../src/output/output_ast';
|
||||||
import * as t from '../../../src/render3/r3_ast';
|
import * as t from '../../../src/render3/r3_ast';
|
||||||
|
@ -15,6 +18,7 @@ import {I18nMeta, formatI18nPlaceholderName, parseI18nMeta} from '../../../src/r
|
||||||
|
|
||||||
import {parseR3 as parse} from './util';
|
import {parseR3 as parse} from './util';
|
||||||
|
|
||||||
|
const expressionParser = new Parser(new Lexer());
|
||||||
const i18nOf = (element: t.Node & {i18n?: i18n.AST}) => element.i18n !;
|
const i18nOf = (element: t.Node & {i18n?: i18n.AST}) => element.i18n !;
|
||||||
|
|
||||||
describe('I18nContext', () => {
|
describe('I18nContext', () => {
|
||||||
|
@ -44,8 +48,8 @@ describe('I18nContext', () => {
|
||||||
|
|
||||||
// binding collection checks
|
// binding collection checks
|
||||||
expect(ctx.bindings.size).toBe(0);
|
expect(ctx.bindings.size).toBe(0);
|
||||||
ctx.appendBinding(o.literal(1));
|
ctx.appendBinding(expressionParser.parseInterpolation('{{ valueA }}', '') as AST);
|
||||||
ctx.appendBinding(o.literal(2));
|
ctx.appendBinding(expressionParser.parseInterpolation('{{ valueB }}', '') as AST);
|
||||||
expect(ctx.bindings.size).toBe(2);
|
expect(ctx.bindings.size).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -72,7 +76,7 @@ describe('I18nContext', () => {
|
||||||
|
|
||||||
// set data for root ctx
|
// set data for root ctx
|
||||||
ctx.appendBoundText(i18nOf(boundTextA));
|
ctx.appendBoundText(i18nOf(boundTextA));
|
||||||
ctx.appendBinding(o.literal('valueA'));
|
ctx.appendBinding(expressionParser.parseInterpolation('{{ valueA }}', '') as AST);
|
||||||
ctx.appendElement(i18nOf(elementA), 0);
|
ctx.appendElement(i18nOf(elementA), 0);
|
||||||
ctx.appendTemplate(i18nOf(templateA), 1);
|
ctx.appendTemplate(i18nOf(templateA), 1);
|
||||||
ctx.appendElement(i18nOf(elementA), 0, true);
|
ctx.appendElement(i18nOf(elementA), 0, true);
|
||||||
|
@ -88,11 +92,11 @@ describe('I18nContext', () => {
|
||||||
// set data for child context
|
// set data for child context
|
||||||
childCtx.appendElement(i18nOf(elementB), 0);
|
childCtx.appendElement(i18nOf(elementB), 0);
|
||||||
childCtx.appendBoundText(i18nOf(boundTextB));
|
childCtx.appendBoundText(i18nOf(boundTextB));
|
||||||
childCtx.appendBinding(o.literal('valueB'));
|
childCtx.appendBinding(expressionParser.parseInterpolation('{{ valueB }}', '') as AST);
|
||||||
childCtx.appendElement(i18nOf(elementC), 1);
|
childCtx.appendElement(i18nOf(elementC), 1);
|
||||||
childCtx.appendElement(i18nOf(elementC), 1, true);
|
childCtx.appendElement(i18nOf(elementC), 1, true);
|
||||||
childCtx.appendBoundText(i18nOf(boundTextC));
|
childCtx.appendBoundText(i18nOf(boundTextC));
|
||||||
childCtx.appendBinding(o.literal('valueC'));
|
childCtx.appendBinding(expressionParser.parseInterpolation('{{ valueC }}', '') as AST);
|
||||||
childCtx.appendElement(i18nOf(elementB), 0, true);
|
childCtx.appendElement(i18nOf(elementB), 0, true);
|
||||||
|
|
||||||
expect(childCtx.bindings.size).toBe(2);
|
expect(childCtx.bindings.size).toBe(2);
|
||||||
|
|
|
@ -23,6 +23,7 @@ class DirectiveWithTplRef {
|
||||||
class MyComp {
|
class MyComp {
|
||||||
name = 'John';
|
name = 'John';
|
||||||
items = ['1', '2', '3'];
|
items = ['1', '2', '3'];
|
||||||
|
obj = {a: {b: 'value'}};
|
||||||
visible = true;
|
visible = true;
|
||||||
age = 20;
|
age = 20;
|
||||||
count = 2;
|
count = 2;
|
||||||
|
@ -42,6 +43,7 @@ const TRANSLATIONS: any = {
|
||||||
'Item {$interpolation}': 'Article {$interpolation}',
|
'Item {$interpolation}': 'Article {$interpolation}',
|
||||||
'\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"',
|
'\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"',
|
||||||
'My logo': 'Mon logo',
|
'My logo': 'Mon logo',
|
||||||
|
'{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)',
|
||||||
'{$startTagSpan}My logo{$tagImg}{$closeTagSpan}':
|
'{$startTagSpan}My logo{$tagImg}{$closeTagSpan}':
|
||||||
'{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}',
|
'{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}',
|
||||||
'{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}':
|
'{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}':
|
||||||
|
@ -175,6 +177,13 @@ onlyInIvy('Ivy i18n logic').describe('i18n', function() {
|
||||||
expect(element).toHaveText('Bonjour John');
|
expect(element).toHaveText('Bonjour John');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support interpolations with complex expressions', () => {
|
||||||
|
const template = `<div i18n>{{ name | uppercase }} - {{ obj?.a?.b }}</div>`;
|
||||||
|
const fixture = getFixtureWithOverrides({template});
|
||||||
|
const element = fixture.nativeElement.firstChild;
|
||||||
|
expect(element).toHaveText('JOHN - value (fr)');
|
||||||
|
});
|
||||||
|
|
||||||
it('should properly escape quotes in content', () => {
|
it('should properly escape quotes in content', () => {
|
||||||
const content = `'Single quotes' and "Double quotes"`;
|
const content = `'Single quotes' and "Double quotes"`;
|
||||||
const template = `<div i18n>${content}</div>`;
|
const template = `<div i18n>${content}</div>`;
|
||||||
|
|
Loading…
Reference in New Issue