Skip to content

Commit 5b3933f

Browse files
committed
fix(compiler): error when ng-content fallback has translated children (angular#63156)
Fixes that the pipeline wasn't processing the fallback content of `ng-content` for i18n which resulted in a compiler error further down the line. Fixes angular#63065. PR Close angular#63156
1 parent 9ac6385 commit 5b3933f

8 files changed

Lines changed: 162 additions & 0 deletions

File tree

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/GOLDEN_PARTIAL.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,40 @@ export declare class MyComponent {
10451045
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-cmp", never, {}, {}, never, never, true, never>;
10461046
}
10471047

1048+
/****************************************************************************************************
1049+
* PARTIAL FILE: ng_content_with_i18n_children.js
1050+
****************************************************************************************************/
1051+
import { Component } from '@angular/core';
1052+
import * as i0 from "@angular/core";
1053+
export class MyComponent {
1054+
}
1055+
MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1056+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
1057+
<ng-content>
1058+
<span i18n="@@MY_ID">a <b>b</b> c</span>
1059+
</ng-content>
1060+
`, isInline: true });
1061+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
1062+
type: Component,
1063+
args: [{
1064+
selector: 'my-component',
1065+
template: `
1066+
<ng-content>
1067+
<span i18n="@@MY_ID">a <b>b</b> c</span>
1068+
</ng-content>
1069+
`,
1070+
}]
1071+
}] });
1072+
1073+
/****************************************************************************************************
1074+
* PARTIAL FILE: ng_content_with_i18n_children.d.ts
1075+
****************************************************************************************************/
1076+
import * as i0 from "@angular/core";
1077+
export declare class MyComponent {
1078+
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
1079+
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, ["*"], true, never>;
1080+
}
1081+
10481082
/****************************************************************************************************
10491083
* PARTIAL FILE: last_elem_inside_i18n_block.js
10501084
****************************************************************************************************/

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/TEST_CASES.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,20 @@
283283
}
284284
]
285285
},
286+
{
287+
"description": "should handle ng-content with i18n children",
288+
"inputFiles": [
289+
"ng_content_with_i18n_children.ts"
290+
],
291+
"expectations": [
292+
{
293+
"extraChecks": [
294+
"verifyPlaceholdersIntegrity",
295+
"verifyUniqueConsts"
296+
]
297+
}
298+
]
299+
},
286300
{
287301
"description": "when the last element inside an i18n block has i18n attributes",
288302
"inputFiles": [
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const $_c0$ = ["*"];
2+
3+
function MyComponent_ProjectionFallback_0_Template(rf, ctx) {
4+
if (rf & 1) {
5+
$r3$.ɵɵdomElementStart(0, "span");
6+
$r3$.ɵɵi18nStart(1, 0);
7+
$r3$.ɵɵdomElement(2, "b");
8+
$r3$.ɵɵi18nEnd();
9+
$r3$.ɵɵdomElementEnd();
10+
}
11+
}
12+
13+
14+
15+
$r3$.ɵɵdefineComponent({
16+
17+
ngContentSelectors: $_c0$,
18+
decls: 2,
19+
vars: 0,
20+
consts: () => {
21+
__i18nMsg__('a {$startBoldText}b{$closeBoldText} c', [['closeBoldText', String.raw`\uFFFD/#2\uFFFD`], ['startBoldText', String.raw`\uFFFD#2\uFFFD`]], {original_code: {"closeBoldText": "</b>", "startBoldText": "<b>"}}, {id: 'MY_ID'})
22+
return [$i18n_0$];
23+
},
24+
template: function MyComponent_Template(rf, ctx) {
25+
if (rf & 1) {
26+
$r3$.ɵɵprojectionDef();
27+
$r3$.ɵɵprojection(0, 0, null, MyComponent_ProjectionFallback_0_Template, 3, 0);
28+
}
29+
},
30+
31+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
selector: 'my-component',
5+
template: `
6+
<ng-content>
7+
<span i18n="@@MY_ID">a <b>b</b> c</span>
8+
</ng-content>
9+
`,
10+
})
11+
export class MyComponent {}

