Skip to content

Bump WebKit: omit dayPeriod from Intl.DateTimeFormat resolvedOptions for bare 12-hour patterns - #36361

Open
robobun wants to merge 1 commit into
mainfrom
farm/e88055a8/intl-dayperiod-resolvedoptions
Open

Bump WebKit: omit dayPeriod from Intl.DateTimeFormat resolvedOptions for bare 12-hour patterns#36361
robobun wants to merge 1 commit into
mainfrom
farm/e88055a8/intl-dayperiod-resolvedoptions

Conversation

@robobun

@robobun robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Bumps WEBKIT_VERSION to pick up oven-sh/WebKit#373.

Problem

Any Intl.DateTimeFormat component formatter that resolves to a 12-hour cycle (hour with h11/h12, hour12: true, or a 12-hour locale default) reports resolvedOptions().dayPeriod === "short" even though no dayPeriod option was passed and the output contains no flexible day period. Feeding resolvedOptions() back into the constructor (the ECMA-402 round-trip idiom) then genuinely requests dayPeriod, so the same time formats differently:

const t = Date.UTC(2026, 0, 1, 10, 5);
const a = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", hour: "numeric", minute: "2-digit" });
const ro = a.resolvedOptions();
const b = new Intl.DateTimeFormat("en-US", ro);
console.log(ro.dayPeriod, a.format(t), b.format(t));
// bun : "short"   "10:05 AM"  "10:05 in the morning"
// node: undefined "10:05 AM"  "10:05 AM"

h23/h24 and the timeStyle path are unaffected.

Cause

IntlDateTimeFormat::setFormatsFromPattern in JSC maps the resolved ICU pattern back into the fields that resolvedOptions() reports. It was treating 'a' (the AM/PM marker, emitted whenever the resolved hour cycle is h11/h12) the same as 'b'/'B' (the flexible day-period fields that the ECMA-402 dayPeriod option actually requests). buildSkeleton maps dayPeriod back to 'B', so the round-trip swaps the AM/PM marker for a flexible day period.

Fix

'a' no longer populates m_dayPeriod; only 'b'/'B' do. m_dayPeriod is read exclusively by resolvedOptions(), so formatToParts (which maps UDAT_AM_PM_FIELD to the "dayPeriod" part type independently) is unaffected. This matches V8.

Verification

New test in test/js/web/intl/intl.test.ts covers the undefined dayPeriod, the round-trip equality, the explicit-dayPeriod case still being reported, and the formatToParts part type.

Also in this range (oven-sh/WebKit 34c01d1339..)

Nothing else; oven-sh/WebKit#373 is directly on top of the current WEBKIT_VERSION.

Note

WEBKIT_VERSION currently points at the preview tag autobuild-preview-pr-373-0c1a9be2 while oven-sh/WebKit#373 is open. It will be switched to the merged main sha before this lands.


no test proof · iteration 0 · Platform-specific test-only change; deferring to CI.

…terns

Picks up oven-sh/WebKit#373.

Any Intl.DateTimeFormat that resolves to a 12-hour cycle (hour with
h11/h12, hour12: true, or a 12-hour locale default) was reporting
resolvedOptions().dayPeriod === "short" even though no dayPeriod option
was passed and the output contains no flexible day period. Feeding
resolvedOptions() back into the constructor then requests dayPeriod for
real, so the round-trip changes "10:05 AM" to "10:05 in the morning".

setFormatsFromPattern in JSC was mapping the AM/PM pattern field 'a'
into m_dayPeriod alongside the real flexible-day-period fields 'b'/'B';
'a' is now left to hourCycle/hour12. formatToParts is unaffected.
@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Status: waiting on the oven-sh/WebKit#373 preview build (autobuild-preview-pr-373-0c1a9be2). Bun CI will fail to download the prebuilt until that release is published; the test itself fails against the current WebKit as expected:

{
- "dayPeriod": undefined,
+ "dayPeriod": "short",
  "hourCycle": "h12",
  "original": "10:05 AM",
- "roundTrip": "10:05 AM",
+ "roundTrip": "10:05 in the morning",
}

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b520b9c0-d772-4149-a468-1a6981ed2b4b

📥 Commits

Reviewing files that changed from the base of the PR and between 960a91e and 3631703.

📒 Files selected for processing (2)
  • scripts/build/deps/webkit.ts
  • test/js/web/intl/intl.test.ts

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

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:04 AM PT - Jul 29th, 2026

@robobun, your commit 3631703 has 2 failures in Build #85184 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36361

That installs a local version of the PR into your bun-36361 executable, so you can run:

bun-36361 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the inline nit, I also checked that prebuiltUrl() and prebuiltDestDir() in scripts/build/deps/webkit.ts already special-case the autobuild- prefix, so the preview tag doesn't double-prefix the release URL or truncate to a colliding cache key. Note the PR description already flags that WEBKIT_VERSION must be switched from the preview tag to the merged sha before this lands.

Extended reasoning...

Verified the non-sha WEBKIT_VERSION value flows correctly through the two consumers in webkit.ts: prebuiltUrl guards startsWith("autobuild-") before prefixing, and prebuiltDestDir strips the prefix rather than slicing to 16 chars, so webkit-preview-pr-373-0c1a9be2{suffix} is a distinct cache dir. No other consumers of cfg.webkitVersion assume a 40-hex sha. This is a WebKit dependency bump with an explicit pre-merge TODO in the description, so leaving for human review.

