From c2bfc5bdcd65c41ebe31e8e50ffd672db03d9a7d Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 8 May 2025 17:49:11 -0400 Subject: [PATCH] Add tests for Intl.Locale.prototype.variants Ref https://github.com/tc39/ecma402/pull/960 --- .../Locale/constructor-getter-order.js | 12 +++ .../constructor-options-throwing-getters.js | 1 + .../constructor-options-variants-invalid.js | 54 ++++++++++++ .../constructor-options-variants-valid.js | 60 +++++++++++++ test/intl402/Locale/getters-grandfathered.js | 75 +++++++++++++++-- test/intl402/Locale/getters-missing.js | 4 + test/intl402/Locale/getters.js | 84 ++++++++++++++++--- .../Locale/prototype/variants/branding.js | 28 +++++++ .../intl402/Locale/prototype/variants/name.js | 22 +++++ .../Locale/prototype/variants/prop-desc.js | 23 +++++ 10 files changed, 341 insertions(+), 22 deletions(-) create mode 100644 test/intl402/Locale/constructor-options-variants-invalid.js create mode 100644 test/intl402/Locale/constructor-options-variants-valid.js create mode 100644 test/intl402/Locale/prototype/variants/branding.js create mode 100644 test/intl402/Locale/prototype/variants/name.js create mode 100644 test/intl402/Locale/prototype/variants/prop-desc.js 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, +});