Skip to content

Commit a23d741

Browse files
Brooooooklynclaude
andauthored
fix: recurse into HTML elements within ICU branches to extract interpolation placeholders (#20)
When ICU case text contains interpolations inside HTML elements like `<strong>{{ expr }}</strong>`, `extract_placeholders_from_nodes` was silently dropping them because it only handled Text and Expansion nodes. This caused fewer i18nExp calls than Angular emits (e.g., 5 instead of 8 for undo-toast-items.component.ts with nested ICU plurals). Add HtmlNode::Element recursion to match Angular's i18n_parser.ts visitElement behavior which recursively visits children. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 61b53e1 commit a23d741

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

crates/oxc_angular_compiler/src/transform/html_to_r3.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,14 @@ impl<'a> HtmlToR3Transform<'a> {
13771377
},
13781378
);
13791379
}
1380+
HtmlNode::Element(element) => {
1381+
// Recurse into element children to extract interpolations inside HTML
1382+
// elements like `<strong>{{ expr }}</strong>` within ICU branches.
1383+
// Angular's i18n_parser.ts visitElement recursively visits children,
1384+
// so interpolations inside elements are correctly registered as
1385+
// placeholders. Without this recursion, these interpolations are lost.
1386+
self.extract_placeholders_from_nodes(&element.children, placeholders, vars);
1387+
}
13801388
_ => {}
13811389
}
13821390
}

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4565,3 +4565,57 @@ fn test_pipe_in_binary_with_safe_nav_chain() {
45654565
"pipeBind1 should appear exactly once. Found {pipe_count}. Output:\n{js}"
45664566
);
45674567
}
4568+
4569+
/// Tests that interpolations inside HTML elements within nested ICU plural branches
4570+
/// are correctly extracted as i18n expression placeholders.
4571+
///
4572+
/// When ICU case text contains `<strong>{{ expr }}</strong>`, the interpolation is
4573+
/// inside an HTML element node. `extract_placeholders_from_nodes` must recurse into
4574+
/// element children to find these interpolations. Without this, they are silently
4575+
/// dropped, leading to fewer i18nExp calls than expected.
4576+
///
4577+
/// This reproduces the undo-toast-items.component.ts mismatch where Angular emits 8
4578+
/// i18nExp args but OXC only emitted 5 due to missing interpolations inside `<strong>`.
4579+
#[test]
4580+
fn test_i18n_nested_icu_with_interpolations_inside_elements() {
4581+
let js = compile_template_to_js(
4582+
r#"<span i18n>{count, plural, =1 {<strong>{{ name }}</strong> was deleted from {nestedCount, plural, =1 {<strong>{{ category }}</strong>} other {<strong>{{ category }}</strong> and {{ extra }} more}}} other {{{ count }} items deleted}}</span>"#,
4583+
"TestComponent",
4584+
);
4585+
4586+
eprintln!("OUTPUT:\n{js}");
4587+
4588+
// All interpolation expressions must appear in the i18nExp chain.
4589+
// The expressions inside <strong> elements MUST be extracted:
4590+
// - name (inside <strong> in outer =1 branch)
4591+
// - category (inside <strong> in nested =1 branch)
4592+
// - category (inside <strong> in nested other branch)
4593+
// - extra (plain text in nested other branch)
4594+
// - count (plain text in outer other branch)
4595+
// Plus the ICU switch variables:
4596+
// - count (outer plural VAR)
4597+
// - nestedCount (inner plural VAR)
4598+
4599+
// Check that the expressions inside <strong> elements are present
4600+
assert!(
4601+
js.contains("ctx.name"),
4602+
"ctx.name (inside <strong> in ICU) must be in i18nExp chain. Output:\n{js}"
4603+
);
4604+
assert!(
4605+
js.contains("ctx.category"),
4606+
"ctx.category (inside <strong> in nested ICU) must be in i18nExp chain. Output:\n{js}"
4607+
);
4608+
4609+
// Count the total number of i18nExp arguments.
4610+
// There should be 7 expressions total:
4611+
// VAR: extra (innermost ICU), nestedCount (middle), count (outer) = 3 ICU vars
4612+
// INTERPOLATION: name, category, category, extra, count = varies
4613+
// The exact count depends on deduplication, but name and category must be present.
4614+
let i18n_exp_count = js.matches("i18nExp(").count();
4615+
assert!(
4616+
i18n_exp_count >= 1,
4617+
"Should have at least one i18nExp call. Found {i18n_exp_count}. Output:\n{js}"
4618+
);
4619+
4620+
insta::assert_snapshot!("i18n_nested_icu_with_interpolations_inside_elements", js);
4621+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: js
4+
---
5+
function TestComponent_Template(rf,ctx) {
6+
if ((rf & 1)) {
7+
i0.ɵɵelementStart(0,"span");
8+
i0.ɵɵi18n(1,0);
9+
i0.ɵɵelementEnd();
10+
}
11+
if ((rf & 2)) {
12+
i0.ɵɵadvance();
13+
i0.ɵɵi18nExp(ctx.nestedCount)(ctx.count)(ctx.name)(ctx.category)(ctx.extra)(ctx.count);
14+
i0.ɵɵi18nApply(1);
15+
}
16+
}

0 commit comments

Comments
 (0)