Skip to content

Commit c19950c

Browse files
Brooooooklynclaude
andauthored
fix: i18n property bindings use I18n AttributeMarker in consts array (#1)
Property bindings with i18n-* markers (e.g., [heading]="title" i18n-heading) were extracted as BindingKind::Property (marker 3) instead of BindingKind::I18n (marker 6), causing const index mismatches with Angular's output. Two fixes: - Parser (html_to_r3.rs): pass i18n metadata from i18n-* attributes to bracket and bind- property bindings, matching Angular's categorizePropertyAttributes - attribute_extraction.rs: convert Property ops with i18n_message to BindingKind::I18n, ported from Angular's attribute_extraction.ts Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c1acbde commit c19950c

3 files changed

Lines changed: 108 additions & 11 deletions

File tree

crates/oxc_angular_compiler/src/pipeline/phases/attribute_extraction.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,29 @@ fn process_view_attributes<'a>(
187187
continue;
188188
}
189189

190+
// Determine the extracted binding kind.
191+
// Ported from Angular's attribute_extraction.ts lines 32-39:
192+
// if (op.i18nMessage !== null && op.templateKind === null) {
193+
// bindingKind = ir.BindingKind.I18n;
194+
// } else if (op.isStructuralTemplateAttribute) {
195+
// bindingKind = ir.BindingKind.Template;
196+
// } else {
197+
// bindingKind = ir.BindingKind.Property;
198+
// }
199+
let binding_kind = if prop_op.i18n_message.is_some()
200+
&& prop_op.binding_kind != BindingKind::Template
201+
{
202+
BindingKind::I18n
203+
} else {
204+
prop_op.binding_kind
205+
};
206+
190207
// Properties also generate extracted attributes for directive matching
191208
// Note: Property ops are NOT removed - they still need runtime updates
192-
// Use the actual binding_kind from the op (may be Template for structural directives)
193209
let extracted = ExtractedAttributeOp {
194210
base: CreateOpBase::default(),
195211
target: prop_op.target,
196-
binding_kind: prop_op.binding_kind,
212+
binding_kind,
197213
namespace: None,
198214
name: prop_op.name.clone(),
199215
value: None, // Property bindings don't copy the expression

crates/oxc_angular_compiler/src/transform/html_to_r3.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2605,13 +2605,16 @@ impl<'a> HtmlToR3Transform<'a> {
26052605
None,
26062606
));
26072607
} else {
2608-
inputs.push(self.create_bound_attribute(
2608+
let i18n = i18n_attrs_meta.remove(rest);
2609+
let mut bound_attr = self.create_bound_attribute(
26092610
element_name,
26102611
rest,
26112612
attr,
26122613
BindingType::Property,
26132614
None,
2614-
));
2615+
);
2616+
bound_attr.i18n = i18n;
2617+
inputs.push(bound_attr);
26152618
}
26162619
}
26172620
BindingPrefix::Let => {
@@ -2717,13 +2720,13 @@ impl<'a> HtmlToR3Transform<'a> {
27172720
} else {
27182721
(BindingType::Property, prop_name, None)
27192722
};
2720-
inputs.push(self.create_bound_attribute(
2721-
element_name,
2722-
final_name,
2723-
attr,
2724-
binding_type,
2725-
unit,
2726-
));
2723+
// Look up i18n metadata for this property binding (e.g., i18n-heading for [heading])
2724+
// Ported from Angular's categorizePropertyAttributes in r3_template_transform.ts
2725+
let i18n = i18n_attrs_meta.remove(final_name);
2726+
let mut bound_attr =
2727+
self.create_bound_attribute(element_name, final_name, attr, binding_type, unit);
2728+
bound_attr.i18n = i18n;
2729+
inputs.push(bound_attr);
27272730
continue;
27282731
}
27292732

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3572,3 +3572,81 @@ fn test_let_declaration_with_multiple_context_refs_variable_naming() {
35723572
"Context variable used by both @let and conditional should be named properly, not _unnamed_. Output:\n{js}"
35733573
);
35743574
}
3575+
3576+
// ============================================================================
3577+
// Const reference index: i18n property binding extraction
3578+
// ============================================================================
3579+
3580+
/// Tests that property bindings with i18n markers are extracted as BindingKind::I18n
3581+
/// in the consts array. Angular's attribute_extraction.ts checks `op.i18nMessage !== null`
3582+
/// on Property ops and converts them to BindingKind.I18n. Without this, the const entry
3583+
/// would be `[3, "heading"]` (Bindings marker) instead of `[6, "heading"]` (I18n marker),
3584+
/// causing const index mismatches.
3585+
#[test]
3586+
fn test_i18n_property_binding_extracted_as_i18n_kind() {
3587+
let allocator = Allocator::default();
3588+
let source = r#"
3589+
import { Component } from '@angular/core';
3590+
3591+
@Component({
3592+
selector: 'test-comp',
3593+
template: '<my-comp [heading]="title" i18n-heading="@@my-heading">content</my-comp>',
3594+
standalone: true,
3595+
})
3596+
export class TestComponent {
3597+
title = 'hello';
3598+
}
3599+
"#;
3600+
3601+
let result = transform_angular_file(
3602+
&allocator,
3603+
"test.component.ts",
3604+
source,
3605+
&ComponentTransformOptions::default(),
3606+
None,
3607+
);
3608+
3609+
// The consts array should contain [6,"heading"] (AttributeMarker.I18n = 6)
3610+
// not [3,"heading"] (AttributeMarker.Bindings = 3)
3611+
assert!(
3612+
result.code.contains(r#"6,"heading""#),
3613+
"Property binding with i18n marker should produce I18n AttributeMarker (6), not Bindings (3). Output:\n{}",
3614+
result.code
3615+
);
3616+
}
3617+
3618+
/// Tests that interpolated attributes with i18n markers (e.g., heading="{{ name }}" i18n-heading)
3619+
/// are extracted as BindingKind::I18n in the consts array.
3620+
/// This matches the real-world pattern in ClickUp's old-join-team component.
3621+
#[test]
3622+
fn test_i18n_interpolated_attribute_extracted_as_i18n_kind() {
3623+
let allocator = Allocator::default();
3624+
let source = r#"
3625+
import { Component } from '@angular/core';
3626+
3627+
@Component({
3628+
selector: 'test-comp',
3629+
template: '<my-comp heading="Join the {{ name }} Workspace" i18n-heading="@@join-workspace">content</my-comp>',
3630+
standalone: true,
3631+
})
3632+
export class TestComponent {
3633+
name = 'hello';
3634+
}
3635+
"#;
3636+
3637+
let result = transform_angular_file(
3638+
&allocator,
3639+
"test.component.ts",
3640+
source,
3641+
&ComponentTransformOptions::default(),
3642+
None,
3643+
);
3644+
3645+
// The consts array should contain [6,"heading"] (AttributeMarker.I18n = 6)
3646+
// not [3,"heading"] (AttributeMarker.Bindings = 3)
3647+
assert!(
3648+
result.code.contains(r#"6,"heading""#),
3649+
"Interpolated attribute with i18n marker should produce I18n AttributeMarker (6), not Bindings (3). Output:\n{}",
3650+
result.code
3651+
);
3652+
}

0 commit comments

Comments
 (0)