Skip to content

Commit 55ed478

Browse files
Brooooooklynclaude
andauthored
fix: i18n interpolated attributes use I18n marker and fix build-test script (#17)
Interpolated attributes with i18n markers (e.g., heading="Join {{ name }}" i18n-heading) now correctly use BindingKind::I18n (AttributeMarker 6) instead of BindingKind::Property (AttributeMarker 3). Pure property bindings ([attr]="expr" i18n-attr) keep the Property marker since the runtime uses domProperty, not i18nAttributes. Also fixes the build-test script which was broken due to double --features flags, and adds the build:ts step for TypeScript compilation. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7cdb9a4 commit 55ed478

3 files changed

Lines changed: 87 additions & 32 deletions

File tree

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,22 @@ fn process_view_attributes<'a>(
187187
continue;
188188
}
189189

190-
// Use the binding kind from the property op directly.
191-
// Angular's attribute_extraction.ts has a condition:
190+
// Angular's attribute_extraction.ts (lines 31-40):
192191
// if (op.i18nMessage !== null && op.templateKind === null)
193-
// that sets bindingKind to I18n, but empirically the Angular
194-
// compiler never produces I18n AttributeMarker (6) in consts
195-
// arrays. The templateKind guard (which OXC's PropertyOp lacks)
196-
// prevents it from triggering in practice.
197-
let binding_kind = prop_op.binding_kind;
192+
// bindingKind = ir.BindingKind.I18n;
193+
//
194+
// The I18n binding kind applies only to interpolated attributes
195+
// with i18n markers (e.g., heading="Join {{ name }}" i18n-heading).
196+
// Pure property bindings ([attr]="expr" i18n-attr) keep Property
197+
// kind because the runtime uses domProperty, not i18nAttributes.
198+
let binding_kind = if prop_op.i18n_message.is_some()
199+
&& prop_op.binding_kind != BindingKind::Template
200+
&& matches!(*prop_op.expression, IrExpression::Interpolation(_))
201+
{
202+
BindingKind::I18n
203+
} else {
204+
prop_op.binding_kind
205+
};
198206