packages/compiler/src/template/pipeline/ir/src/ops/create.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,8 @@ export interface ProjectionOp extends Op<CreateOp>, ConsumesSlotOpTrait {
11471147
sourceSpan: ParseSourceSpan;
11481148

11491149
fallbackView: XrefId | null;
1150+
1151+
fallbackViewI18nPlaceholder?: i18n.BlockPlaceholder;
11501152
}
11511153

11521154
export function createProjectionOp(

packages/compiler/src/template/pipeline/src/phases/propagate_i18n_blocks.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ function propagateI18nBlocksToTemplates(
7070
);
7171
}
7272
break;
73+
case ir.OpKind.Projection:
74+
if (op.fallbackView !== null) {
75+
subTemplateIndex = propagateI18nBlocksForView(
76+
unit.job.views.get(op.fallbackView)!,
77+
i18nBlock,
78+
op.fallbackViewI18nPlaceholder,
79+
subTemplateIndex,
80+
);
81+
}
82+
break;
7383
}
7484
}
7585
return subTemplateIndex;

packages/compiler/src/template/pipeline/src/phases/resolve_i18n_element_placeholders.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,38 @@ function resolvePlaceholdersForView(
128128
// Clear out the pending structural directive now that its been accounted for.
129129
pendingStructuralDirective = undefined;
130130
}
131+
132+
if (op.fallbackView !== null) {
133+
const view = job.views.get(op.fallbackView)!;
134+
if (op.fallbackViewI18nPlaceholder === undefined) {
135+
resolvePlaceholdersForView(job, view, i18nContexts, elements);
136+
} else {
137+
if (currentOps === null) {
138+
throw Error('i18n tag placeholder should only occur inside an i18n block');
139+
}
140+
recordTemplateStart(
141+
job,
142+
view,
143+
op.handle.slot!,
144+
op.fallbackViewI18nPlaceholder,
145+
currentOps.i18nContext,
146+
currentOps.i18nBlock,
147+
pendingStructuralDirective,
148+
);
149+
resolvePlaceholdersForView(job, view, i18nContexts, elements);
150+
recordTemplateClose(
151+
job,
152+
view,
153+
op.handle.slot!,
154+
op.fallbackViewI18nPlaceholder,
155+
currentOps.i18nContext,
156+
currentOps.i18nBlock,
157+
pendingStructuralDirective,
158+
);
159+
pendingStructuralDirective = undefined;
160+
}
161+
}
162+
131163
break;
132164
case ir.OpKind.ConditionalCreate:
133165
case ir.OpKind.ConditionalBranchCreate:

packages/core/test/acceptance/content_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10+
import {loadTranslations} from '@angular/localize';
1011
import {
1112
ChangeDetectorRef,
1213
Component,
@@ -2039,5 +2040,32 @@ describe('projection', () => {
20392040
);
20402041
},
20412042
);
2043+
2044+
it('should translate elements inside fallback content', () => {
2045+
@Component({
2046+
selector: 'projection',
2047+
template: `
2048+
<ng-content>
2049+
<span i18n="@@MY_ID">a <b>b</b> c</span>
2050+
</ng-content>
2051+
`,
2052+
})
2053+
class Projection {}
2054+
2055+
@Component({
2056+
imports: [Projection],
2057+
template: `<projection/>`,
2058+
})
2059+
class App {}
2060+
2061+
loadTranslations({
2062+
MY_ID: '1 {$START_BOLD_TEXT}2{$CLOSE_BOLD_TEXT} 3',
2063+
});
2064+
2065+
const fixture = TestBed.createComponent(App);
2066+
expect(getElementHtml(fixture.nativeElement)).toContain(
2067+
`<projection><span>1 <b>2</b> 3</span></projection>`,
2068+
);
2069+
});
20422070
});
20432071
});

0 commit comments

Comments
 (0)