Skip to content

IntlLocale: normalize ICU's "yes" sentinel in keywordValue; scope "true"->"" to kf - #351

Open
robobun wants to merge 1 commit into
mainfrom
robobun/intl-locale-yes-sentinel
Open

IntlLocale: normalize ICU's "yes" sentinel in keywordValue; scope "true"->"" to kf#351
robobun wants to merge 1 commit into
mainfrom
robobun/intl-locale-yes-sentinel

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Problem

Intl.Locale keyword getters leak ICU's internal "yes" sentinel when a -u- key is written without a value:

new Intl.Locale("en-u-kf").caseFirst   // "yes"  (want "")
new Intl.Locale("en-u-ca").calendar    // "yes"  (want "true")

"en-u-kf" and "en-u-kf-true" are the same canonical tag (toString confirms), but the getters disagree ("" vs "yes"), so new Intl.Locale(String(loc)).caseFirst !== loc.caseFirst.

Cause

uloc_forLanguageTag("en-u-ca") produces "en@calendar=yes": ICU represents a keyword-without-type with the sentinel "yes". keywordValue() then reads that with uloc_getKeywordValue and passes it through uloc_toUnicodeLocaleType, which returns "yes" unchanged (it is a well-formed 3-char type not present in any key's type map). The existing result == "true"_s check never matches.

That existing check is also too broad: it maps "true" to "" for every key, so new Intl.Locale("en-u-ca-true").calendar returns "" where V8 returns "true".

Fix

In IntlLocale::keywordValue:

  • Normalize "yes" to "true" after uloc_toUnicodeLocaleType so the sentinel never reaches JS.
  • Restrict the "true" -> "" mapping to colcasefirst only. kf's value set is {upper, lower, false}, so "true" there has no meaning; the other keys (ca/co/hc/nu/fw) should expose "true" per UTS Support typed arrays up to 512 GB #35.

This matches V8's UnicodeKeywordValue (if (value == "yes") value = "true"; if (value == "true" && strcmp(key, "kf") == 0) return "";).

Verification

Before / after, against Node v26.3.0:

tag getter JSC before JSC after V8
en-u-kf caseFirst "yes" "" ""
en-u-kf-true caseFirst "" "" ""
en-u-kf-upper caseFirst "upper" "upper" "upper"
en-u-ca calendar "yes" "true" "true"
en-u-ca-true calendar "" "true" "true"
en-u-ca-gregory calendar "gregory" "gregory" "gregory"
en-u-hc hourCycle "yes" "true" "true"
en-u-nu numberingSystem "yes" "true" "true"
en calendar undefined undefined undefined

numeric (kn) is unaffected: it takes the isBoolean early-return and compares the raw buffer to "yes" directly.

Bun-side test landing in oven-sh/bun: test/js/web/intl/intl.test.ts covers bare / -true / -yes for all six getters plus the toString() round-trip.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 14 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dcef478e-0608-4adf-ad92-319af28f74d4

📥 Commits

Reviewing files that changed from the base of the PR and between d103ebd and 5b1aa0a.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/IntlLocale.cpp

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

@claude claude 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.

LGTM — small, well-scoped fix that aligns Intl.Locale keyword getters with V8 and UTS #35.

What was reviewed:

  • Confirmed ASCIILiteral == ASCIILiteral is defined (ASCIILiteral.h:112), so the key == "colcasefirst"_s guard compiles and matches only caseFirst().
  • Traced all six keywordValue() callers: only caseFirst passes "colcasefirst"; numeric() short-circuits via isBoolean before the new code, so kn handling is unchanged.
  • Checked that no non-kf key has "yes" as a legitimate BCP 47 type, so the "yes""true" normalization can't clobber a real value.
Extended reasoning...

Overview

This PR modifies a single function, IntlLocale::keywordValue() in Source/JavaScriptCore/runtime/IntlLocale.cpp, replacing one line with nine. It fixes two related issues in how Intl.Locale getters (calendar, caseFirst, collation, hourCycle, numberingSystem, firstDayOfWeek) surface Unicode extension keyword values: (1) ICU's internal "yes" sentinel — used to represent a -u- key written without a type — was leaking to JavaScript; it is now normalized to the UTS #35 implied value "true"; (2) the pre-existing "true""" mapping applied to all keys, but is now scoped to colcasefirst (the kf key), where "true" is not part of the valid value set {upper, lower, false}.

Security risks

None. This is pure string post-processing on values already extracted by ICU. There is no new parsing, no allocation-size arithmetic, no user-controlled indexing, and no auth/permissions surface. The comparison key == "colcasefirst"_s is ASCIILiteral == ASCIILiteral, which is a well-defined content comparison in WTF.

Level of scrutiny

Low-to-moderate. The diff is tiny and mechanical, mirrors V8's UnicodeKeywordValue line-for-line (if (value == "yes") value = "true"; if (value == "true" && key == "kf") return "";), and the PR description includes a before/after/V8 verification table plus a reference to test262 intl402/Locale/getters.js. The one intentional behavior change beyond the bug fix — new Intl.Locale("en-u-ca-true").calendar now returns "true" instead of "" — is explicitly called out and matches V8.

Other factors

  • All six non-boolean callers of keywordValue() were audited in the same file; only caseFirst() passes "colcasefirst", so the scoped emptying hits exactly the intended getter.
  • numeric() passes isBoolean = true and returns before the modified code, so its raw == "yes" check is unaffected.
  • No CODEOWNERS gating, no outstanding reviewer comments (only a coderabbit rate-limit notice), and companion tests are landing in oven-sh/bun.
  • The bug hunting system found no issues.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
5b1aa0ae autobuild-preview-pr-351-5b1aa0ae 2026-07-26 09:00:50 UTC
d568d3ad autobuild-preview-pr-351-d568d3ad 2026-07-26 08:03:38 UTC

…ue" -> "" to colcasefirst

uloc_forLanguageTag stores a -u- keyword written without a type as
"@key=yes" (ICU's internal no-value marker), and uloc_toUnicodeLocaleType
passes "yes" through unchanged because it is syntactically a valid BCP 47
type. The getters (calendar/caseFirst/collation/firstDayOfWeek/hourCycle/
numberingSystem) then surface the raw "yes" to JavaScript, and because
-xx-true canonicalizes to -xx, the value does not survive a
new Intl.Locale(loc.toString()) round-trip.

Normalize "yes" to the UTS #35 implied value "true", and restrict the
existing "true" -> "" mapping to colcasefirst (kf), whose value set is
{upper, lower, false} and so has no meaningful "true". The other keys now
return "true" for a bare/"true"/"yes" value, matching V8 and test262
intl402/Locale/getters.js.
@robobun
robobun force-pushed the robobun/intl-locale-yes-sentinel branch from d568d3a to 5b1aa0a Compare July 26, 2026 08:22

@claude claude 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.

LGTM — small, well-scoped spec-conformance fix that mirrors V8's UnicodeKeywordValue exactly.

What was reviewed:

  • Verified all 7 keywordValue() call sites: only caseFirst() passes "colcasefirst"_s, so the scoped "true""" mapping hits the intended getter and no others.
  • Confirmed ASCIILiteral == ASCIILiteral does content comparison via equalSpans (not pointer equality), so the key check is sound.
  • numeric() (colnumeric) takes the isBoolean early return before the new code — unaffected as stated.
  • "yes" is not a valid registered type for any of ca/co/hc/nu/fw/kf, so unconditionally folding it to "true" matches V8 and can't shadow a real value.
Extended reasoning...

Overview

This PR modifies a single function, IntlLocale::keywordValue() in Source/JavaScriptCore/runtime/IntlLocale.cpp, adding two small branches: (1) normalize ICU's internal "yes" sentinel to "true" after uloc_toUnicodeLocaleType, and (2) narrow the pre-existing "true""" mapping so it only applies when key == "colcasefirst" rather than to every keyword. Net diff is +9/-1 with two of the added lines being logic and the rest comments.

Security risks

None. This is pure string-constant comparison and assignment on WTF::String / ASCIILiteral values inside an Intl getter helper. There is no user-controlled buffer arithmetic, no allocation change, no pointer manipulation, and no auth/permission surface. The inputs are already-validated ICU output strings.

Level of scrutiny

Low-to-moderate. This is an ECMA-402 conformance fix in a leaf helper with a small, closed set of callers (all in the same file). The PR description includes a before/after/V8 comparison table for 9 cases plus a reference to the exact V8 source pattern being matched, and a companion test is landing in oven-sh/bun. The behavioral change for non-kf keys (en-u-ca-true now returns "true" instead of "") is intentional and spec-aligned per UTS #35.

Other factors

I grepped for all keywordValue( call sites — there are exactly 7, all in IntlLocale.cpp, and only caseFirst() passes "colcasefirst"_s, so the scoped check is correct. I also confirmed that operator==(ASCIILiteral, ASCIILiteral) in WTF/wtf/text/ASCIILiteral.h performs content comparison via equalSpans (not pointer identity), so the key == "colcasefirst"_s test is sound even though the two literals are distinct objects. The isBoolean path (colnumeric) returns before the new code and still compares the raw buffer to "yes" directly, so numeric is unaffected. No outstanding reviewer comments; CodeRabbit was rate-limited and left no substantive review.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto 549170099226f816 (bun's currently pinned WEBKIT_VERSION) so the preview build carries only this change. The previous head was based on main and pulled in five unrelated commits, which tripped bun's binary-size check in oven-sh/bun#35895.

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.

2 participants