fix: i18n property bindings use I18n AttributeMarker in consts array - #1
Merged
Merged
Conversation
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>
Brooooooklyn
deleted the
fix/const-ref-index-i18n-attribute-extraction
branch
February 4, 2026 00:48
BenjaminDobler
added a commit
to BenjaminDobler/oxc-angular-compiler
that referenced
this pull request
Mar 23, 2026
ɵɵtextInterpolate1/2/...N always require all positional string args including the trailing suffix. When the suffix was an empty string, it was incorrectly dropped, causing `undefined` to be concatenated at runtime (e.g. `#1undefined` instead of `voidzero-dev#1`). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brooooooklyn
pushed a commit
that referenced
this pull request
Sep 23, 2026
…ith Angular's own tests (#491) * test(i18n): add failing tests for i18n attribute and ICU messages Expected output is taken from Angular 22.1.5 ngc. - i18n-<attr> messages (plain, interpolated, and alongside an i18n element body) currently lose their text: goog.getMsg("") and $localize`:@@id:`. - An element whose only child is an ICU currently emits an extra empty sub-message referenced as {"icu": "�i18n_N�"}, splits the ICU's inner {{count}} into a $localize substitution, and emits no i18nPostprocess call, so the runtime fails to parse the ICU. - An ICU next to other content currently gets an empty sub-message instead of the ICU text. * fix(i18n): use the attribute value as the text of i18n-<attr> messages Messages for i18n-<attr> were built from the i18n-<attr> metadata alone (meaning, description, @@id), so their text was always empty: goog.getMsg("") and $localize`:@@close:`. At runtime the attribute rendered as an empty string in the source locale. Angular's I18nMetaVisitor builds these messages with _generateI18nMessage([attr], meta) whenever the attribute has a value. Do the same through I18nMessageFactory, and keep the message's text and interpolation placeholder nodes. Ingest now reads interpolation placeholder names from those nodes, matching Angular's Object.keys(message.placeholders), so an interpolated attribute gets its INTERPOLATION param instead of "". * test(i18n): add failing test for tag placeholders inside an ICU Angular 22.1.5 maps each tag placeholder inside an ICU (START_BOLD_TEXT and so on) back to its markup through ɵɵi18nPostprocess. * fix(i18n): emit ICU messages and their post-processing like Angular ICU messages were built as an empty, id-less message wrapping a single ICU placeholder, and ingest never linked an ICU to its message. As a result every ICU became an empty sub-message (i18n_N = $localize``), referenced from the parent as the string "�i18n_N�"; the ICU's inner {{count}} was split into a $localize substitution with value ""; and no ɵɵi18nPostprocess call was emitted. At runtime @angular/core threw "Unable to parse ICU expression". This ports Angular's behaviour: - I18nMetaVisitor builds an ICU's message from [expansion], and an ICU that is the only child of an i18n element reuses that element's message (currentMessage || meta). ingestIcu passes icu.i18n to the IcuStart op, so create_i18n_contexts treats such an ICU as the message itself rather than a sub-message. - Placeholders inside an ICU serialize as literal {NAME} ICU text (IcuSerializerVisitor), not {$name} / $localize substitutions. - r3 ICU placeholders are keyed by the message's placeholder names (INTERPOLATION, START_BOLD_TEXT, ...) and include element markup, as in r3_template_transform's visitExpansion. - Each message gets its context's post-processing params plus its ICU placeholder literals (extractI18nMessages), with formatIcuPlaceholder joining strings and values without a ${} wrapper. - collectMessage emits `i18n_N = ɵɵi18nPostprocess(i18n_N, {...})` after the Closure/$localize branch, and passes a single ICU sub-message by its variable ({ "icu": i18n_0 }), as addSubMessageParams does. * test(i18n): add failing test for two ICUs in one message Angular 22.1.5 names the placeholders ICU and ICU_1 and passes each its own sub-message. oxc names both ICU, so the second ICU renders as "". * fix(i18n): give each ICU in a message its own placeholder name Every ICU placeholder was hardcoded to "ICU". With two ICUs in one message, both sub-messages mapped to the same placeholder, so the root message passed "�I18N_EXP_ICU�" for ICU and "" for ICU_1, and the second ICU rendered as an empty string. Angular names ICU placeholders with getPlaceholderName('ICU', ...), giving ICU, ICU_1, ... when it builds the enclosing message. Take the names from that message, which the factory already builds, matched by each ICU's start offset. * test(i18n): add failing tests from Angular's i18n compliance scenarios Found by compiling Angular 22.1.5's r3_view_compiler_i18n compliance cases with ngc and oxc and comparing messages and rendered output. Expected values are ngc's output. - $localize placeholder names lose their underscores (STARTBOLDTEXT instead of START_BOLD_TEXT), which changes the message id. - i18n-<attr> on an empty attribute creates an empty message. - Custom placeholder names (// i18n(ph="PH")) are not used for the param, so the interpolation renders as "". - Whitespace next to an ICU is dropped from the message. - Elements with structural directives get duplicated tag values and a spurious i18nPostprocess. - @if/@else inside i18n gets duplicated and missing block values. * fix(i18n): keep original placeholder names in $localize messages Messages were stored with camelCase placeholders ({$startBoldText}) for goog.getMsg, and $localize recovered the name by upper-casing it, which drops the underscores: :STARTBOLDTEXT: instead of :START_BOLD_TEXT:. Placeholder names are part of the id $localize computes, so every message containing an element, block or custom placeholder got a different id from the one ng extract-i18n produces, and its translation was never applied. Angular serializes the message separately for each target: LocalizeSerializerVisitor uses formatI18nPlaceholderName(name, false) and GetMsgSerializerVisitor the camelCase form. Store the original names ({$START_BOLD_TEXT}), use them as-is for $localize and param lookup, and convert to camelCase only for goog.getMsg. * fix(i18n): only create i18n-<attr> messages for attributes with a value i18n-title on title="" produced an empty translatable message. Angular's I18nMetaVisitor only attaches a message to a plain attribute of that name when it has a value (`meta !== undefined && attr.value`), so an empty attribute, or a bound [title], stays a regular attribute/binding. * fix(i18n): honour custom placeholder names for interpolations `{{ name // i18n(ph="PH") }}` produced a message with {$ph} but a param named INTERPOLATION, so goog.getMsg got {"interpolation": ...} and $localize substituted "" for PH. The placeholder names used for the binding came from a separate code path that always used INTERPOLATION. Use the custom name there too, as Angular's i18n parser does with extractPlaceholderName. * fix(i18n): keep whitespace-only text in i18n messages The i18n message factory dropped whitespace-only text nodes, so `{count, plural, ...} <b>x</b>` became "{$icu}{$startBoldText}..." and whitespace in preserveWhitespaces templates was lost from messages. Angular's _I18nVisitor.visitText keeps every text node; retainEmptyTokens only concerns zero-length tokens in interpolated text. Whitespace that should not be translated is removed earlier by the WhitespaceVisitor, which keeps whitespace next to ICUs. * fix(i18n): resolve element and block placeholders once per view, in order resolve_i18n_element_placeholders walked the view tree from the root and then processed every child view again on its own. Placeholders in child views were recorded twice, the second time without their structural directive, giving values like "[�*2:1��#1:1�|�#1:1�]" and a spurious i18nPostprocess. @else/@else if branches (ConditionalBranch ops) were not handled, so their block placeholders had no value, and child views were resolved after their parent's ops rather than at their position. Port Angular's resolvePlaceholdersForView: only walk from the root, handle ConditionalCreate and ConditionalBranchCreate like templates, and record template start, recurse, then record template close in op order. recordTemplateClose also uses the enclosing block's subTemplateIndex, not null. * fix(i18n): take block placeholder names from the i18n message Block placeholders were named from a counter shared by every block, so the @else in `@if (a) {...} @else {...}` became START_BLOCK_ELSE_1 in the params while the message said START_BLOCK_ELSE, and its value was lost. Angular names block placeholders with the message's placeholder registry, which numbers per name and reuses a name for an identical block. Take the names from the message the factory already builds, matched by each block's start offset, as is done for ICUs. * test(i18n): add failing tests for the remaining compliance divergences Second batch from comparing oxc with Angular 22.1.5 ngc on the r3_view_compiler_i18n compliance cases (expected values are ngc's): - colons in meaning/description are not escaped in the $localize raw metadata block, so the text renders the metadata - ICUs outside an i18n block get no message and render nothing - nested ICU VAR placeholders are numbered parent first - interpolated attributes on elements inside an ICU are not mapped - spaces around ICU keywords leave VAR_SELECT unresolved (runtime error) - void elements add an empty {$} placeholder - nested tag placeholders are numbered parent first - <ng-content> tag placeholders get no value - custom placeholder names with spaces (ph = "who") are ignored - interpolations lose their value in an i18n element with a structural directive, or next to an element with an interpolated i18n attribute - elements inheriting a namespace are not named with it * fix(i18n): serialize void elements as a single placeholder <img> inside an i18n block produced "{$TAG_IMG}{$}": the message serializer always wrote a close placeholder, empty for void elements, which added an unnamed placeholder with no value. Angular's serializers write only the start placeholder when isVoid. * fix(i18n): accept whitespace in custom placeholder name comments `{{ name // i18n(ph = "who") }}` was named INTERPOLATION because the custom name was only recognised as the exact text i18n(ph=. Port Angular's _CUSTOM_PH_EXP, which allows whitespace between the comment, i18n, (, ph, = and the quoted name. * fix(i18n): escape $localize metadata blocks in the raw strings $localize finds the end of a message's metadata block by the first `:` not escaped in the template's raw string. oxc emitted identical cooked and raw strings with no escapes, so a meaning or description containing `:` ended the block early: i18n="meaning:A|descA@@ida" rendered "A|descA@@ida:Content A" and lost its custom id. A message starting with `:` got a backslash in its cooked text instead. Port Angular's createCookedRawString: cooked strings are unescaped, and raw strings escape backslashes, colons inside the metadata block, a leading colon, backticks and `${`. LocalizedStringExpr carries the raw parts for the emitter. * fix(i18n): name nested tag placeholders children first In `<span title="x">a<span>inner</span></span>` the outer span was START_TAG_SPAN and the inner START_TAG_SPAN_1; Angular's _I18nVisitor._visitElementLike visits children before naming the element, so the inner span is START_TAG_SPAN and the outer START_TAG_SPAN_1. The message id and the param mapping both depend on this. Visit children first in the message factory, and take the tag names used for element placeholders from that message (matched by the element's start offset) instead of a separate registry that could disagree with it. * fix(i18n): take nested ICU VAR names from the ICU's message For nested ICUs the message (built by the factory, which follows Angular) numbers inner ICUs first: `{VAR_SELECT_2, select, male {{VAR_SELECT, ...}} female {{VAR_SELECT_1, ...}} ...}`. The r3 ICU vars were named separately, parent first, so VAR_SELECT_2 was mapped to the first nested ICU's expression and the outer ICU switched on the wrong value at runtime. Name each ICU var from the ICU's message placeholders, matched by the switch expression's start offset. * fix(i18n): trim ICU var names when spaces surround the ICU type `{count, select , ...}` has the type "select ", so the message's VAR placeholder is "VAR_SELECT ". oxc used that name for the ICU's var, and the post-processing key became VAR_SELECT_, which left the ICU's {VAR_SELECT , ...} unresolved: "Unable to parse ICU expression" at runtime. Angular's r3 visitExpansion trims VAR keys (key.trim()). * fix(i18n): bind interpolations in element markup inside ICUs In `{x, select, other {<span title="{{a}}-{{a}}">foo</span>}}` the START_TAG_SPAN post-processing value was the literal markup, so the rendered title was "{a}-{a}". Angular's r3 visitExpansion passes each non-VAR placeholder through _visitTextWithInterpolation, making the markup bound text whose expressions become placeholders: <span title="�1�-�2�">. * fix(i18n): give <ng-content> inside i18n blocks its placeholder values <ng-content> in an i18n block appeared in the message as START_TAG_NG_CONTENT/CLOSE_TAG_NG_CONTENT, but the r3 Content node had no i18n metadata and ingest never set the projection's placeholder, so both params were "" and the projected content was dropped from the translated message. Angular's ingestContent passes content.i18n (a tag placeholder) to createProjectionOp. * fix(i18n): keep placeholder names for text that is only an interpolation A text node inside an i18n block that is only an interpolation, like `<b>{{ name }}</b>` or `{{ name }}<h1 ...>`, got a bare placeholder as its i18n metadata instead of a container. Ingest reads interpolation placeholder names from a container only, so the expression got no placeholder, INTERPOLATION had no value, and the text rendered empty. Angular's _visitTextWithInterpolation returns a Container whenever the text has an interpolation; do the same. * fix(i18n): compile ICUs outside i18n blocks `<div>{gender, select, male {...} other {...}}</div>` without an i18n attribute rendered nothing: the ICU was dropped when not inside an i18n element. Angular's I18nMetaVisitor gives every expansion a message, and the wrapI18nIcus phase wraps an ICU that is not in an i18n block in its own i18nStart/i18nEnd, which oxc already implements. * test(i18n): add failing test for SVG element placeholder names Angular 22.1.5 names SVG elements in an i18n message with their namespace (START_TAG__SVG_SVG, START_TAG__SVG_CIRCLE), which is part of the message id. oxc uses START_TAG_SVG. * fix(i18n): name element placeholders with their namespace Angular's HTML parser gives elements their full namespaced name (an <svg> is :svg:svg, a <circle> inside it :svg:circle, a <span> inside <xhtml:div> :xhtml:span), and i18n placeholder names derive from it: START_TAG__SVG_SVG. oxc's HTML AST keeps the raw name, so any SVG icon inside an i18n message was START_TAG_SVG, giving the message a different id from ng extract-i18n. Port _getElementFullName (explicit prefix, else the tag's implicit namespace, else the parent's unless it prevents inheritance) into the i18n message factory, tracking the parent element's full name, and pass the i18n root's full name from html_to_r3. * test(i18n): add failing test for whitespace around a root-level ICU Angular 22.1.5 compiles a template that is only an ICU surrounded by whitespace to a single i18n instruction; oxc keeps the whitespace as text nodes. * fix(i18n): drop whitespace around ICUs at the template root A template that is only `{age, select, ...}` with surrounding newlines compiled to text(" "), i18n, text("\n"), so the rendered ICU had extra whitespace. Angular's parseTemplate runs the whitespace visitor over root nodes with visitAll rather than visitAllWithSiblings, so root-level text has no sibling context and whitespace next to a root-level ICU is removed (it is kept inside elements). * test(i18n): add failing test for &ngsp; in i18n messages Angular 22.1.5 keeps &ngsp; as U+E500 in the message text, which is part of the message id; oxc replaces it with a space. * fix(i18n): build text messages from tokens like Angular `a&ngsp;b` in an i18n block became the message "a b"; Angular's is "a\uE500b", so the message ids differed. Angular's _I18nVisitor builds text with more than one token from the tokens, where whitespace processing has only rewritten plain text tokens and an entity token keeps its decoded value. Do the same, and return a Container for any text with an interpolation, as _visitTextWithInterpolation does. * refactor(i18n): address clippy lints in the i18n fixes * docs: note that legacy i18n message IDs are not supported oxc emits no legacy message IDs (enableI18nLegacyMessageIdFormat) and has no i18nNormalizeLineEndingsInICUs option, so translations keyed only by legacy IDs are not applied. Remove the i18nNormalizeLineEndingsInIcus option from the root README, which does not exist. * fix(compare): compare static definitions emitted as class fields compareFullFileSemantically only compared `ClassName.ɵcmp = ...` assignments, but both Angular and Oxc now emit `static ɵcmp = ...` class fields, so no ɵcmp/ɵfac/ɵdir/ɵpipe/ɵmod was compared in full-file mode. The template function, consts (where every i18n message lives), decls and vars were never checked: with the compiler from before the recent i18n fixes, which emitted goog.getMsg("") where Angular emits goog.getMsg("Close"), all i18n fixtures still matched. With this change 20 of the 28 fail against that compiler. - Extract `static ɵ*` class fields from the AST. - Compare field values by AST, ignoring differences that do not change behaviour: formatting and string escapes, arrow vs function for consts, $localize tagged template vs __makeTemplateObject call (cooked and raw strings are still compared), pure: true in ɵɵdefinePipe, and Closure-only i18n metadata Oxc does not emit (goog.getMsg variable names, original_code). - Add fixtures/known-differences.ts for the 40 fixtures that this surfaces with real, non-i18n differences (query chaining, factory form, NgModule imports, host attribute order, style shimming, ...). Listed fixtures report as known differences; a listed fixture that matches fails so the entry gets removed. * test(i18n): add failing tests found by the compare tool's i18n fixtures Expected values are Angular 22.1.5 ngc output (legacy message ids off, as in the compare tool): - ICU placeholders in $localize carry their sub-message's id (:ICU@@911278603808503436:); oxc writes :ICU:. - i18n text expressions are applied after advancing to the i18n block's last slot; oxc skips the advance() when an element with i18n attributes comes after the text. - an interpolated i18n attribute on an explicit <ng-template> uses the bindings marker (3); oxc uses the i18n marker (6). * fix(i18n): use the bindings marker for i18n attributes on ng-template An interpolated i18n attribute on an explicit <ng-template> was extracted with the i18n attribute marker (6). Angular's attribute extraction uses the i18n marker only when the binding's templateKind is null, i.e. not on a template, so it emits the bindings marker (3). oxc's PropertyOp has no templateKind; treat bindings that target a Template op as having one. * fix: advance to i18nAttributes and conditional branch slots generate_advance only mapped some slot-consuming ops to their slots. Update ops targeting an i18nAttributes op (such as i18n text expressions, which assign_i18n_slot_dependencies moves to the i18n block's last slot consumer) or a conditional branch got no advance(), so they ran with the previous element selected. Angular's generateAdvance maps every op with the consumes-slot trait. * fix(i18n): name ICU sub-messages on their $localize placeholders When a message has no legacy ids, Angular's serializeI18nTemplatePart writes the id of an ICU placeholder's sub-message into the placeholder metadata: `${i18n_0}:ICU@@911278603808503436:`. oxc emits no legacy ids, which is Angular's enableI18nLegacyMessageIdFormat: false output, but wrote plain `:ICU:`. Compute each ICU sub-message's id as Angular does, computeMsgId(message.messageString, meaning), with a port of serializeMessage (the messageString serializer, which differs from the code-generation strings), and carry it through the message metadata to the $localize placeholder. Message metadata is now built by one I18nMessageMetadata::from_message instead of six copies. The three tests that asserted `:ICU:` took their expected values from ngc with legacy ids on; they now use ngc's legacy-off output, matching oxc's legacy-less messages. * fix(compare): keep whitespace in static fields for the semantic check Class static fields were whitespace-collapsed before the AST comparison, which also collapsed newlines inside Angular's multi-line $localize template literals, so messages with newlines looked different from Oxc's (which escapes them). Compare the raw field text: the AST comparison already ignores formatting. Known differences: remove edge-cases/unicode-special-escapes, which was an artifact of the collapsing, and add the fixtures whose remaining differences are selector whitespace in shimmed styles and the pipe factory form (used by the i18n fixtures added next). * test(compare): add i18n fixtures from Angular's own i18n tests Generated by the new `pnpm generate:i18n-fixtures [angular-dir]` (defaults to the Angular submodule) from Angular 22.1.5: - every r3_view_compiler_i18n compliance case (99 input files), one fixture file per directory - every template in the runtime i18n acceptance spec (129) plus 34 hand-written scenarios checked while fixing oxc's i18n output (entities, &ngsp;, @let, @switch, *ngFor, listeners, pipes in ICUs, namespaces, custom placeholder names, ...). NgModule declarations are made standalone so that fixtures compare i18n output rather than NgModule scoping, which Oxc does not resolve. Skipped, with reasons: external-template cases, three acceptance templates that are intentionally invalid or need a spec-local directive, and ngNonBindable inside i18n (Angular's compiler crashes). Against the compiler from before the recent i18n fixes, 211 of the 262 active i18n fixtures fail; all pass now. * docs(compare): document known differences and the i18n fixture generator * refactor(i18n): address clippy lints in the i18n fixes * refactor(i18n): tidy the placeholder-name plumbing - Remove serialize_i18n_nodes and its helpers: the i18n message string now comes from the message factory, leaving them unused. - Group the placeholder names taken from a message into MessagePlaceholderNames (icus/blocks/tags) and IcuPlaceholderNames (vars/interpolations) instead of five separate maps on the transformer. - Merge create_message_in back into create_message with the parent_element parameter, rather than keeping two entry points. * test(i18n): add failing test for literal placeholder-like text Angular keeps `{$notAPlaceholder}` in an i18n message as text; oxc treats it as a placeholder, dropping the text from $localize and case-folding it in goog.getMsg. * fix(i18n): keep text that looks like a placeholder marker `{$notAPlaceholder}` in an i18n message was treated as a placeholder: $localize replaced it with an empty substitution and goog.getMsg case-folded it, so the text was lost and the message id was wrong. The message string cannot tell `{$NAME}` markers from text that looks like one. Escape `{$` and `{\` in message text when serializing the message string, and unescape when generating goog.getMsg and $localize. The serializer used for message ids is Angular's and stays unescaped. * fix(i18n): clear the sole-ICU message after visiting an element's children The i18n message an element shares with its only ICU child is consumed by that child. Clear it afterwards so it cannot be picked up by a later ICU if the child is ever not visited. * refactor(compare): type the static field comparison like the rest of the file Use NormAstNode and unknown instead of any in the new AST comparison and class field extraction. * test(compare): regenerate i18n fixtures from the Angular submodule The fixtures were generated from a 22.1.5 checkout; regenerate from the repository's submodule (22.0.0) so that `pnpm generate:i18n-fixtures` with no arguments reproduces them. Only the version header differs: the i18n test cases are identical in both. * perf(i18n): avoid allocating a full element name per element element_full_name allocated a String for every element in the template, including the common case of an un-namespaced tag in a non-i18n template where the name is returned unchanged. Returning a Cow borrows the name unless a namespace prefix has to be prepended. * test(compare): scope known differences to the fields they document A fixture listed in known-differences.ts had every difference forgiven, so a new regression in one of those 51 fixtures was reported as the known difference instead of a failure. Each entry now records the static fields that differ, and a difference in any other field of the same fixture fails as usual. * test(compare): stop recording an i18n fixture flag nothing reads The generator copied angularCompilerOptions.i18nUseExternalIds from Angular's test cases into the fixtures, but neither compiler wrapper passes it on, so both sides compile with external IDs either way. Drop it rather than imply coverage of local message IDs that does not exist. * docs: correct the option names in the README examples TransformOptions has crossFileElision and i18NUseExternalIds (N-API capitalises the N), not enableCrossFileElision or i18nUseExternalIds, and has no useDomOnlyMode. The plugin options block listed an interface name and options the plugin does not have, and omitted ssrEntry, angularVersion and templateTransform. * test(compare): refresh the known differences for Angular 22.1.7 The recorded differences were captured against Angular 21.2.6. Fourteen of them now match and are removed; the change-detection and incremental hydration differences that 22.1.7 exposes are recorded with the fields they affect. * test(compare): drop a known-difference reason with no remaining users * chore: drop the handoff note that was committed by mistake
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Note
Medium Risk
Changes IR extraction/categorization for property bindings, which can alter generated consts arrays and directive matching/i18n behavior at runtime. Scope is narrow and backed by new integration tests.
Overview
Aligns i18n handling with Angular by propagating
i18n-*metadata ontobind-fooand[foo]property bindings during HTML-to-R3 transform, and by extracting property ops with ani18n_messageasBindingKind::I18n(while preservingTemplatebindings).Adds integration coverage to assert both bracket property bindings and interpolated attributes with
i18n-*markers produceAttributeMarker.I18n(6) entries in the generated consts array.Written by Cursor Bugbot for commit af86328. This will update automatically on new commits. Configure here.