Comment on lines +99 to +123
expect({
dayPeriod: ro.dayPeriod,
hourCycle: ro.hourCycle,
original: a.format(t),
roundTrip: b.format(t),
}).toEqual({
dayPeriod: undefined,
hourCycle: "h12",
original: "10:05 AM",
roundTrip: "10:05 AM",
});

expect(
new Intl.DateTimeFormat("en-US", { timeZone: "UTC", hour: "2-digit", hourCycle: "h11" }).resolvedOptions()
.dayPeriod,
).toBeUndefined();

// When dayPeriod *is* requested it must still be reported.
expect(
new Intl.DateTimeFormat("en-US", { timeZone: "UTC", hour: "numeric", dayPeriod: "long" }).resolvedOptions()
.dayPeriod,
).toBe("long");

// formatToParts still labels the AM/PM marker as type "dayPeriod" (ECMA-402 Table 16).
expect(a.formatToParts(t).find(p => p.value === "AM")?.type).toBe("dayPeriod");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 nit: This test hardcodes the CLDR-derived literals "10:05 AM" and p.value === "AM" under plain test(), while every other exact-output check in this file is gated by snapshotIf per the file header (macOS links Apple's libicucore, Windows a different ICU build). The invariants actually under test are CLDR-independent — consider asserting roundTrip: a.format(t) (equality, not a literal) and locating the formatToParts entry by p.type === "dayPeriod" rather than by value.

Extended reasoning...

What this is

The new test at test/js/web/intl/intl.test.ts:99-123 runs on every platform (plain test()) and asserts two CLDR-derived literals: original: "10:05 AM" / roundTrip: "10:05 AM" in the .toEqual object, and .find(p => p.value === "AM") in the formatToParts check. This file's own header states that exact Intl output is CLDR-version-specific — "macOS uses Apple's libicucore and Windows is on a different ICU build, so snapshot diffs there are expected and not a regression" — and scripts/build/deps/webkit.ts confirms darwin uses system ICU (prebuiltIcuLibs returns [] for darwin, commented // darwin: system ICU). Every other exact-format-string check in this file is accordingly gated behind snapshotIf (Linux + process.versions.icu === "75.1" only); this test bypasses that gate.

Step-by-step: how a divergence would manifest

  1. On macOS, JSC links whatever libicucore the OS ships — its CLDR data version is not pinned by Bun and drifts per macOS release.
  2. Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit" }) resolves its output pattern from that CLDR data. The en time patterns have changed across ICU releases — most notably CLDR 42 (ICU 72) switched the space before the AM/PM marker to U+202F NARROW NO-BREAK SPACE, and CLDR 43 reverted it.
  3. If a macOS lane's ICU emits anything other than exactly 10:05 + U+0020 + AM, the .toEqual({ ..., original: "10:05 AM", roundTrip: "10:05 AM" }) assertion fails.
  4. Similarly, .find(p => p.value === "AM") returns undefined if the day-period value string differs at all (e.g. lowercase, or with a trailing marker), so undefined?.type yields undefined and .toBe("dayPeriod") fails — with a much less informative message than finding by type would give.
  5. In all of these cases the WebKit fix under test (dayPeriod omitted, round-trip equality preserved) is still correct — only the CLDR literal diverged.

Why this is a nit, not a blocker

Two mitigations make current-CI failure unlikely. First, JSC's IntlDateTimeFormat normalizes U+202F → U+0020 in its output (replaceNarrowNoBreakSpaces, a WebKit web-compat change) — verified empirically that Bun on Linux ICU 75.1 emits codepoints [31 30 3a 30 35 20 41 4d] even though raw CLDR 45 data uses NNBSP, and that normalization is C++-side so it applies on macOS too. Second, en-US "AM" is among the most stable strings in CLDR history. There is also precedent in this file for ungated test() with ICU-backed exact strings (the tr dotted-i case and getCanonicalLocales), so snapshotIf is a convention for multi-locale format sweeps rather than a hard rule. Net: fragile to future macOS ICU drift and inconsistent with the file's documented convention (REVIEW.md: "Copy harness conventions exactly"), but not demonstrably failing today.

Suggested fix

The invariants the WebKit change guarantees — ro.dayPeriod === undefined, ro.hourCycle === "h12", a.format(t) === b.format(t), and the AM/PM part's .type === "dayPeriod" — are all CLDR-independent. Rewriting the two fragile spots removes the uncontrolled-ICU dependency without weakening what's tested:

expect({
  dayPeriod: ro.dayPeriod,
  hourCycle: ro.hourCycle,
  original: a.format(t),
  roundTrip: b.format(t),
}).toEqual({
  dayPeriod: undefined,
  hourCycle: "h12",
  original: a.format(t),   // non-empty; the literal isn't what's under test
  roundTrip: a.format(t),  // round-trip EQUALITY is the invariant
});

// find by type, assert the value is present
const dp = a.formatToParts(t).find(p => p.type === "dayPeriod");
expect(dp?.value).toBeTruthy();

Alternatively, keep the "10:05 AM" literal but wrap that one assertion in snapshotIf like the rest of the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants