Skip to content

Accept the collations Intl.Locale.prototype.getCollations reports - #2974

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/collator-accepts-what-getcollations-reports
Aug 11, 2026
Merged

Accept the collations Intl.Locale.prototype.getCollations reports#2974
lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/collator-accepts-what-getcollations-reports

Conversation

@lahma

@lahma lahma commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

#2867 made getCollations() report real collation data — and exposed that Intl.Collator refused emoji for 13 of the locales the query advertises it for (ar da de es hi ja ko ln si sv zh tr fr): the acceptance predicate's fallback conceded eor from the root pair and forgot emoji, and its table branch trusted a table that lists emoji for en alone.

The two views may not legally disagree: 15.5.10 CollationsOfLocale reads [[SortLocaleData]].[[<foundLocale>]].[[co]], and 9.2.7 ResolveLocale step 10 gates both the -u-co- keyword branch and the options branch on that same list. One [[co]] per locale; getCollations() and the constructor are two views of it.

The fix makes them one list by construction: LocaleCollations is derived once from the table plus the root pair, GetCollationsForLanguage hands it out and IsCollationSupportedForLocale scans it. standard/search stay out of the list rather than being guarded at resolution time (10.2.3 forbids them as [[co]] elements, and filtering at derivation means a future table edit cannot reintroduce this bug class); default is accepted as a request only — it is the spelling of the null [[co]] — and never reported. ValidCollationTypes, a second wider whitelist that never changed an answer but could drift exactly the way this bug did, is deleted.

