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 e4796d8dd..1e80f1b46 100644
--- a/crates/oxc_angular_compiler/src/transform/html_to_r3.rs
+++ b/crates/oxc_angular_compiler/src/transform/html_to_r3.rs
@@ -468,18 +468,27 @@ impl<'a> HtmlToR3Transform<'a> {
let selector = self.get_ng_content_selector(element);
self.ng_content_selectors.push(selector.clone());
- // For ng-content, include the structural directive attribute (*ngIf, etc.)
- // as a text attribute. This is needed because the projection instruction
- // includes these attributes in its output (e.g., ["*ngIf", "!subtitle()"]).
- // Reference: r3_template_transform.ts line 193 - all attrs are included
- let mut content_attributes = attributes;
- if let Some(ref tpl_attr) = template_attr {
+ // For ng-content, Angular converts ALL raw HTML attributes to TextAttributes.
+ // Reference: r3_template_transform.ts line 193:
+ // const attrs: t.TextAttribute[] = element.attrs.map((attr) => this.visitAttribute(attr));
+ // This includes bound attributes like [select]="..." which get serialized with
+ // their raw names (e.g., "[select]") and values into the projection instruction.
+ //
+ // However, i18n/i18n-* attributes are excluded because Angular's I18nMetaVisitor
+ // strips them from element.attrs before r3_template_transform runs.
+ let mut content_attributes: Vec<'a, R3TextAttribute<'a>> =
+ Vec::with_capacity_in(element.attrs.len(), self.allocator);
+ for attr in &element.attrs {
+ let name = attr.name.as_str();
+ if name == "i18n" || name.starts_with("i18n-") {
+ continue;
+ }
content_attributes.push(R3TextAttribute {
- name: tpl_attr.name.clone(),
- value: tpl_attr.value.clone(),
- source_span: tpl_attr.span,
- key_span: Some(tpl_attr.name_span),
- value_span: tpl_attr.value_span,
+ name: attr.name.clone(),
+ value: attr.value.clone(),
+ source_span: attr.span,
+ key_span: Some(attr.name_span),
+ value_span: attr.value_span,
i18n: None,
});
}
diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs
index 880a42e57..b14e4da31 100644
--- a/crates/oxc_angular_compiler/tests/integration_test.rs
+++ b/crates/oxc_angular_compiler/tests/integration_test.rs
@@ -756,6 +756,36 @@ fn test_ng_content_select() {
insta::assert_snapshot!("ng_content_select", js);
}
+#[test]
+fn test_ng_content_i18n_attr_not_in_projection() {
+ // Verify i18n/i18n-* attrs are NOT included in ng-content projection attributes.
+ // Angular's I18nMetaVisitor strips these before r3_template_transform runs.
+ let js = compile_template_to_js(
+ r#""#,
+ "TestComponent",
+ );
+ assert!(
+ !js.contains(r#""i18n""#),
+ "i18n attribute should not appear in projection output. Got:\n{js}"
+ );
+}
+
+#[test]
+fn test_ng_content_with_bound_select() {
+ // Tests that [select] binding on ng-content passes the binding name and value
+ // as attributes to the projection instruction.
+ // Angular treats ALL raw attrs on ng-content as TextAttributes, including bindings.
+ // [select] with brackets is NOT the same as the static `select` attribute for the
+ // CSS selector — the selector stays as "*" (wildcard).
+ // Expected: ɵɵprojectionDef() with no args (single wildcard),
+ // ɵɵprojection(0, 0, ["[select]", "'[slot=expanded-content]'"])
+ let js = compile_template_to_js(
+ r#""#,
+ "TestComponent",
+ );
+ insta::assert_snapshot!("ng_content_with_bound_select", js);
+}
+
#[test]
fn test_ng_content_with_ng_project_as() {
// Tests that ngProjectAs attribute generates the correct ProjectAs marker (5)
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap
new file mode 100644
index 000000000..ddaa731f2
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap
@@ -0,0 +1,11 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+const _c0 = ["*"];
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵprojectionDef();
+ i0.ɵɵprojection(0,0,["[select]","'[slot=expanded-content]'"]);
+ }
+}