Skip to content

feat(intl): getCanonicalLocales, supportedValuesOf, formatToParts + ListFormat/RelativeTimeFormat/PluralRules (#5298)#5320

Merged
proggeramlug merged 1 commit into
mainfrom
fix/intl402-canonical-formattoparts-5298
Jun 17, 2026
Merged

feat(intl): getCanonicalLocales, supportedValuesOf, formatToParts + ListFormat/RelativeTimeFormat/PluralRules (#5298)#5320
proggeramlug merged 1 commit into
mainfrom
fix/intl402-canonical-formattoparts-5298

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Closes the largest test262 intl402 gaps. Tracks #5298.

Result

intl402 differential parity (node v26 oracle, --all-features): 60.5% → 66.0%, +184 cases passing, zero regressions in any directory.

dir before after Δ
ListFormat 1 56 +55
RelativeTimeFormat 0 42 +42
PluralRules 2 36 +34
Intl (top-level) 5 32 +27
DateTimeFormat 58 68 +10
NumberFormat 44 50 +6
Temporal 1779 1786 +7
Collator / Segmenter +1 / +2

built-ins/Temporal self-validate suite: unchanged (no Temporal code touched).
cargo test -p perry-runtime -p perry-stdlib: green (the two parallel-only flakes typed_array_indexof_includes / builtin_prototype_methods_reject_dynamic_new pass isolated and are pre-existing).

What changed (all in crates/perry-runtime/src/intl.rs)

1. Intl.getCanonicalLocales (was missing → 23 "is not a function").
CanonicalizeLocaleList: undefined[], String→singleton, null→TypeError, Array/array-like→canonicalize+dedupe, non-string/non-object element→TypeError. Tag canonicalization delegates to icu_locale_core's data-free BCP-47/UTS #35 structural parser (Locale::normalize) — correct case normalization, variant ordering, extension well-formedness, and UTS #35 rejection of extlang/grandfathered/duplicate-singleton tags (which a hand-rolled validator gets subtly wrong). Gated behind a new default-on intl-locale feature, auto-enabled by the compiler on getCanonicalLocales/supportedLocalesOf usage, with a hand-rolled fallback when off. icu_locale_core is already in the lock graph via temporal, so default/shipped builds carry no extra weight.

2. Intl.supportedValuesOf (was missing → 30 "is not a function").
Sorted, duplicate-free value tables for calendar/collation/currency/numberingSystem/timeZone/unit; new array per call; RangeError on any other (string-coerced) key.

3. formatToParts on NumberFormat + DateTimeFormat (was missing → 76 "is not a function").
Refactored number/date formatting into typed {type,value} part builders so format() is the concatenation of formatToParts() values — the invariant the spec's own main tests assert.

4. New constructors Intl.ListFormat, Intl.RelativeTimeFormat, Intl.PluralRules (were missing → "undefined is not a constructor").
en-US format/select matching node byte-for-byte (verified), spec-shaped resolvedOptions (PluralRules incl. conditional significant-vs-fraction digits + rounding keys), and RangeError validation of enum options. ListFormat consumes any iterable (array / array-iterator / custom [Symbol.iterator]) via collection_iter::classify_init.

5. Symbol.toStringTag = "Intl.<Name>" on every Intl prototype (descriptor tests; instance-toString inheritance still limited by a separate runtime gap in prototype-chain symbol lookup).

Auto-optimize plumbing (feature_detect.rs / types.rs / optimized_libs.rs) wires the intl-locale feature; perry-api-manifest consistency test green (these APIs are runtime-reified, not codegen dispatch).

Remaining tail (follow-up, not in scope here)

  • Constructors Intl.Locale (142), Intl.DurationFormat (108), Intl.DisplayNames (52) — the bulk of remaining "undefined is not a constructor"; Locale needs many getters + likelySubtags data, DisplayNames/DurationFormat need CLDR display data.
  • formatRange / formatRangeToParts on DateTimeFormat/NumberFormat (35).
  • ICU-CLDR-data-dependent cases documented as out of scope: non-gregorian Temporal calendars (ethiopic/islamic/japanese/roc era boundaries — temporal_rs limitation), locale-specific number/date output (currency sign placement, non-latn numbering systems, es-ES/pl-PL list & relative-time forms).

Summary by CodeRabbit

  • New Features
    • Added Intl.getCanonicalLocales() and Intl.supportedValuesOf() runtime support for locale/value lookups.
    • Introduced Intl.ListFormat, Intl.RelativeTimeFormat, and Intl.PluralRules for spec-aligned formatting and resolvedOptions.
    • Added formatToParts() to Intl.NumberFormat, plus default formatToParts() for Intl.DateTimeFormat.
    • Enhanced locale canonicalization with optional BCP-47/UTS #35 behavior, including in-order de-duplication.
    • Updated compilation/optimization to automatically enable and correctly cache the required locale support when these APIs are used.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR extends the perry-runtime ECMA-402 Intl module with Intl.getCanonicalLocales (feature-gated via a new intl-locale/icu_locale_core Cargo feature), Intl.supportedValuesOf, formatToParts for NumberFormat and DateTimeFormat, and three new constructors (ListFormat, RelativeTimeFormat, PluralRules) with full method surfaces. The compiler gains HIR-based feature detection and build-cache invalidation for the new optional Intl locale feature.

Changes

ECMA-402 Intl Expansion

Layer / File(s) Summary
intl-locale feature gate and CompilationContext
crates/perry-runtime/Cargo.toml, crates/perry/src/commands/compile/types.rs
Adds the intl-locale Cargo feature backed by optional icu_locale_core, and introduces uses_intl_locale: bool to CompilationContext (initialized to false by default).
Feature detection and build-cache integration
crates/perry/src/commands/compile/collect_modules/feature_detect.rs, crates/perry/src/commands/compile/optimized_libs.rs
Detects Intl.getCanonicalLocales and Intl.*.supportedLocalesOf usage in HIR output to set ctx.uses_intl_locale, threads the flag into the auto-optimize cache key (loc={} component), and conditionally enables perry-runtime/intl-locale in the cargo feature list.
Internal KIND and option-key constants
crates/perry-runtime/src/intl.rs
Defines KIND_LIST_FORMAT, KIND_PLURAL_RULES, and KIND_RELATIVE_TIME identifiers, plus option-key constants for storing list/relative/plural configuration on instances (type, style, numeric, digit options).
Locale canonicalization and supportedValuesOf
crates/perry-runtime/src/intl.rs, crates/perry-runtime/src/intl/locales.rs
Implements ECMA-402 locale-tag canonicalization with feature-gated ICU structural normalization (intl-locale) and fallback structural validator, order-preserving de-duplication of canonical locale tags, element type validation, and get_canonical_locales_thunk. New intl/locales.rs module provides Intl.supportedValuesOf with static supported-key tables (calendars, collations, currencies, numbering systems, time zones, units) and RangeError handling for invalid keys.
NumberFormat formatToParts implementation
crates/perry-runtime/src/intl.rs
Reworks the NumberFormat pipeline to produce typed part lists (minus/integer/group/decimal/fraction/currency) from already-formatted strings, converts parts to JS objects via a helper, and adds unbound and bound formatToParts thunks. Instance setup installs the bound method on NUMBER instances.
DateTimeFormat formatToParts implementation
crates/perry-runtime/src/intl.rs
Adds typed segment generation for short DateTimeFormat output (month/literal/day/literal/year) with Invalid Date handling, and provides unbound and bound formatToParts thunks that return {type, value} arrays consistent with format(). Instance setup installs the bound method on DATE_TIME instances.
ListFormat, RelativeTimeFormat, and PluralRules implementations
crates/perry-runtime/src/intl.rs
Adds full end-to-end implementations of three new Intl constructors: ListFormat (string-list collection and type/style-dependent connectors), RelativeTimeFormat (unit normalization, long-form parts with ago/in handling), and PluralRules (en-US cardinal/ordinal category selection, selectRange, resolvedOptions). Includes all method thunks and constructor wiring into make_instance.
Symbol.toStringTag and namespace installation
crates/perry-runtime/src/intl.rs
Introduces set_proto_to_string_tag to stamp Intl.<Name> on each constructor prototype, applies it to all constructors, and updates install_intl_namespace to register getCanonicalLocales, supportedValuesOf, formatToParts for NumberFormat/DateTimeFormat, and full method tables for the three new constructors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Poem

🐇 Hop, hop! The locales are canonical now,
ListFormat lines up strings, quite the fine bow.
PluralRules counts: one rabbit, many hares,
RelativeTimeFormat says "3 seconds ago" with flair.
formatToParts slices numbers just right —
ICU and fallback both shining bright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes all major features added: getCanonicalLocales, supportedValuesOf, formatToParts, and three new constructors (ListFormat/RelativeTimeFormat/PluralRules).
Description check ✅ Passed The PR description comprehensively covers the changes with impact metrics, detailed feature explanations, test results across components, remaining work, and implementation details matching the template structure.
Docstring Coverage ✅ Passed Docstring coverage is 84.93% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/intl402-canonical-formattoparts-5298

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/intl.rs`:
- Around line 1212-1250: The rtf_instance_parts and rtf_parts functions
currently ignore the style and numeric options that are stored in the
RelativeTimeFormat instance during construction, causing them to always produce
output in long numeric format. To fix this, pass the instance object to
rtf_instance_parts so it can read the KEY_NUMERIC and KEY_RTF_STYLE fields from
the stored options, then thread these values into rtf_parts and add conditional
logic to handle different numeric modes (auto vs always) and style modes (long
vs short vs narrow) appropriately, or throw range errors for unsupported
combinations instead of silently ignoring them.
- Around line 1388-1393: The plural_select_range function only validates against
NaN values but does not reject infinities, which should be rejected according to
the API specification. Update the condition in the validation check to use the
is_finite() method instead of is_nan(), replacing the current check that uses
s.is_nan() || e.is_nan() with !s.is_finite() || !e.is_finite() to properly
reject both NaN and infinite values before processing further.
- Around line 225-248: The `supported_locales_of_thunk` function bypasses the
new ICU-backed canonicalization path and continues to use the old
`canonical_locale` function through `locales_from_value`. Update
`supported_locales_of_thunk` to call `canonicalize_language_tag` directly (the
function defined in this diff) instead of routing through `locales_from_value`,
so that `Intl.*.supportedLocalesOf` gets the same ICU structural validation and
canonicalization as `Intl.getCanonicalLocales`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f15e64c4-a2e4-4060-980d-04d7ffe9f735

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5f4b8 and e42a4c2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • crates/perry-runtime/Cargo.toml
  • crates/perry-runtime/src/intl.rs
  • crates/perry/src/commands/compile/collect_modules/feature_detect.rs
  • crates/perry/src/commands/compile/optimized_libs.rs
  • crates/perry/src/commands/compile/types.rs

Comment thread crates/perry-runtime/src/intl.rs
Comment on lines +1212 to +1250
/// Build the long-form, `numeric: "always"` en-US relative-time parts for
/// `value` in `unit`. (`short`/`narrow` abbreviations and the `numeric: "auto"`
/// special words — "tomorrow"/"yesterday" — need CLDR data and fall back to the
/// long numeric form here.) Returns `(leading, number, trailing)` literal/number
/// fragments so `format` and `formatToParts` stay consistent.
fn rtf_parts(value: f64, unit: &str) -> Vec<(&'static str, String)> {
let abs = value.abs();
let num_str = format_number_parts(abs, "en-US", None, None);
let unit_display = if abs == 1.0 {
unit.to_string()
} else {
format!("{unit}s")
};
let past = value.is_sign_negative();
let mut parts: Vec<(&'static str, String)> = Vec::new();
if past {
split_numeric_parts(&num_str, "en-US", &mut parts);
parts.push(("literal", format!(" {unit_display} ago")));
} else {
parts.push(("literal", "in ".to_string()));
split_numeric_parts(&num_str, "en-US", &mut parts);
parts.push(("literal", format!(" {unit_display}")));
}
parts
}

fn rtf_instance_parts(value: f64, unit_arg: f64) -> Vec<(&'static str, String)> {
let number = JSValue::from_bits(value.to_bits()).to_number();
if !number.is_finite() {
throw_range_error("Value need to be finite number for Intl.RelativeTimeFormat.format()");
}
let unit_str = value_to_string(unit_arg);
let Some(unit) = rtf_singular_unit(&unit_str) else {
throw_range_error(&format!(
"Value {unit_str} out of range for Intl.RelativeTimeFormat.format() unit"
));
};
rtf_parts(number, unit)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

RelativeTimeFormat formatting ignores style/numeric instance options

The constructor stores style/numeric (Lines 1589-1592), but formatting paths here compute output without reading instance fields. This makes numeric: "auto" and style: "short" | "narrow" behave like a fixed long numeric format.

Please thread the instance object into rtf_instance_parts and branch on KEY_NUMERIC / KEY_RTF_STYLE (or reject unsupported modes instead of silently accepting and ignoring them).

Also applies to: 1252-1275

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 1212 - 1250, The
rtf_instance_parts and rtf_parts functions currently ignore the style and
numeric options that are stored in the RelativeTimeFormat instance during
construction, causing them to always produce output in long numeric format. To
fix this, pass the instance object to rtf_instance_parts so it can read the
KEY_NUMERIC and KEY_RTF_STYLE fields from the stored options, then thread these
values into rtf_parts and add conditional logic to handle different numeric
modes (auto vs always) and style modes (long vs short vs narrow) appropriately,
or throw range errors for unsupported combinations instead of silently ignoring
them.

Comment on lines +1388 to +1393
fn plural_select_range(start: f64, end: f64) -> f64 {
let s = JSValue::from_bits(start.to_bits()).to_number();
let e = JSValue::from_bits(end.to_bits()).to_number();
if s.is_nan() || e.is_nan() {
throw_range_error("Invalid values for Intl.PluralRules.selectRange()");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

PluralRules.selectRange should reject infinities

Line 1391 only rejects NaN; +/-Infinity currently falls through and returns "other". The API should reject non-finite inputs.

Suggested fix
-    if s.is_nan() || e.is_nan() {
+    if !s.is_finite() || !e.is_finite() {
         throw_range_error("Invalid values for Intl.PluralRules.selectRange()");
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 1388 - 1393, The
plural_select_range function only validates against NaN values but does not
reject infinities, which should be rejected according to the API specification.
Update the condition in the validation check to use the is_finite() method
instead of is_nan(), replacing the current check that uses s.is_nan() ||
e.is_nan() with !s.is_finite() || !e.is_finite() to properly reject both NaN and
infinite values before processing further.

@proggeramlug proggeramlug force-pushed the fix/intl402-canonical-formattoparts-5298 branch from e42a4c2 to d570561 Compare June 17, 2026 12:04
…istFormat/RelativeTimeFormat/PluralRules (#5298)

Closes the largest test262 intl402 gaps (parity 60.5% -> 66.0%, +184 cases,
no regressions):

- Intl.getCanonicalLocales: CanonicalizeLocaleList via icu_locale_core's
  data-free BCP-47/UTS#35 structural parser (new default-on intl-locale
  feature, auto-enabled on use; already in the lock graph via temporal so
  default builds gain nothing). TypeError/RangeError per spec, dedup.
- Intl.supportedValuesOf: sorted, dedup'd value tables for calendar/collation/
  currency/numberingSystem/timeZone/unit; RangeError on invalid key.
- Intl.NumberFormat/DateTimeFormat.prototype.formatToParts: typed {type,value}
  parts that reconstruct format() byte-for-byte.
- New constructors Intl.ListFormat, Intl.RelativeTimeFormat, Intl.PluralRules
  with en-US format/select + spec-shaped resolvedOptions + enum option
  RangeError validation. ListFormat consumes any iterable via
  collection_iter::classify_init.
- Symbol.toStringTag = 'Intl.<Name>' on every Intl prototype.

Remaining tail (follow-up): Intl.Locale/DisplayNames/DurationFormat
constructors, formatRange/formatRangeToParts, and ICU-CLDR-data-dependent
locale-formatting cases (non-gregorian Temporal calendars, locale-specific
number/date output).
@proggeramlug proggeramlug force-pushed the fix/intl402-canonical-formattoparts-5298 branch from d570561 to 3f33d7f Compare June 17, 2026 12:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/intl.rs`:
- Around line 1190-1193: The plural_rules_select function reads the type field
from the object but does not apply the digit options (such as
maximumFractionDigits, minimumFractionDigits, maximumSignificantDigits,
minimumSignificantDigits) that were stored during construction. Before passing
the value to plural_select_en, extract these digit option fields from the object
using the same pattern as the type field retrieval, then apply the appropriate
digit formatting/rounding to the number based on the stored options to ensure
select() respects the same constraints as resolvedOptions(). Apply the same fix
to the other location mentioned in the comment around lines 1450-1464.
- Around line 1108-1119: The rtf_to_parts_thunk and rtf_bound_to_parts_thunk
functions currently use parts_to_js_array which only includes type and value
properties, but RelativeTimeFormat parts also need to include the unit property.
Modify the code to add the unit property to each part before converting to a JS
array. You should capture the unit parameter (which is passed to both functions)
and include it in the parts structure, either by creating a
RelativeTimeFormat-specific version of parts_to_js_array or by adding the unit
property to the parts returned from rtf_instance_parts before calling
parts_to_js_array.
- Around line 1450-1464: The code retrieves numeric options for
minimumIntegerDigits, minimumSignificantDigits, maximumSignificantDigits,
minimumFractionDigits, and maximumFractionDigits using get_option_number without
validating their ranges. Add validation checks after retrieving each option to
ensure minimumIntegerDigits is >= 1, significant digits are >= 1, maximum values
are >= minimum values where applicable, and fraction digits are >= 0. If any
validation fails, throw a RangeError with a descriptive message instead of
storing the invalid values. Apply these validations to the calls that retrieve
these options and their unwrap_or defaults before the set_internal_field calls.
- Around line 428-435: The issue is that for USD and EUR currencies, negative
values display the minus sign after the currency symbol (e.g., `$-1.00`) instead
of before it (e.g., `-$1.00`). To fix this, in both the USD and EUR match arms
where you push the currency symbol, check if the numeric parts contain a minus
sign as the first element. If present, extract and remove that minus sign from
numeric, push it to parts first, then push the currency symbol, and finally
extend with the remaining numeric parts. This ensures the minus sign appears
before the currency symbol for negative values.

In `@crates/perry-runtime/src/intl/locales.rs`:
- Around line 227-230: The error message in the throw_range_error call has
incorrect formatting with an extra space before the colon. In the format string
passed to throw_range_error, change "Invalid key : {key_str}" to "Invalid key:
{key_str}" by removing the space before the colon while keeping one space after
it to follow standard English punctuation conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: afe4b618-b119-4e72-886a-463de0768553

📥 Commits

Reviewing files that changed from the base of the PR and between d570561 and 3f33d7f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • crates/perry-runtime/Cargo.toml
  • crates/perry-runtime/src/intl.rs
  • crates/perry-runtime/src/intl/locales.rs
  • crates/perry/src/commands/compile/collect_modules/feature_detect.rs
  • crates/perry/src/commands/compile/optimized_libs.rs
  • crates/perry/src/commands/compile/types.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/perry/src/commands/compile/collect_modules/feature_detect.rs
  • crates/perry-runtime/Cargo.toml
  • crates/perry/src/commands/compile/types.rs

Comment on lines +428 to +435
Some("EUR") => {
parts.push(("currency", "\u{20ac}".to_string()));
parts.extend(numeric);
}
Some("USD") => {
parts.push(("currency", "$".to_string()));
parts.extend(numeric);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve the leading minus sign for prefix currencies.

For USD/non-DE EUR, numeric already contains the minusSign part, so negative values format as $-1.00 / €-1.00 instead of -$1.00 / -€1.00.

Proposed fix
             Some("EUR") => {
+                if matches!(numeric.first().map(|(ty, _)| *ty), Some("minusSign")) {
+                    parts.push(numeric.remove(0));
+                }
                 parts.push(("currency", "\u{20ac}".to_string()));
                 parts.extend(numeric);
             }
             Some("USD") => {
+                if matches!(numeric.first().map(|(ty, _)| *ty), Some("minusSign")) {
+                    parts.push(numeric.remove(0));
+                }
                 parts.push(("currency", "$".to_string()));
                 parts.extend(numeric);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 428 - 435, The issue is that
for USD and EUR currencies, negative values display the minus sign after the
currency symbol (e.g., `$-1.00`) instead of before it (e.g., `-$1.00`). To fix
this, in both the USD and EUR match arms where you push the currency symbol,
check if the numeric parts contain a minus sign as the first element. If
present, extract and remove that minus sign from numeric, push it to parts
first, then push the currency symbol, and finally extend with the remaining
numeric parts. This ensures the minus sign appears before the currency symbol
for negative values.

Comment on lines +1108 to +1119
extern "C" fn rtf_to_parts_thunk(_closure: *const ClosureHeader, value: f64, unit: f64) -> f64 {
let _obj = this_intl_object("formatToParts", KIND_RELATIVE_TIME);
parts_to_js_array(&rtf_instance_parts(value, unit))
}

extern "C" fn rtf_bound_to_parts_thunk(
closure: *const ClosureHeader,
value: f64,
unit: f64,
) -> f64 {
let _obj = captured_intl_object(closure, "formatToParts", KIND_RELATIVE_TIME);
parts_to_js_array(&rtf_instance_parts(value, unit))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the unit property to RelativeTimeFormat number parts.

formatToParts currently reuses parts_to_js_array, which only emits { type, value }. RelativeTimeFormat numeric parts also need the selected unit, otherwise consumers cannot distinguish { type: "integer", value: "3", unit: "day" } from a plain NumberFormat part.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 1108 - 1119, The
rtf_to_parts_thunk and rtf_bound_to_parts_thunk functions currently use
parts_to_js_array which only includes type and value properties, but
RelativeTimeFormat parts also need to include the unit property. Modify the code
to add the unit property to each part before converting to a JS array. You
should capture the unit parameter (which is passed to both functions) and
include it in the parts structure, either by creating a
RelativeTimeFormat-specific version of parts_to_js_array or by adding the unit
property to the parts returned from rtf_instance_parts before calling
parts_to_js_array.

Comment on lines +1190 to +1193
fn plural_rules_select(obj: *const ObjectHeader, value: f64) -> f64 {
let n = JSValue::from_bits(value.to_bits()).to_number();
let is_ordinal = get_string_field(obj, KEY_TYPE).as_deref() == Some("ordinal");
string_value(plural_select_en(n, is_ordinal))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Apply PluralRules digit options before category selection.

The constructor stores fraction/significant digit options, but plural_rules_select only reads type and passes the raw number to plural_select_en. Options such as maximumFractionDigits: 0 therefore change resolvedOptions() but not select().

Also applies to: 1450-1464

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 1190 - 1193, The
plural_rules_select function reads the type field from the object but does not
apply the digit options (such as maximumFractionDigits, minimumFractionDigits,
maximumSignificantDigits, minimumSignificantDigits) that were stored during
construction. Before passing the value to plural_select_en, extract these digit
option fields from the object using the same pattern as the type field
retrieval, then apply the appropriate digit formatting/rounding to the number
based on the stored options to ensure select() respects the same constraints as
resolvedOptions(). Apply the same fix to the other location mentioned in the
comment around lines 1450-1464.

Comment on lines +1450 to +1464
let min_int = get_option_number(options, "minimumIntegerDigits").unwrap_or(1.0);
set_internal_field(obj, KEY_PR_MIN_INT, min_int);
let min_sig = get_option_number(options, "minimumSignificantDigits");
let max_sig = get_option_number(options, "maximumSignificantDigits");
if min_sig.is_some() || max_sig.is_some() {
set_internal_field(obj, KEY_PR_USE_SIG, bool_value(true));
set_internal_field(obj, KEY_PR_MIN_SIG, min_sig.unwrap_or(1.0));
set_internal_field(obj, KEY_PR_MAX_SIG, max_sig.unwrap_or(21.0));
} else {
set_internal_field(obj, KEY_PR_USE_SIG, bool_value(false));
let min_frac = get_option_number(options, "minimumFractionDigits").unwrap_or(0.0);
let max_frac = get_option_number(options, "maximumFractionDigits")
.unwrap_or_else(|| min_frac.max(3.0));
set_internal_field(obj, KEY_PR_MIN_FRAC, min_frac);
set_internal_field(obj, KEY_PR_MAX_FRAC, max_frac);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate PluralRules digit option ranges.

get_option_number accepts any finite value here, so invalid values like minimumIntegerDigits: -1, maximumSignificantDigits: 0, or maximumFractionDigits < minimumFractionDigits are stored and exposed instead of throwing RangeError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl.rs` around lines 1450 - 1464, The code
retrieves numeric options for minimumIntegerDigits, minimumSignificantDigits,
maximumSignificantDigits, minimumFractionDigits, and maximumFractionDigits using
get_option_number without validating their ranges. Add validation checks after
retrieving each option to ensure minimumIntegerDigits is >= 1, significant
digits are >= 1, maximum values are >= minimum values where applicable, and
fraction digits are >= 0. If any validation fails, throw a RangeError with a
descriptive message instead of storing the invalid values. Apply these
validations to the calls that retrieve these options and their unwrap_or
defaults before the set_internal_field calls.

Comment on lines +227 to +230
None => throw_range_error(&format!(
"Invalid key : {key_str}. Wanted calendar, collation, currency, \
numberingSystem, timeZone, or unit"
)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix error message formatting.

The error message has an extra space before the colon: "Invalid key : {key_str}" should be "Invalid key: {key_str}" (no space before colon, one space after).

✏️ Proposed fix
-            "Invalid key : {key_str}. Wanted calendar, collation, currency, \
+            "Invalid key: {key_str}. Wanted calendar, collation, currency, \
              numberingSystem, timeZone, or unit"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl/locales.rs` around lines 227 - 230, The error
message in the throw_range_error call has incorrect formatting with an extra
space before the colon. In the format string passed to throw_range_error, change
"Invalid key : {key_str}" to "Invalid key: {key_str}" by removing the space
before the colon while keeping one space after it to follow standard English
punctuation conventions.

@proggeramlug proggeramlug merged commit a4361a4 into main Jun 17, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/intl402-canonical-formattoparts-5298 branch June 17, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant