Skip to content

fix(intl): #5904 — ResolveLocale for the nu numbering-system extension (NumberFormat/DateTimeFormat/DurationFormat) - #5963

Merged
proggeramlug merged 1 commit into
mainfrom
fix/t262-5904-numbering-system
Jul 4, 2026
Merged

fix(intl): #5904 — ResolveLocale for the nu numbering-system extension (NumberFormat/DateTimeFormat/DurationFormat)#5963
proggeramlug merged 1 commit into
mainfrom
fix/t262-5904-numbering-system

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the ECMA-402 ResolveLocale step for the nu (numbering system) Unicode extension key across Intl.NumberFormat, Intl.DateTimeFormat, and Intl.DurationFormat. The requested locale's -u-nu- keyword is reconciled with an explicit options.numberingSystem, and the resolved locale is rewritten so resolvedOptions().locale / .numberingSystem reflect only the supported value actually used:

requested locale options.numberingSystem resolved locale resolved numberingSystem
en-u-nu-arab invalid (unsupported) en-u-nu-arab arab
en-u-nu-invalid invalid2 en latn
en-u-nu-latn arab en arab
en-u-nu-arab arab en-u-nu-arab arab

Rules:

  • option present + supported → option wins; the -u-nu- keyword survives in the resolved locale only when it names the same value as the (supported) option.
  • no usable option → fall back to the supported locale-extension value, else the latn default (keyword dropped from the locale).

A numbering system counts as supported when it is latn (the default Latin/ASCII digits, which need no transliteration table) or Perry has a digit table for it.

Root cause

configure_number_format (and the DTF / DurationFormat equivalents) only validated the numberingSystem option for well-formedness and never ran ResolveLocale: the resolved locale kept the -u-nu- extension verbatim regardless of whether the system was supported or overridden, and resolvedOptions().numberingSystem didn't reflect the option-vs-extension precedence.

Implementation notes

  • New crates/perry-runtime/src/intl/numbering_system.rs module holds the -u-nu- BCP-47 tag helpers (resolve_numbering_system, split_u_extension, strip/with_numbering_system_keyword, is_supported_numbering_system, plus the pre-existing numbering_system_from_locale / is_well_formed_numbering_system moved out of intl.rs to keep it under the 2,000-line gate).
  • Base-tag casing is preserved — the extension parser lower-cases only the extension/tail region, so en-US no longer collapses to en-us (this was the subtle bug that would otherwise have regressed numbering-system-options.js).
  • Wired into all three constructors at their existing numberingSystem GetOption read site, preserving the option-read order that constructor-options-order.js asserts.

Before / after (test262, internal Linux box, Node v26.3.0)

slice before failing after failing fixed
intl402/NumberFormat 28 27 resolved-numbering-system-unicode-extensions-and-options.js
intl402/DateTimeFormat 44 43 resolved-numbering-system-unicode-extensions-and-options.js
intl402/DurationFormat 11 10 resolved-numbering-system-unicode-extensions-and-options.js

+3 tests, zero regressions (verified across NumberFormat / DateTimeFormat / DurationFormat / Collator, including numbering-system-options.js staying green).

cargo fmt --all -- --check and scripts/check_file_size.sh pass.

Refs #5904 #5899 #5906

Summary by CodeRabbit

  • New Features

    • Locale-based numbering system handling is now applied consistently across date/time and number formatting.
    • Formatting now resolves the effective locale and numbering system together, including Unicode locale extensions.
  • Bug Fixes

    • Improved handling of numberingSystem so invalid or unsupported values are normalized correctly.
    • Fixed cases where the displayed locale could differ from the actual numbering system used.

Reconcile the requested locale's `-u-nu-` keyword with an explicit
`options.numberingSystem` per ECMA-402 ResolveLocale, and update the
resolved locale so `resolvedOptions().locale` / `.numberingSystem` reflect
only the *supported* value actually used:

  * option present + supported  → option wins; the `-u-nu-` keyword survives
    in the locale only when it names the same value
  * no usable option           → fall back to the supported locale extension,
    else the `latn` default (keyword dropped)

A numbering system counts as supported when it is `latn` (default Latin/ASCII
digits) or Perry has a transliteration table for it. Base-tag casing is
preserved (`en-US` no longer collapses to `en-us`).

New `intl/numbering_system.rs` module holds the `nu`-extension BCP-47 tag
helpers (split out of `intl.rs` to keep it under the 2,000-line gate). Wired
into NumberFormat, DateTimeFormat, and DurationFormat construction.

Fixes intl402 resolved-numbering-system-unicode-extensions-and-options.js for
NumberFormat, DateTimeFormat, and DurationFormat (3 tests). Zero regressions
across all three slices plus Collator.