15-tag consistency matrix (everything reported must construct and echo in resolvedOptions().collation, options bag and -u-co-): Jint before 13 inconsistent cells, after 0. One correction to the review that found this: node is not clean on the same matrix — it has 3 inconsistent cells (zh/pinyin resolves to default because pinyin is zh's default, and both und-u-co-* forms).

One deliberate non-change, now pinned so nobody "fixes" it toward V8: resolvedOptions().locale for an options-bag collation stays de, not de-u-co-emoji — ResolveLocale sets supportedKeyword to empty on the options branch, and test262's resolved-collation-unicode-extensions-and-options.js pins exactly that.

Red 15 / green 25 on both TFMs. Test262 exactly 99,738 / 0 / 157; Test262Harness.settings.json untouched.

Found and left for the backlog: an invalid collation option ({collation:'a'}) silently falls back where 9.2.8 step 7.d.ii wants a RangeError (node throws); the LocaleCollationSupport table's CLDR accuracy (ducet/direct/big5han/reformed-vs-trad) — this consistency fix is the prerequisite for correcting that data, not a substitute; and Intl.supportedValuesOf("collation") is backed by a third hardcoded list that is consistent today but can drift the same way.

🤖 Generated with Claude Code

ECMA-402 gives a locale one collation list and reads it from two places.
https://tc39.es/ecma402/#sec-collationsoflocale (15.5.10) step 3.c takes
%Intl.Collator%.[[SortLocaleData]].[[<foundLocale>]].[[co]] and reports it,
and https://tc39.es/ecma402/#sec-resolvelocale (9.2.7) step 10 - reached from
https://tc39.es/ecma402/#sec-intl.collator (10.1.1) through ResolveOptions -
resolves a requested "co" by asking whether that same list contains it, from
the -u-co- keyword and from the options bag alike. getCollations() and
Intl.Collator are two views of one list, so they may not disagree.

Jint's disagreed on thirteen of fifteen tags probed. GetCollationsForLanguage
appends the root collations "emoji" and "eor" to every locale, which is right
- CLDR's root locale carries both, and 15.5.10 step 4 hardcodes exactly that
pair for a tag matching nothing - while IsCollationSupportedForLocale conceded
only "eor" from that pair and otherwise allowed just what the per-language
table lists. The table lists "emoji" for "en" alone, so

    new Intl.Locale('de').getCollations()
    // ["emoji", "eor", "phonebk"]
    new Intl.Collator('de', { collation: 'emoji' }).resolvedOptions().collation
    // "default"

and likewise for ar, da, es, hi, ja, ko, ln, si, sv, zh, tr and fr. Only "en",
whose table entry happens to list "emoji", and "und", which resolves through
the default locale, were consistent. V8 accepts "emoji" on all fifteen.

The acceptance predicate is the older half, but the disagreement is new: until
sebastienros#2867 getCollations() answered ["default"] for every locale and so had nothing
to contradict.

Both views now read one derived list. LocaleCollations is built once from the
CLDR table plus the root collations, in the code unit order 15.5.10 returns;
GetCollationsForLanguage hands it out and IsCollationSupportedForLocale scans
it. Adding a collation to the table now reports it and accepts it by the same
act, which is the property that was missing.

Two identifiers stay out of that list rather than being special-cased where a
request is resolved. "standard" and "search" are excluded because
https://tc39.es/ecma402/#sec-intl-collator-internal-slots (10.2.3) forbids
either from being an element of any [[SortLocaleData]].[[<locale>]].[[co]] or
[[SearchLocaleData]].[[<locale>]].[[co]] List, and keeping them out of the list
makes them unreportable and unrequestable at once - the guard they used to have
in GetCollationOption would have gone on rejecting them even if a table edit
made getCollations() report them, which is this same bug in miniature.
"default" is excluded because no locale reports it, but stays acceptable as a
request: it is how Jint spells the null [[co]] that 10.1.1 turns into a
resolved collation of "default" anyway.

ValidCollationTypes goes with them. It was a second, wider whitelist every
request had to clear first, and because every entry of every per-language list
is already in it the check never changed an answer - but it was one more list
to keep in step, and a collation added to the table and not to it would have
been reported and refused exactly as "emoji" was.

The fifteen-tag probe now finds nothing: everything getCollations() reports
constructs and resolves back to itself, through the options bag and through
-u-co-. The resolved locale for a -u-co-emoji tag moves with it, from "de" to
"de-u-co-emoji", because 9.2.7 step 10 inserts a keyword back into the locale
once its value resolves. A collation supplied through the options bag still
does not appear there - the same step sets supportedKeyword to empty on the
options branch - which is what
intl402/Collator/prototype/resolvedOptions/resolved-collation-unicode-extensions-and-options.js
pins, and is one place V8 answers differently.

Left alone deliberately: whether the per-language table is accurate CLDR at
all. Its "ducet", "direct", "big5han" and "reformed"-rather-than-"trad" entries
are flagged as suspect and are a data question of their own. This change makes
the two views agree on whatever the table says, which is a prerequisite for
correcting it rather than a substitute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lahma
lahma merged commit 3f16f83 into sebastienros:main Aug 11, 2026
5 checks passed
@lahma
lahma deleted the fix/collator-accepts-what-getcollations-reports branch August 11, 2026 06:04
lahma added a commit to lahma/jint that referenced this pull request Aug 12, 2026
sebastienros#2974 made Intl.Locale.prototype.getCollations and Intl.Collator agree on
whatever LocaleCollationSupport says, and left open whether what it says is
CLDR at all. It is not, in six places. The table is the one CLDR reads two
ways: common/bcp47/collation.xml registers the collation identifiers and marks
the dead ones deprecated, and common/collation/<language>.xml says which of
them a language actually defines.

    identifier   registry                    per-language file
    big5han      deprecated="true"           defined by no locale
    direct       deprecated="true"           defined by no locale
    gb2312       deprecated="true"           defined by no locale
    reformed     deprecated="true"           defined by no locale
    ducet        current                     defined by no locale

Those five were reachable. "ducet" is the one that is not deprecated - it is
registered and current, and CLDR simply ships no data for it, a search of
common/collation for a <collation type="ducet"> finding nothing and the hi.xml
hit being prose about the DUCET order rather than a type. Either way no
locale's file defines it, so no locale's [[co]] list may contain it. The other
four are deprecated and equally undefined; "reformed" in particular is the old
name for what sv.xml has called "traditional" for many releases.

What the per-language files say, minus the "standard" and "search" every one of
them carries and the "private-" types CLDR keeps only to be [import]ed:

    ar   compat                              unchanged
    de   search, phonebook, eor              unchanged as phonebk, eor
    en   (no <collations> element at all)    "ducet" removed, row now empty
    es   traditional                         unchanged as trad
    fi   traditional                         "trad" added, row is new
    hi   standard                            "direct" removed, row now empty
    ja   private-kana, unihan                unchanged
    ko   searchjl, unihan                    unchanged
    ln   phonetic                            unchanged
    si   dictionary                          unchanged as dict
    sv   traditional                         "reformed" replaced by "trad"
    vi   traditional                         "trad" added, row is new
    zh   private-pinyin, pinyin, stroke,     "big5han" and "gb2312" removed
         zhuyin, unihan

root.xml defines "standard", "search", "eor", "private-unihan" and "emoji", so
RootCollations stays ["emoji", "eor"] and every locale keeps getting it. That
is also why the en, hi and da rows go away entirely rather than shrinking: once
what CLDR does not support is gone, what is left of them is the root pair every
locale already gets, which is the same reason fr and tr never needed a row. The
rows that remain now say one thing - what this language adds - so the "eor"
that ar, es, ja, ko, ln, si, sv and zh used to repeat is gone from them too,
while de keeps its own, which de.xml really does define as a German delta on
the European ordering rules.

Intl.supportedValuesOf("collation") moves with them.
https://tc39.es/ecma402/#sec-availablecanonicalcollations wants the collations
the implementation provides Intl.Collator functionality for, and
intl402/Intl/supportedValuesOf/collations-accepted-by-Collator.js checks
exactly that against ten locales - so leaving the five in
DefaultCldrProvider.GetSupportedCollations while no locale resolves them any
more turns that test red on "big5han is supported by Collator". It is still a
third hardcoded list that can drift from the table; deriving it is a separate
change.

Left alone: yue, which CLDR does give zh's set. It has no collation file, and
supplementalData.xml's <parentLocales component="collations"> makes zh_Hant its
collation parent, whose file defines only a defaultCollation - which is why
node reports pinyin, stroke, unihan and zhuyin for it and ICU's coll/yue.txt is
a bare %%ALIAS to zh_Hant. Jint cannot report it yet: "yue" matches no element
of IntlUtilities.GetAvailableLocales(), so Intl.Collator.supportedLocalesOf
(['yue']) is empty and new Intl.Collator('yue') resolves to the default locale.
A row would have advertised four collations Intl.Collator answers "default" for
- the disagreement sebastienros#2974 removed, restored on one tag. Reporting them waits on
making yue an available Collator locale, and a test pins the current answer and
the reason so a later row cannot be added without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma added a commit that referenced this pull request Aug 13, 2026
#2974 made Intl.Locale.prototype.getCollations and Intl.Collator agree on
whatever LocaleCollationSupport says, and left open whether what it says is
CLDR at all. It is not, in six places. The table is the one CLDR reads two
ways: common/bcp47/collation.xml registers the collation identifiers and marks
the dead ones deprecated, and common/collation/<language>.xml says which of
them a language actually defines.

    identifier   registry                    per-language file
    big5han      deprecated="true"           defined by no locale
    direct       deprecated="true"           defined by no locale
    gb2312       deprecated="true"           defined by no locale
    reformed     deprecated="true"           defined by no locale
    ducet        current                     defined by no locale

Those five were reachable. "ducet" is the one that is not deprecated - it is
registered and current, and CLDR simply ships no data for it, a search of
common/collation for a <collation type="ducet"> finding nothing and the hi.xml
hit being prose about the DUCET order rather than a type. Either way no
locale's file defines it, so no locale's [[co]] list may contain it. The other
four are deprecated and equally undefined; "reformed" in particular is the old
name for what sv.xml has called "traditional" for many releases.

What the per-language files say, minus the "standard" and "search" every one of
them carries and the "private-" types CLDR keeps only to be [import]ed:

    ar   compat                              unchanged
    de   search, phonebook, eor              unchanged as phonebk, eor
    en   (no <collations> element at all)    "ducet" removed, row now empty
    es   traditional                         unchanged as trad
    fi   traditional                         "trad" added, row is new
    hi   standard                            "direct" removed, row now empty
    ja   private-kana, unihan                unchanged
    ko   searchjl, unihan                    unchanged
    ln   phonetic                            unchanged
    si   dictionary                          unchanged as dict
    sv   traditional                         "reformed" replaced by "trad"
    vi   traditional                         "trad" added, row is new
    zh   private-pinyin, pinyin, stroke,     "big5han" and "gb2312" removed
         zhuyin, unihan

root.xml defines "standard", "search", "eor", "private-unihan" and "emoji", so
RootCollations stays ["emoji", "eor"] and every locale keeps getting it. That
is also why the en, hi and da rows go away entirely rather than shrinking: once
what CLDR does not support is gone, what is left of them is the root pair every
locale already gets, which is the same reason fr and tr never needed a row. The
rows that remain now say one thing - what this language adds - so the "eor"
that ar, es, ja, ko, ln, si, sv and zh used to repeat is gone from them too,
while de keeps its own, which de.xml really does define as a German delta on
the European ordering rules.

Intl.supportedValuesOf("collation") moves with them.
https://tc39.es/ecma402/#sec-availablecanonicalcollations wants the collations
the implementation provides Intl.Collator functionality for, and
intl402/Intl/supportedValuesOf/collations-accepted-by-Collator.js checks
exactly that against ten locales - so leaving the five in
DefaultCldrProvider.GetSupportedCollations while no locale resolves them any
more turns that test red on "big5han is supported by Collator". It is still a
third hardcoded list that can drift from the table; deriving it is a separate
change.

Left alone: yue, which CLDR does give zh's set. It has no collation file, and
supplementalData.xml's <parentLocales component="collations"> makes zh_Hant its
collation parent, whose file defines only a defaultCollation - which is why
node reports pinyin, stroke, unihan and zhuyin for it and ICU's coll/yue.txt is
a bare %%ALIAS to zh_Hant. Jint cannot report it yet: "yue" matches no element
of IntlUtilities.GetAvailableLocales(), so Intl.Collator.supportedLocalesOf
(['yue']) is empty and new Intl.Collator('yue') resolves to the default locale.
A row would have advertised four collations Intl.Collator answers "default" for
- the disagreement #2974 removed, restored on one tag. Reporting them waits on
making yue an available Collator locale, and a test pins the current answer and
the reason so a later row cannot be added without it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lahma added a commit to lahma/jint that referenced this pull request Aug 13, 2026
Intl.supportedValuesOf("collation") was a third hardcoded collation list, in
DefaultCldrProvider.GetSupportedCollations, next to the per-language table and
the root pair the other two views read.
https://tc39.es/ecma402/#sec-availablecanonicalcollations asks for "the
collations for which the implementation provides the functionality of
Intl.Collator objects", sorted in lexicographic code unit order and unique -
which is the union of the one [[co]] list every locale has and nothing else, so
there was never a second fact for that list to hold. It is now read off
LocaleCollations, the same derived data getCollations and Intl.Collator resolve
against.

It agreed with them when this was written, and the previous change is what made
that true again - but agreeing today is exactly what the two views did before
sebastienros#2974. The failure modes are asymmetric and only one of them is caught:
a value listed that no locale resolves turns
intl402/Intl/supportedValuesOf/collations-accepted-by-Collator.js red, which is
how the deprecated CLDR types were noticed, while a collation some locale
reports and the list omits is invisible to test262 - AvailableCollations is
implementation-defined, so nothing checks it from that side. Adding a collation
to LocaleCollationSupport used to produce exactly that silent second case; it
now shows up in all three views by the same act.

"standard", "search" and "default" need no exclusion of their own here.
IsReportableCollation already keeps them out of every list being unioned, which
is what https://tc39.es/ecma402/#sec-intl-collator-internal-slots (10.2.3)
requires of the per-locale lists and what
intl402/Intl/supportedValuesOf/collations.js checks of this one - the same
guard now answering for both. Sorting is likewise not restated: the union is
built in code unit order, which is what 15.5.10 and this clause both ask for
and what IntlInstance.SupportedValuesOf sorts by anyway.

The values are unchanged: compat, dict, emoji, eor, phonebk, phonetic, pinyin,
searchjl, stroke, trad, unihan and zhuyin, the same twelve the hardcoded list
ended at. The tests pin both the derivation - the union over every language,
built from getCollations rather than from the expected answer - and that
literal list, so a change to the table has to be a deliberate edit in two
places rather than a silent divergence in one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma added a commit that referenced this pull request Aug 13, 2026
Intl.supportedValuesOf("collation") was a third hardcoded collation list, in
DefaultCldrProvider.GetSupportedCollations, next to the per-language table and
the root pair the other two views read.
https://tc39.es/ecma402/#sec-availablecanonicalcollations asks for "the
collations for which the implementation provides the functionality of
Intl.Collator objects", sorted in lexicographic code unit order and unique -
which is the union of the one [[co]] list every locale has and nothing else, so
there was never a second fact for that list to hold. It is now read off
LocaleCollations, the same derived data getCollations and Intl.Collator resolve
against.

It agreed with them when this was written, and the previous change is what made
that true again - but agreeing today is exactly what the two views did before
#2974. The failure modes are asymmetric and only one of them is caught:
a value listed that no locale resolves turns
intl402/Intl/supportedValuesOf/collations-accepted-by-Collator.js red, which is
how the deprecated CLDR types were noticed, while a collation some locale
reports and the list omits is invisible to test262 - AvailableCollations is
implementation-defined, so nothing checks it from that side. Adding a collation
to LocaleCollationSupport used to produce exactly that silent second case; it
now shows up in all three views by the same act.

"standard", "search" and "default" need no exclusion of their own here.
IsReportableCollation already keeps them out of every list being unioned, which
is what https://tc39.es/ecma402/#sec-intl-collator-internal-slots (10.2.3)
requires of the per-locale lists and what
intl402/Intl/supportedValuesOf/collations.js checks of this one - the same
guard now answering for both. Sorting is likewise not restated: the union is
built in code unit order, which is what 15.5.10 and this clause both ask for
and what IntlInstance.SupportedValuesOf sorts by anyway.

The values are unchanged: compat, dict, emoji, eor, phonebk, phonetic, pinyin,
searchjl, stroke, trad, unihan and zhuyin, the same twelve the hardcoded list
ended at. The tests pin both the derivation - the union over every language,
built from getCollations rather than from the expected answer - and that
literal list, so a change to the table has to be a deliberate edit in two
places rather than a silent divergence in one.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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