199207
// Properties also generate extracted attributes for directive matching
200208
// Note: Property ops are NOT removed - they still need runtime updates

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3873,12 +3873,10 @@ fn test_let_declaration_with_multiple_context_refs_variable_naming() {
38733873
// Const reference index: i18n property binding extraction
38743874
// ============================================================================
38753875

3876-
/// Tests that property bindings with i18n markers are extracted as BindingKind::Property
3877-
/// in the consts array. Angular's attribute_extraction.ts has a condition
3878-
/// `op.i18nMessage !== null && op.templateKind === null` that would produce I18n kind,
3879-
/// but empirically Angular never produces I18n marker (6) in consts arrays across all
3880-
/// tested components. The i18n metadata is handled by the i18n pipeline separately.
3881-
/// The property binding should use Bindings marker (3) for directive matching.
3876+
/// Tests that pure property bindings with i18n markers are extracted as BindingKind::Property.
3877+
/// Pure property bindings like [heading]="title" i18n-heading keep Bindings marker (3) because
3878+
/// the runtime uses domProperty to set the value, not i18nAttributes. The I18n marker (6) is
3879+
/// only used for interpolated attributes that go through the i18n pipeline.
38823880
#[test]
38833881
fn test_i18n_property_binding_extracted_as_property_kind() {
38843882
let allocator = Allocator::default();
@@ -3903,26 +3901,22 @@ export class TestComponent {
39033901
None,
39043902
);
39053903

3906-
// The consts array should contain [3,"heading"] (AttributeMarker.Bindings = 3)
3907-
// Angular never produces [6,"heading"] (AttributeMarker.I18n = 6) in consts arrays.
3904+
// Pure property bindings keep Bindings marker (3), NOT I18n marker (6).
3905+
// The i18n marker on a property binding is a no-op for directive matching.
39083906
assert!(
39093907
result.code.contains(r#"3,"heading""#),
3910-
"Property binding with i18n marker should produce Bindings AttributeMarker (3), not I18n (6). Output:\n{}",
3911-
result.code
3912-
);
3913-
assert!(
3914-
!result.code.contains(r#"6,"heading""#),
3915-
"Property binding with i18n marker should NOT produce I18n AttributeMarker (6). Output:\n{}",
3908+
"Pure property binding with i18n marker should produce Bindings AttributeMarker (3). Output:\n{}",
39163909
result.code
39173910
);
39183911
}
39193912

39203913
/// Tests that interpolated attributes with i18n markers (e.g., heading="{{ name }}" i18n-heading)
3921-
/// are extracted as BindingKind::Property (Bindings marker 3), not I18n marker 6.
3922-
/// Angular's compiler never produces I18n AttributeMarker (6) in consts arrays.
3914+
/// are extracted as BindingKind::I18n (marker 6).
3915+
/// Angular's attribute_extraction.ts checks `op.i18nMessage !== null && op.templateKind === null`
3916+
/// and overrides the binding kind to I18n.
39233917
/// This matches the real-world pattern in ClickUp's old-join-team component.
39243918
#[test]
3925-
fn test_i18n_interpolated_attribute_extracted_as_property_kind() {
3919+
fn test_i18n_interpolated_attribute_extracted_as_i18n_kind() {
39263920
let allocator = Allocator::default();
39273921
let source = r#"
39283922
import { Component } from '@angular/core';
@@ -3945,17 +3939,70 @@ export class TestComponent {
39453939
None,
39463940
);
39473941

3948-
// The consts array should contain [3,"heading"] (AttributeMarker.Bindings = 3)
3949-
// not [6,"heading"] (AttributeMarker.I18n = 6)
3950-
// Angular's compiler never produces I18n marker in consts arrays.
3942+
// The consts array should contain [6,"heading"] (AttributeMarker.I18n = 6)
3943+
// because the interpolated attribute has an i18n message (i18n-heading).
39513944
assert!(
3952-
result.code.contains(r#"3,"heading""#),
3953-
"Interpolated attribute with i18n marker should produce Bindings AttributeMarker (3), not I18n (6). Output:\n{}",
3945+
result.code.contains(r#"6,"heading""#),
3946+
"Interpolated attribute with i18n marker should produce I18n AttributeMarker (6). Output:\n{}",
3947+
result.code
3948+
);
3949+
assert!(
3950+
!result.code.contains(r#"3,"heading""#),
3951+
"Interpolated attribute with i18n marker should NOT produce Bindings AttributeMarker (3). Output:\n{}",
3952+
result.code
3953+
);
3954+
}
3955+
3956+
/// Tests that i18n property bindings in control flow don't produce extra consts entries.
3957+
/// When a property binding has i18n-attr (e.g., [cuTooltip]="expr" i18n-cuTooltip),
3958+
/// the consts entry should use Bindings marker (3), matching the conditional insertion point.
3959+
/// This ensures the entries deduplicate and don't shift downstream consts indices.
3960+
#[test]
3961+
fn test_i18n_property_binding_in_control_flow_no_extra_consts() {
3962+
let allocator = Allocator::default();
3963+
let source = r#"
3964+
import { Component } from '@angular/core';
3965+
3966+
@Component({
3967+
selector: 'test-comp',
3968+
template: `
3969+
<div data-test="body" class="body">
3970+
@if (showTooltip) {
3971+
<div data-test="inner"
3972+
[cuTooltip]="someExpr"
3973+
i18n-cuTooltip="@@copy-id">
3974+
Content
3975+
</div>
3976+
}
3977+
</div>
3978+
`,
3979+
standalone: true,
3980+
})
3981+
export class TestComponent {
3982+
someExpr = 'hello';
3983+
showTooltip = true;
3984+
}
3985+
"#;
3986+
3987+
let result = transform_angular_file(
3988+
&allocator,
3989+
"test.component.ts",
3990+
source,
3991+
&ComponentTransformOptions::default(),
3992+
None,
3993+
);
3994+
3995+
// The consts array should NOT contain [6,"cuTooltip"] because [cuTooltip]="expr"
3996+
// is a pure property binding, not an interpolated attribute.
3997+
assert!(
3998+
!result.code.contains(r#"6,"cuTooltip""#),
3999+
"Pure property binding in control flow should NOT produce I18n AttributeMarker (6). Output:\n{}",
39544000
result.code
39554001
);
4002+
// Should use Bindings marker (3) instead
39564003
assert!(
3957-
!result.code.contains(r#"6,"heading""#),
3958-
"Interpolated attribute with i18n marker should NOT produce I18n AttributeMarker (6). Output:\n{}",
4004+
result.code.contains(r#"3,"cuTooltip""#),
4005+
"Pure property binding in control flow should produce Bindings AttributeMarker (3). Output:\n{}",
39594006
result.code
39604007
);
39614008
}

napi/angular-compiler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"scripts": {
5252
"artifacts": "napi artifacts",
5353
"build-dev": "oxnode build.ts --esm --platform --features allocator",
54-
"build-test": "pnpm run build-dev --features cross_file_elision",
54+
"build-test": "oxnode build.ts --esm --platform --features allocator,cross_file_elision && pnpm run build:ts",
5555
"build": "pnpm run build:native && pnpm run build:ts",
5656
"build:native": "pnpm run build-dev --release",
5757
"build:ts": "tsdown",

0 commit comments

Comments
 (0)