Refs #5904 #5899 #5906
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a shared numbering_system module for parsing, validating, and resolving Intl numberingSystem options against a locale's -u-nu- Unicode extension keyword. intl.rs, duration_format.rs, and number_format_options.rs are updated to use this shared resolution logic instead of prior local/independent handling, updating both the resolved locale and numbering system stored on the formatter object.

Changes

Numbering system resolution unification

Layer / File(s) Summary
Numbering system module
crates/perry-runtime/src/intl/numbering_system.rs
New module adding well-formedness/support validation, resolve_numbering_system, numbering_system_from_locale, and -u- extension parsing/rebuild/strip/insert helpers.
DateTimeFormat wiring
crates/perry-runtime/src/intl.rs
Removes local numberingSystem helpers, imports the new module, and updates the DateTimeFormat constructor to validate, lowercase, and resolve numberingSystem via resolve_numbering_system, storing resolved locale and numbering system.
DurationFormat wiring
crates/perry-runtime/src/intl/duration_format.rs
Updates configure to resolve numberingSystem via the shared resolver instead of defaulting to "latn".
NumberFormat wiring
crates/perry-runtime/src/intl/number_format_options.rs
Updates configure_number_format to resolve numberingSystem via the shared resolver and update the stored locale accordingly.

Estimated code review effort: 4 (Complex) | ~50 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Constructor as Intl constructor (DateTimeFormat/DurationFormat/NumberFormat)
  participant Resolver as resolve_numbering_system
  participant LocaleUtil as -u- extension utilities

  Constructor->>Constructor: parse and validate numberingSystem option
  Constructor->>Resolver: resolve_numbering_system(locale, opt_ns)
  Resolver->>LocaleUtil: split_u_extension(locale)
  LocaleUtil-->>Resolver: base, keywords, tail
  Resolver->>Resolver: reconcile opt_ns with existing nu keyword
  Resolver->>LocaleUtil: rebuild_locale / strip or add nu keyword
  LocaleUtil-->>Resolver: resolved locale string
  Resolver-->>Constructor: (resolved_locale, numbering)
  Constructor->>Constructor: store KEY_LOCALE and numbering system key
Loading

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5602: Both refactor -u- extension keyword handling for numbering systems in Intl.
  • PerryTS/perry#5605: Both modify numberingSystem/-u-nu- locale reconciliation logic affecting configure_number_format.
  • PerryTS/perry#5649: Both modify Intl.DateTimeFormat's numberingSystem option parsing and storage.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main intl locale-resolution change across NumberFormat, DateTimeFormat, and DurationFormat.
Description check ✅ Passed It covers the summary, root cause, implementation notes, issue refs, and test results, though the template’s explicit Changes/Test plan sections are absent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/t262-5904-numbering-system

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 1

🧹 Nitpick comments (1)
crates/perry-runtime/src/intl/duration_format.rs (1)

107-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: valid_numbering_system duplicates is_well_formed_numbering_system.

