diff --git a/crates/oxc_angular_compiler/src/transform/html_to_r3.rs b/crates/oxc_angular_compiler/src/transform/html_to_r3.rs index e6d2f9042..e4796d8dd 100644 --- a/crates/oxc_angular_compiler/src/transform/html_to_r3.rs +++ b/crates/oxc_angular_compiler/src/transform/html_to_r3.rs @@ -1377,6 +1377,14 @@ impl<'a> HtmlToR3Transform<'a> { }, ); } + HtmlNode::Element(element) => { + // Recurse into element children to extract interpolations inside HTML + // elements like `{{ expr }}` within ICU branches. + // Angular's i18n_parser.ts visitElement recursively visits children, + // so interpolations inside elements are correctly registered as + // placeholders. Without this recursion, these interpolations are lost. + self.extract_placeholders_from_nodes(&element.children, placeholders, vars); + } _ => {} } } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index cdd1a0cb8..5f471321b 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -4565,3 +4565,57 @@ fn test_pipe_in_binary_with_safe_nav_chain() { "pipeBind1 should appear exactly once. Found {pipe_count}. Output:\n{js}" ); } + +/// Tests that interpolations inside HTML elements within nested ICU plural branches +/// are correctly extracted as i18n expression placeholders. +/// +/// When ICU case text contains `{{ expr }}`, the interpolation is +/// inside an HTML element node. `extract_placeholders_from_nodes` must recurse into +/// element children to find these interpolations. Without this, they are silently +/// dropped, leading to fewer i18nExp calls than expected. +/// +/// This reproduces the undo-toast-items.component.ts mismatch where Angular emits 8 +/// i18nExp args but OXC only emitted 5 due to missing interpolations inside ``. +#[test] +fn test_i18n_nested_icu_with_interpolations_inside_elements() { + let js = compile_template_to_js( + r#"{count, plural, =1 {{{ name }} was deleted from {nestedCount, plural, =1 {{{ category }}} other {{{ category }} and {{ extra }} more}}} other {{{ count }} items deleted}}"#, + "TestComponent", + ); + + eprintln!("OUTPUT:\n{js}"); + + // All interpolation expressions must appear in the i18nExp chain. + // The expressions inside elements MUST be extracted: + // - name (inside in outer =1 branch) + // - category (inside in nested =1 branch) + // - category (inside in nested other branch) + // - extra (plain text in nested other branch) + // - count (plain text in outer other branch) + // Plus the ICU switch variables: + // - count (outer plural VAR) + // - nestedCount (inner plural VAR) + + // Check that the expressions inside elements are present + assert!( + js.contains("ctx.name"), + "ctx.name (inside in ICU) must be in i18nExp chain. Output:\n{js}" + ); + assert!( + js.contains("ctx.category"), + "ctx.category (inside in nested ICU) must be in i18nExp chain. Output:\n{js}" + ); + + // Count the total number of i18nExp arguments. + // There should be 7 expressions total: + // VAR: extra (innermost ICU), nestedCount (middle), count (outer) = 3 ICU vars + // INTERPOLATION: name, category, category, extra, count = varies + // The exact count depends on deduplication, but name and category must be present. + let i18n_exp_count = js.matches("i18nExp(").count(); + assert!( + i18n_exp_count >= 1, + "Should have at least one i18nExp call. Found {i18n_exp_count}. Output:\n{js}" + ); + + insta::assert_snapshot!("i18n_nested_icu_with_interpolations_inside_elements", js); +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__i18n_nested_icu_with_interpolations_inside_elements.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__i18n_nested_icu_with_interpolations_inside_elements.snap new file mode 100644 index 000000000..4521efca7 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__i18n_nested_icu_with_interpolations_inside_elements.snap @@ -0,0 +1,16 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵelementStart(0,"span"); + i0.ɵɵi18n(1,0); + i0.ɵɵelementEnd(); + } + if ((rf & 2)) { + i0.ɵɵadvance(); + i0.ɵɵi18nExp(ctx.nestedCount)(ctx.count)(ctx.name)(ctx.category)(ctx.extra)(ctx.count); + i0.ɵɵi18nApply(1); + } +}