diff --git a/test/intl402/Locale/constructor-getter-order.js b/test/intl402/Locale/constructor-getter-order.js
index b12ae8ba7f..f4635b1d6a 100644
--- a/test/intl402/Locale/constructor-getter-order.js
+++ b/test/intl402/Locale/constructor-getter-order.js
@@ -44,6 +44,16 @@ new Intl.Locale(
}
},
+ get variants() {
+ order.push("get variants");
+ return {
+ toString() {
+ order.push("toString variants");
+ return "fonipa-1996";
+ }
+ }
+ },
+
get calendar() {
order.push("get calendar");
return {
@@ -109,6 +119,8 @@ const expected_order = [
"toString script",
"get region",
"toString region",
+ "get variants",
+ "toString variants",
"get calendar",
"toString calendar",
"get collation",
diff --git a/test/intl402/Locale/constructor-options-throwing-getters.js b/test/intl402/Locale/constructor-options-throwing-getters.js
index f82abd6221..ba5fb0b4f6 100644
--- a/test/intl402/Locale/constructor-options-throwing-getters.js
+++ b/test/intl402/Locale/constructor-options-throwing-getters.js
@@ -13,6 +13,7 @@ const options = [
"language",
"script",
"region",
+ "variants",
"calendar",
"collation",
"hourCycle",
diff --git a/test/intl402/Locale/constructor-options-variants-invalid.js b/test/intl402/Locale/constructor-options-variants-invalid.js
new file mode 100644
index 0000000000..814801559a
--- /dev/null
+++ b/test/intl402/Locale/constructor-options-variants-invalid.js
@@ -0,0 +1,54 @@
+// Copyright 2025 Richard Gibson. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-intl.locale
+description: >
+ Checks error cases for the options argument to the Locale
+ constructor.
+info: |
+ Intl.Locale( tag [, options] )
+ 12. Set _tag_ to ? UpdateLanguageId(_tag_, _options_).
+
+ UpdateLanguageId ( tag, options )
+ 8. Let _variants_ be ? GetOption(_options_, *"variants"*, ~string~, ~empty~, GetLocaleVariants(_baseName_)).
+ 9. If _variants_ is not *undefined*, then
+ a. If _variants_ cannot be matched by the unicode_variant_subtag
Unicode locale nonterminal, throw a *RangeError* exception.
+
+features: [Intl.Locale]
+---*/
+
+/*
+ unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3})
+*/
+const invalidVariantsOptions = [
+ "",
+ "a",
+ "1",
+ "ab",
+ "2x",
+ "abc",
+ "3xy",
+ "abcd",
+ "abcdefghi",
+
+ // Value contains more than just the 'variants' production.
+ "GB-scouse",
+
+ // Value contains duplicates.
+ "fonipa-fonipa",
+ "fonipa-valencia-Fonipa",
+
+ // Value contains out-of-place dashes.
+ "-",
+ "-spanglis",
+ "spanglis-",
+ "-spanglis-oxendict",
+ "spanglis-oxendict-",
+ "spanglis--oxendict",
+];
+for (const variants of invalidVariantsOptions) {
+ assert.throws(RangeError, function() {
+ new Intl.Locale("en", {variants});
+ }, `new Intl.Locale("en", {variants: "${variants}"}) throws RangeError`);
+}
diff --git a/test/intl402/Locale/constructor-options-variants-valid.js b/test/intl402/Locale/constructor-options-variants-valid.js
new file mode 100644
index 0000000000..1f2134a247
--- /dev/null
+++ b/test/intl402/Locale/constructor-options-variants-valid.js
@@ -0,0 +1,60 @@
+// Copyright 2025 Richard Gibson. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-intl.locale
+description: >
+ Checks error cases for the options argument to the Locale
+ constructor.
+info: |
+ Intl.Locale( tag [, options] )
+ 12. Set _tag_ to ? UpdateLanguageId(_tag_, _options_).
+
+ UpdateLanguageId ( tag, options )
+ 8. Let _variants_ be ? GetOption(_options_, *"variants"*, ~string~, ~empty~, GetLocaleVariants(_baseName_)).
+ ...
+ 13. If _variants_ is not *undefined*, set _newTag_ to the string-concatenation of _newTag_, *"-"*, and _variants_.
+
+features: [Intl.Locale]
+---*/
+
+const validVariantsOptions = [
+ ['en', undefined, undefined],
+ ['en', 'spanglis', 'en-spanglis'],
+
+ // unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3})
+ ['xx', '1xyz', 'xx-1xyz'],
+ ['xx', '1234', 'xx-1234'],
+ ['xx', 'abcde', 'xx-abcde'],
+ ['xx', '12345678', 'xx-12345678'],
+ ['xx', '1xyz-1234-abcde-12345678', 'xx-1234-12345678-1xyz-abcde'],
+
+ // Canonicalization affects subtag ordering.
+ ['en', 'spanglis-oxendict', 'en-oxendict-spanglis'],
+];
+for (const [lang, variants, baseName] of validVariantsOptions) {
+ let options = { variants };
+ let optionsRepr = `{variants: ${typeof variants === "string" ? `"${variants}"` : variants}}`;
+ let instance;
+ let expect;
+
+ instance = new Intl.Locale(lang, options);
+ expect = baseName || lang;
+ assert.sameValue(instance.toString(), expect,
+ `new Intl.Locale("${lang}", ${optionsRepr}).toString() returns "${expect}"`);
+
+ instance = new Intl.Locale(lang + '-fonipa', options);
+ expect = baseName || (lang + '-fonipa');
+ assert.sameValue(instance.toString(), expect,
+ `new Intl.Locale("${lang}-fonipa", ${optionsRepr}).toString() returns "${expect}"`);
+
+ instance = new Intl.Locale(lang + '-u-ca-gregory', options);
+ expect = (baseName || lang) + '-u-ca-gregory';
+ assert.sameValue(instance.toString(), expect,
+ `new Intl.Locale("${lang}-u-ca-gregory", ${optionsRepr}).toString() returns "${expect}"`);
+
+ instance = new Intl.Locale(lang + '-fonipa-u-ca-gregory', options);
+ expect = (baseName || (lang + '-fonipa')) + '-u-ca-gregory';
+ assert.sameValue(instance.toString(), expect,
+ `new Intl.Locale("${lang}-fonipa-u-ca-gregory", ${optionsRepr}).toString() returns "${expect}"`);
+}
diff --git a/test/intl402/Locale/getters-grandfathered.js b/test/intl402/Locale/getters-grandfathered.js
index 261af80848..50e5406439 100644
--- a/test/intl402/Locale/getters-grandfathered.js
+++ b/test/intl402/Locale/getters-grandfathered.js
@@ -7,21 +7,70 @@ description: >
Verifies getters with grandfathered tags.
info: |
get Intl.Locale.prototype.baseName
- 5. Return the substring of locale corresponding to the
- language ["-" script] ["-" region] *("-" variant)
- subsequence of the unicode_language_id grammar.
+ 3. Return GetLocaleBaseName(_loc_.[[Locale]]).
+
+ GetLocaleBaseName
+ 2. Return the longest prefix of _locale_ matched by the
+ unicode_language_id
Unicode locale nonterminal.
get Intl.Locale.prototype.language
- 5. Return the substring of locale corresponding to the
- unicode_language_subtag production.
+ 3. Return GetLocaleLanguage(_loc_.[[Locale]]).
+
+ GetLocaleLanguage
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. Assert: The first subtag of _baseName_ can be matched by the
+ unicode_language_subtag
Unicode locale nonterminal.
+ 3. Return the first subtag of _baseName_.
get Intl.Locale.prototype.script
- 6. Return the substring of locale corresponding to the
- unicode_script_subtag production.
+ 3. Return GetLocaleScript(_loc_.[[Locale]]).
+
+ GetLocaleScript
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. Assert: _baseName_ contains at most one subtag that can be matched by the
+ unicode_script_subtag
Unicode locale nonterminal.
+ 3. If _baseName_ contains a subtag matched by the
+ unicode_script_subtag
Unicode locale nonterminal, return
+ that subtag.
+ 4. Return *undefined*.
get Intl.Locale.prototype.region
- 6. Return the substring of locale corresponding to the unicode_region_subtag
- production.
+ 3. Return GetLocaleRegion(_loc_.[[Locale]]).
+
+ GetLocaleRegion
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. NOTE: A unicode_region_subtag
subtag is only valid
+ immediately after an initial unicode_language_subtag
subtag,
+ optionally with a single unicode_script_subtag
subtag
+ between them. In that position, unicode_region_subtag
cannot
+ be confused with any other valid subtag because all their productions are
+ disjoint.
+ 3. Assert: The first subtag of _baseName_ can be matched by the
+ unicode_language_subtag
Unicode locale nonterminal.
+ 4. Let _baseNameTail_ be the suffix of _baseName_ following the first
+ subtag.
+ 5. Assert: _baseNameTail_ contains at most one subtag that can be matched by
+ the unicode_region_subtag
Unicode locale nonterminal.
+ 6. If _baseNameTail_ contains a subtag matched by the
+ unicode_region_subtag
Unicode locale nonterminal, return
+ that subtag.
+ 7. Return *undefined*.
+
+ get Intl.Locale.prototype.variants
+ 3. Return GetLocaleVariants(_loc_.[[Locale]]).
+
+ GetLocaleVariants
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. NOTE: Each subtag in _baseName_ that is preceded by *"-"* is either a
+ unicode_script_subtag
, unicode_region_subtag
,
+ or unicode_variant_subtag
, but any substring matched by
+ unicode_variant_subtag
is strictly longer than any prefix
+ thereof which could also be matched by one of the other productions.
+ 3. Let _variants_ be the longest suffix of _baseName_ that starts with a
+ *"-"* followed by a substring that is matched
+ by the unicode_variant_subtag
Unicode locale nonterminal. If
+ there is no such suffix, return *undefined*.
+ 4. Return the substring of _variants_ from 1.
features: [Intl.Locale]
---*/
@@ -31,6 +80,14 @@ assert.sameValue(loc.baseName, "xtg");
assert.sameValue(loc.language, "xtg");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, undefined);
+
+loc = new Intl.Locale("cel", { variants: "gaulish" });
+assert.sameValue(loc.baseName, "xtg");
+assert.sameValue(loc.language, "xtg");
+assert.sameValue(loc.script, undefined);
+assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, undefined);
// Regular grandfathered language tag.
assert.throws(RangeError, () => new Intl.Locale("zh-min"));
diff --git a/test/intl402/Locale/getters-missing.js b/test/intl402/Locale/getters-missing.js
index e57c8a300e..a48cf6d7cf 100644
--- a/test/intl402/Locale/getters-missing.js
+++ b/test/intl402/Locale/getters-missing.js
@@ -30,6 +30,7 @@ assert.sameValue(loc.baseName, "sv");
assert.sameValue(loc.language, "sv");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, undefined);
// 'region' subtag not present.
var loc = new Intl.Locale("sv-Latn");
@@ -37,6 +38,7 @@ assert.sameValue(loc.baseName, "sv-Latn");
assert.sameValue(loc.language, "sv");
assert.sameValue(loc.script, "Latn");
assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, undefined);
// 'script' subtag not present.
var loc = new Intl.Locale("sv-SE");
@@ -44,6 +46,7 @@ assert.sameValue(loc.baseName, "sv-SE");
assert.sameValue(loc.language, "sv");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, "SE");
+assert.sameValue(loc.variants, undefined);
// 'variant' subtag present.
var loc = new Intl.Locale("de-1901");
@@ -51,3 +54,4 @@ assert.sameValue(loc.baseName, "de-1901");
assert.sameValue(loc.language, "de");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, '1901');
diff --git a/test/intl402/Locale/getters.js b/test/intl402/Locale/getters.js
index 3ee86e186f..dd9823c3be 100644
--- a/test/intl402/Locale/getters.js
+++ b/test/intl402/Locale/getters.js
@@ -10,18 +10,70 @@ info: |
3. Return loc.[[Locale]].
get Intl.Locale.prototype.baseName
- 5. Return the substring of locale corresponding to the
- language ["-" script] ["-" region] *("-" variant)
- subsequence of the langtag grammar.
+ 3. Return GetLocaleBaseName(_loc_.[[Locale]]).
+
+ GetLocaleBaseName
+ 2. Return the longest prefix of _locale_ matched by the
+ unicode_language_id
Unicode locale nonterminal.
get Intl.Locale.prototype.language
- 4. Return the substring of locale corresponding to the language production.
+ 3. Return GetLocaleLanguage(_loc_.[[Locale]]).
+
+ GetLocaleLanguage
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. Assert: The first subtag of _baseName_ can be matched by the
+ unicode_language_subtag
Unicode locale nonterminal.
+ 3. Return the first subtag of _baseName_.
get Intl.Locale.prototype.script
- 7. Return the substring of locale corresponding to the script production.
+ 3. Return GetLocaleScript(_loc_.[[Locale]]).
+
+ GetLocaleScript
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. Assert: _baseName_ contains at most one subtag that can be matched by the
+ unicode_script_subtag
Unicode locale nonterminal.
+ 3. If _baseName_ contains a subtag matched by the
+ unicode_script_subtag
Unicode locale nonterminal, return
+ that subtag.
+ 4. Return *undefined*.
get Intl.Locale.prototype.region
- 7. Return the substring of locale corresponding to the region production.
+ 3. Return GetLocaleRegion(_loc_.[[Locale]]).
+
+ GetLocaleRegion
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. NOTE: A unicode_region_subtag
subtag is only valid
+ immediately after an initial unicode_language_subtag
subtag,
+ optionally with a single unicode_script_subtag
subtag
+ between them. In that position, unicode_region_subtag
cannot
+ be confused with any other valid subtag because all their productions are
+ disjoint.
+ 3. Assert: The first subtag of _baseName_ can be matched by the
+ unicode_language_subtag
Unicode locale nonterminal.
+ 4. Let _baseNameTail_ be the suffix of _baseName_ following the first
+ subtag.
+ 5. Assert: _baseNameTail_ contains at most one subtag that can be matched by
+ the unicode_region_subtag
Unicode locale nonterminal.
+ 6. If _baseNameTail_ contains a subtag matched by the
+ unicode_region_subtag
Unicode locale nonterminal, return
+ that subtag.
+ 7. Return *undefined*.
+
+ get Intl.Locale.prototype.variants
+ 3. Return GetLocaleVariants(_loc_.[[Locale]]).
+
+ GetLocaleVariants
+ 1. Let _baseName_ be GetLocaleBaseName(_locale_).
+ 2. NOTE: Each subtag in _baseName_ that is preceded by *"-"* is either a
+ unicode_script_subtag
, unicode_region_subtag
,
+ or unicode_variant_subtag
, but any substring matched by
+ unicode_variant_subtag
is strictly longer than any prefix
+ thereof which could also be matched by one of the other productions.
+ 3. Let _variants_ be the longest suffix of _baseName_ that starts with a
+ *"-"* followed by a substring that is matched
+ by the unicode_variant_subtag
Unicode locale nonterminal. If
+ there is no such suffix, return *undefined*.
+ 4. Return the substring of _variants_ from 1.
get Intl.Locale.prototype.calendar
3. Return loc.[[Calendar]].
@@ -47,14 +99,15 @@ features: [Intl.Locale]
---*/
// Test all getters return the expected results.
-var langtag = "de-latn-de-u-ca-gregory-co-phonebk-hc-h23-kf-true-kn-false-nu-latn";
+var langtag = "de-latn-de-fonipa-1996-u-ca-gregory-co-phonebk-hc-h23-kf-true-kn-false-nu-latn";
var loc = new Intl.Locale(langtag);
-assert.sameValue(loc.toString(), "de-Latn-DE-u-ca-gregory-co-phonebk-hc-h23-kf-kn-false-nu-latn");
-assert.sameValue(loc.baseName, "de-Latn-DE");
+assert.sameValue(loc.toString(), "de-Latn-DE-1996-fonipa-u-ca-gregory-co-phonebk-hc-h23-kf-kn-false-nu-latn");
+assert.sameValue(loc.baseName, "de-Latn-DE-1996-fonipa");
assert.sameValue(loc.language, "de");
assert.sameValue(loc.script, "Latn");
assert.sameValue(loc.region, "DE");
+assert.sameValue(loc.variants, "1996-fonipa");
assert.sameValue(loc.calendar, "gregory");
assert.sameValue(loc.collation, "phonebk");
assert.sameValue(loc.hourCycle, "h23");
@@ -72,6 +125,7 @@ var loc = new Intl.Locale(langtag, {
language: "ja",
script: "jpan",
region: "jp",
+ variants: "Hepburn",
calendar: "japanese",
collation: "search",
hourCycle: "h24",
@@ -80,11 +134,12 @@ var loc = new Intl.Locale(langtag, {
numberingSystem: "jpanfin",
});
-assert.sameValue(loc.toString(), "ja-Jpan-JP-u-ca-japanese-co-search-hc-h24-kf-false-kn-nu-jpanfin");
-assert.sameValue(loc.baseName, "ja-Jpan-JP");
+assert.sameValue(loc.toString(), "ja-Jpan-JP-hepburn-u-ca-japanese-co-search-hc-h24-kf-false-kn-nu-jpanfin");
+assert.sameValue(loc.baseName, "ja-Jpan-JP-hepburn");
assert.sameValue(loc.language, "ja");
assert.sameValue(loc.script, "Jpan");
assert.sameValue(loc.region, "JP");
+assert.sameValue(loc.variants, "hepburn");
assert.sameValue(loc.calendar, "japanese");
assert.sameValue(loc.collation, "search");
assert.sameValue(loc.hourCycle, "h24");
@@ -105,11 +160,12 @@ var loc = new Intl.Locale(langtag, {
hourCycle: "h11",
});
-assert.sameValue(loc.toString(), "fr-Latn-CA-u-ca-gregory-co-standard-hc-h11-kf-kn-false-nu-latn");
-assert.sameValue(loc.baseName, "fr-Latn-CA");
+assert.sameValue(loc.toString(), "fr-Latn-CA-1996-fonipa-u-ca-gregory-co-standard-hc-h11-kf-kn-false-nu-latn");
+assert.sameValue(loc.baseName, "fr-Latn-CA-1996-fonipa");
assert.sameValue(loc.language, "fr");
assert.sameValue(loc.script, "Latn");
assert.sameValue(loc.region, "CA");
+assert.sameValue(loc.variants, "1996-fonipa");
assert.sameValue(loc.calendar, "gregory");
assert.sameValue(loc.collation, "standard");
assert.sameValue(loc.hourCycle, "h11");
@@ -129,6 +185,7 @@ assert.sameValue(loc.baseName, "und");
assert.sameValue(loc.language, "und");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, undefined);
+assert.sameValue(loc.variants, undefined);
var loc = new Intl.Locale("und-US-u-co-emoji");
@@ -137,6 +194,7 @@ assert.sameValue(loc.baseName, "und-US");
assert.sameValue(loc.language, "und");
assert.sameValue(loc.script, undefined);
assert.sameValue(loc.region, "US");
+assert.sameValue(loc.variants, undefined);
if ("collation" in loc) {
assert.sameValue(loc.collation, "emoji");
}
diff --git a/test/intl402/Locale/prototype/variants/branding.js b/test/intl402/Locale/prototype/variants/branding.js
new file mode 100644
index 0000000000..f55e1d482e
--- /dev/null
+++ b/test/intl402/Locale/prototype/variants/branding.js
@@ -0,0 +1,28 @@
+// Copyright 2025 Richard Gibson. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-Intl.Locale.prototype.variants
+description: >
+ Verifies the branding check for the "variants" property of the Locale prototype object.
+info: |
+ Intl.Locale.prototype.variants
+ 2. Perform ? RequireInternalSlot(_loc_, [[InitializedLocale]]).
+features: [Intl.Locale]
+---*/
+
+const propdesc = Object.getOwnPropertyDescriptor(Intl.Locale.prototype, "variants");
+const invalidValues = [
+ undefined,
+ null,
+ true,
+ "",
+ Symbol(),
+ 1,
+ {},
+ Intl.Locale.prototype,
+];
+
+for (const invalidValue of invalidValues) {
+ assert.throws(TypeError, () => propdesc.get.call(invalidValue));
+}
diff --git a/test/intl402/Locale/prototype/variants/name.js b/test/intl402/Locale/prototype/variants/name.js
new file mode 100644
index 0000000000..b4ec70d5e1
--- /dev/null
+++ b/test/intl402/Locale/prototype/variants/name.js
@@ -0,0 +1,22 @@
+// Copyright 2025 Richard Gibson. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-intl.locale.prototype.variants
+description: >
+ Checks the "name" property of Intl.Locale.prototype.variants.
+info: |
+ Unless specified otherwise in this document, the objects, functions, and constructors described in this standard are subject to the generic requirements and restrictions specified for standard built-in ECMAScript objects in the ECMAScript 2019 Language Specification, 10th edition, clause 17, or successor.
+ Every built-in function object, including constructors, that is not identified as an anonymous function has a name property whose value is a String. Unless otherwise specified, this value is the name that is given to the function in this specification. Functions that are specified as get or set accessor functions of built-in properties have "get " or "set " prepended to the property name string.
+ Unless otherwise specified, the name property of a built-in function object, if it exists, has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
+includes: [propertyHelper.js]
+features: [Intl.Locale]
+---*/
+
+const getter = Object.getOwnPropertyDescriptor(Intl.Locale.prototype, "variants").get;
+verifyProperty(getter, "name", {
+ value: "get variants",
+ writable: false,
+ enumerable: false,
+ configurable: true,
+});
diff --git a/test/intl402/Locale/prototype/variants/prop-desc.js b/test/intl402/Locale/prototype/variants/prop-desc.js
new file mode 100644
index 0000000000..4b376e2c85
--- /dev/null
+++ b/test/intl402/Locale/prototype/variants/prop-desc.js
@@ -0,0 +1,23 @@
+// Copyright 2025 Richard Gibson. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-intl.locale
+description: >
+ Checks the "variants" property of the Locale prototype object.
+info: |
+ Unless specified otherwise in this document, the objects, functions, and constructors described in this standard are subject to the generic requirements and restrictions specified for standard built-in ECMAScript objects in the ECMAScript 2019 Language Specification, 10th edition, clause 17, or successor.
+
+ Every accessor property described in clauses 18 through 26 and in Annex B.2 has the attributes { [[Enumerable]]: false, [[Configurable]]: true } unless otherwise specified. If only a get accessor function is described, the set accessor function is the default value, undefined.
+includes: [propertyHelper.js]
+features: [Intl.Locale]
+---*/
+
+const propdesc = Object.getOwnPropertyDescriptor(Intl.Locale.prototype, "variants");
+assert.sameValue(propdesc.set, undefined);
+assert.sameValue(typeof propdesc.get, "function");
+
+verifyProperty(Intl.Locale.prototype, "variants", {
+ enumerable: false,
+ configurable: true,
+});