This is byte-for-byte identical to numbering_system::is_well_formed_numbering_system, which intl.rs already imports (so it's reachable here as super::is_well_formed_numbering_system). Consider dropping the local copy to keep the well-formedness rule single-sourced.

🤖 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/duration_format.rs` around lines 107 - 112, The
local valid_numbering_system helper duplicates the existing well-formedness
check already available as super::is_well_formed_numbering_system. Remove the
duplicate implementation in duration_format.rs and update the duration
formatting code to call the imported shared helper instead, keeping the rule
single-sourced and ensuring any future changes only need to be made in one
place.
🤖 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/numbering_system.rs`:
- Around line 54-85: `numbering_system_from_locale` is using a custom scan that
mishandles `-u-` attributes and singleton boundaries, so it can miss `nu` or
overrun into later extensions. Update this function to reuse the same
U-extension decomposition logic as `split_u_extension`, and then read the `nu`
keyword from that parsed structure instead of manually walking subtags. Ensure
the fix preserves correct handling for leading attributes and stops cleanly at
the next singleton extension.

---

Nitpick comments:
In `@crates/perry-runtime/src/intl/duration_format.rs`:
- Around line 107-112: The local valid_numbering_system helper duplicates the
existing well-formedness check already available as
super::is_well_formed_numbering_system. Remove the duplicate implementation in
duration_format.rs and update the duration formatting code to call the imported
shared helper instead, keeping the rule single-sourced and ensuring any future
changes only need to be made in one place.
🪄 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: a4d2db44-78a3-44de-a4e6-1f6aa5d616db

📥 Commits

Reviewing files that changed from the base of the PR and between 32aa095 and 98e2506.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/intl.rs
  • crates/perry-runtime/src/intl/duration_format.rs
  • crates/perry-runtime/src/intl/number_format_options.rs
  • crates/perry-runtime/src/intl/numbering_system.rs

Comment on lines +54 to +85
pub(super) fn numbering_system_from_locale(locale: &str) -> Option<String> {
let lower = locale.to_ascii_lowercase();
let subtags: Vec<&str> = lower.split('-').collect();
let u = subtags.iter().position(|s| *s == "u")?;
let mut i = u + 1;
while i < subtags.len() {
let key = subtags[i];
// A keyword key is exactly two chars; everything up to the next key is its value.
if key.len() == 2 {
if key == "nu" {
let mut value = String::new();
let mut j = i + 1;
while j < subtags.len() && subtags[j].len() != 2 {
if !value.is_empty() {
value.push('-');
}
value.push_str(subtags[j]);
j += 1;
}
return (!value.is_empty()).then_some(value);
}
i += 1;
while i < subtags.len() && subtags[i].len() != 2 {
i += 1;
}
} else {
// Hit another singleton extension (e.g. `-t-`); `nu` lives only under `u`.
break;
}
}
None
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

numbering_system_from_locale mis-parses -u- attributes and singleton boundaries, diverging from split_u_extension.

Two defects in this hand-rolled scan:

  1. The else branch (lines 79-82) breaks on any subtag whose length isn't 2, so a leading -u- attribute (3–8 chars, e.g. en-u-attr-nu-latn) is treated as "another singleton" and the nu keyword is never found.
  2. The value loop while subtags[j].len() != 2 (line 66) never stops at a following singleton (len 1). For a valid tag like en-u-nu-arab-x-foo it collects arab-x-foo, which fails is_supported_numbering_system, so ext_ns becomes None and resolution wrongly falls back to latn instead of arab.

split_u_extension already handles attributes and singleton boundaries correctly. Delegating to it removes the divergent parser and fixes both cases.

🐛 Proposed fix: reuse the correct decomposition
 pub(super) fn numbering_system_from_locale(locale: &str) -> Option<String> {
-    let lower = locale.to_ascii_lowercase();
-    let subtags: Vec<&str> = lower.split('-').collect();
-    let u = subtags.iter().position(|s| *s == "u")?;
-    let mut i = u + 1;
-    while i < subtags.len() {
-        let key = subtags[i];
-        // A keyword key is exactly two chars; everything up to the next key is its value.
-        if key.len() == 2 {
-            if key == "nu" {
-                let mut value = String::new();
-                let mut j = i + 1;
-                while j < subtags.len() && subtags[j].len() != 2 {
-                    if !value.is_empty() {
-                        value.push('-');
-                    }
-                    value.push_str(subtags[j]);
-                    j += 1;
-                }
-                return (!value.is_empty()).then_some(value);
-            }
-            i += 1;
-            while i < subtags.len() && subtags[i].len() != 2 {
-                i += 1;
-            }
-        } else {
-            // Hit another singleton extension (e.g. `-t-`); `nu` lives only under `u`.
-            break;
-        }
-    }
-    None
+    let (_, keywords, _) = split_u_extension(locale)?;
+    keywords
+        .into_iter()
+        .find(|(key, _)| key == "nu")
+        .map(|(_, value)| value.join("-"))
+        .filter(|v| !v.is_empty())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub(super) fn numbering_system_from_locale(locale: &str) -> Option<String> {
let lower = locale.to_ascii_lowercase();
let subtags: Vec<&str> = lower.split('-').collect();
let u = subtags.iter().position(|s| *s == "u")?;
let mut i = u + 1;
while i < subtags.len() {
let key = subtags[i];
// A keyword key is exactly two chars; everything up to the next key is its value.
if key.len() == 2 {
if key == "nu" {
let mut value = String::new();
let mut j = i + 1;
while j < subtags.len() && subtags[j].len() != 2 {
if !value.is_empty() {
value.push('-');
}
value.push_str(subtags[j]);
j += 1;
}
return (!value.is_empty()).then_some(value);
}
i += 1;
while i < subtags.len() && subtags[i].len() != 2 {
i += 1;
}
} else {
// Hit another singleton extension (e.g. `-t-`); `nu` lives only under `u`.
break;
}
}
None
}
pub(super) fn numbering_system_from_locale(locale: &str) -> Option<String> {
let (_, keywords, _) = split_u_extension(locale)?;
keywords
.into_iter()
.find(|(key, _)| key == "nu")
.map(|(_, value)| value.join("-"))
.filter(|v| !v.is_empty())
}
🤖 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/numbering_system.rs` around lines 54 - 85,
`numbering_system_from_locale` is using a custom scan that mishandles `-u-`
attributes and singleton boundaries, so it can miss `nu` or overrun into later
extensions. Update this function to reuse the same U-extension decomposition
logic as `split_u_extension`, and then read the `nu` keyword from that parsed
structure instead of manually walking subtags. Ensure the fix preserves correct
handling for leading attributes and stops cleanly at the next singleton
extension.

@proggeramlug
proggeramlug merged commit 17062a8 into main Jul 4, 2026
16 of 17 checks passed
@proggeramlug
proggeramlug deleted the fix/t262-5904-numbering-system branch July 4, 2026 12:48
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