Skip to content

Commit 33b8cad

Browse files
committed
Implement Intl.Locale
1 parent 962624d commit 33b8cad

11 files changed

+976
-122
lines changed

lib/Parser/rterrors.h

+1
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ RT_ERROR_MSG(JSERR_MissingCurrencyCode, 5123, "", "Currency code was not specifi
257257
RT_ERROR_MSG(JSERR_InvalidDate, 5124, "", "Invalid Date", kjstRangeError, 0)
258258
RT_ERROR_MSG(JSERR_IntlNotAvailable, 5125, "", "Intl is not available.", kjstTypeError, 0)
259259
RT_ERROR_MSG(JSERR_IntlNotImplemented, 5126, "", "Intl operation '%s' is not implemented.", kjstTypeError, 0)
260+
RT_ERROR_MSG(JSERR_InvalidPrivateOrGrandfatheredTag, 5127, "", "The arguments provided to Intl.Locale form an invalid privateuse or grandfathered language tag", kjstRangeError, 0)
260261

261262
RT_ERROR_MSG(JSERR_ArgumentOutOfRange, 5130, "%s: argument out of range", "argument out of range", kjstRangeError, 0)
262263
RT_ERROR_MSG(JSERR_ErrorOnNew, 5131, "", "Function is not a constructor", kjstTypeError, 0)

lib/Runtime/Base/JnDirectFields.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ ENTRY(builtInJavascriptArrayEntryMap)
658658
ENTRY(builtInJavascriptArrayEntryPush)
659659
ENTRY(builtInJavascriptArrayEntryReduce)
660660
ENTRY(builtInJavascriptArrayEntrySlice)
661+
ENTRY(builtInJavascriptArrayEntrySort)
661662
ENTRY(builtInJavascriptDateEntryGetDate)
662663
ENTRY(builtInJavascriptDateEntryNow)
663664
ENTRY(builtInJavascriptFunctionEntryApply)
@@ -671,7 +672,6 @@ ENTRY(builtInJavascriptObjectEntryIsExtensible)
671672
ENTRY(builtInJavascriptObjectEntryKeys)
672673
ENTRY(builtInJavascriptObjectGetOwnPropertyDescriptor)
673674
ENTRY(builtInJavascriptObjectPreventExtensions)
674-
ENTRY(builtInJavascriptRegExpEntryTest) // TODO(jahorto): is this needed?
675675
ENTRY(builtInJavascriptStringEntryIndexOf)
676676
ENTRY(builtInJavascriptStringEntryMatch)
677677
ENTRY(builtInJavascriptStringEntryRepeat)
@@ -708,6 +708,7 @@ ENTRY(raiseOptionValueOutOfRange_3)
708708
ENTRY(raiseOptionValueOutOfRange)
709709
ENTRY(raiseThis_NullOrUndefined)
710710
ENTRY(raiseFunctionArgument_NeedFunction)
711+
ENTRY(raiseInvalidPrivateOrGrandfatheredTag)
711712

712713
// Promise (ChakraFull)
713714
ENTRY(Promise)

lib/Runtime/Library/Chakra.Runtime.Library.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
<ClInclude Include="..\SerializableFunctionFields.h" />
158158
<ClInclude Include="AtomicsObject.h" />
159159
<ClInclude Include="AtomicsOperations.h" />
160+
<ClInclude Include="EngineInterfaceObjectBuiltIns.h" />
160161
<ClInclude Include="PropertyRecordUsageCache.h" />
161162
<ClInclude Include="CustomExternalIterator.h" />
162163
<ClInclude Include="JsBuiltInEngineInterfaceExtensionObject.h" />

lib/Runtime/Library/Chakra.Runtime.Library.vcxproj.filters

+4-1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@
226226
<ClInclude Include="JsBuiltIn\JsBuiltIn.js.nojit.bc.64b.h" />
227227
<ClInclude Include="PropertyRecordUsageCache.h" />
228228
<ClInclude Include="..\LibraryFunction.h" />
229+
<ClInclude Include="IntlExtensionObjectBuiltIns.h" />
230+
<ClInclude Include="StringCacheList.h" />
231+
<ClInclude Include="EngineInterfaceObjectBuiltIns.h" />
229232
</ItemGroup>
230233
<ItemGroup>
231234
<None Include="ConcatString.inl" />
@@ -286,4 +289,4 @@
286289
<ARMASM Include="$(MSBuildThisFileDirectory)arm64\arm64_CallFunction.asm" />
287290
<ARMASM Include="$(MSBuildThisFileDirectory)arm64\arm64_DeferredParsingThunk.asm" />
288291
</ItemGroup>
289-
</Project>
292+
</Project>

lib/Runtime/Library/EngineInterfaceObjectBuiltIns.h

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ GlobalBuiltIn(JavascriptArray, EntryMap)
3030
GlobalBuiltIn(JavascriptArray, EntryReduce)
3131
GlobalBuiltIn(JavascriptArray, EntrySlice)
3232
GlobalBuiltIn(JavascriptArray, EntryConcat)
33+
GlobalBuiltIn(JavascriptArray, EntrySort)
3334

3435
GlobalBuiltIn(JavascriptFunction, EntryBind)
3536
GlobalBuiltIn(JavascriptFunction, EntryApply)
@@ -64,3 +65,4 @@ BuiltInRaiseException1(RangeError, InvalidCurrencyCode)
6465
BuiltInRaiseException(TypeError, MissingCurrencyCode)
6566
BuiltInRaiseException(RangeError, InvalidDate)
6667
BuiltInRaiseException1(TypeError, FunctionArgument_NeedFunction)
68+
BuiltInRaiseException(RangeError, InvalidPrivateOrGrandfatheredTag)

lib/Runtime/Library/InJavascript/Intl.js

+766-111
Large diffs are not rendered by default.

lib/Runtime/Library/IntlEngineInterfaceExtensionObject.cpp

+71-2
Original file line numberDiff line numberDiff line change
@@ -944,8 +944,7 @@ PROJECTED_ENUMS(PROJECTED_ENUM)
944944
// of caution and say it is invalid.
945945
// We also check for parsedLength < langtag->GetLength() because there are cases when status == U_ZERO_ERROR
946946
// but the langtag was not valid, such as "en-tesTER-TESter" (OSS-Fuzz #6657).
947-
// NOTE: make sure we check for `undefined` at the platform.normalizeLanguageTag callsite.
948-
return scriptContext->GetLibrary()->GetUndefined();
947+
JavascriptError::ThrowRangeError(scriptContext, JSERR_LocaleNotWellFormed, langtag);
949948
}
950949

951950
// forLangTagResultLength can be 0 if langtag is "und".
@@ -3157,6 +3156,76 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
31573156
#endif
31583157
}
31593158

3159+
#ifdef INTL_ICU
3160+
template <bool minimize>
3161+
static JavascriptString *MinMaxImpl(JavascriptString *langtag, ScriptContext *scriptContext)
3162+
{
3163+
UErrorCode status = U_ZERO_ERROR;
3164+
char localeID[ULOC_FULLNAME_CAPACITY] = { 0 };
3165+
LangtagToLocaleID(langtag, localeID);
3166+
3167+
char minmaxLocaleID[ULOC_FULLNAME_CAPACITY] = { 0 };
3168+
int32_t minmaxLocaleIDLength = 0;
3169+
if (minimize)
3170+
{
3171+
minmaxLocaleIDLength = uloc_minimizeSubtags(localeID, minmaxLocaleID, ULOC_FULLNAME_CAPACITY, &status);
3172+
INTL_TRACE("Minimizing localeID %S to %S", localeID, minmaxLocaleID);
3173+
}
3174+
else
3175+
{
3176+
minmaxLocaleIDLength = uloc_addLikelySubtags(localeID, minmaxLocaleID, ULOC_FULLNAME_CAPACITY, &status);
3177+
INTL_TRACE("Maximizing localeID %S to %S", localeID, minmaxLocaleID);
3178+
}
3179+
ICU_ASSERT(status, minmaxLocaleIDLength < ULOC_FULLNAME_CAPACITY);
3180+
3181+
char minmaxLangtag[ULOC_FULLNAME_CAPACITY] = { 0 };
3182+
int minmaxLangtagLength = uloc_toLanguageTag(minmaxLocaleID, minmaxLangtag, ULOC_FULLNAME_CAPACITY, true, &status);
3183+
ICU_ASSERT(status, minmaxLangtagLength > 0);
3184+
3185+
// allocate maximizedLangtagLength + 1 to leave room for null terminator
3186+
char16 *minmaxLangtag16 = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, minmaxLangtagLength + 1);
3187+
charcount_t minmaxLangtag16Length = 0;
3188+
HRESULT hr = utf8::NarrowStringToWideNoAlloc(
3189+
minmaxLangtag,
3190+
minmaxLangtagLength,
3191+
minmaxLangtag16,
3192+
minmaxLangtagLength + 1,
3193+
&minmaxLangtag16Length
3194+
);
3195+
AssertOrFailFast(hr == S_OK && ((int)minmaxLangtag16Length) == minmaxLangtagLength);
3196+
3197+
return JavascriptString::NewWithBuffer(minmaxLangtag16, minmaxLangtagLength, scriptContext);
3198+
}
3199+
#endif
3200+
3201+
Var IntlEngineInterfaceExtensionObject::EntryIntl_MinimizeLocale(RecyclableObject *function, CallInfo callInfo, ...)
3202+
{
3203+
#ifdef INTL_ICU
3204+
EngineInterfaceObject_CommonFunctionProlog(function, callInfo);
3205+
3206+
INTL_CHECK_ARGS(args.Info.Count == 2 && JavascriptString::Is(args.Values[1]));
3207+
3208+
return MinMaxImpl<true>(JavascriptString::UnsafeFromVar(args.Values[1]), scriptContext);
3209+
#else
3210+
AssertOrFailFastMsg(false, "Intl-WinGlob should not be using MinimizeLocale");
3211+
return nullptr;
3212+
#endif
3213+
}
3214+
3215+
Var IntlEngineInterfaceExtensionObject::EntryIntl_MaximizeLocale(RecyclableObject *function, CallInfo callInfo, ...)
3216+
{
3217+
#ifdef INTL_ICU
3218+
EngineInterfaceObject_CommonFunctionProlog(function, callInfo);
3219+
3220+
INTL_CHECK_ARGS(args.Info.Count == 2 && JavascriptString::Is(args.Values[1]));
3221+
3222+
return MinMaxImpl<false>(JavascriptString::UnsafeFromVar(args.Values[1]), scriptContext);
3223+
#else
3224+
AssertOrFailFastMsg(false, "Intl-WinGlob should not be using MaximizeLocale");
3225+
return nullptr;
3226+
#endif
3227+
}
3228+
31603229
/*
31613230
* This function registers built in functions when Intl initializes.
31623231
* Call with (Function : toRegister, integer : id)

lib/Runtime/Library/IntlExtensionObjectBuiltIns.h

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ INTL_ENTRY(getLocaleData, GetLocaleData)
3737
INTL_ENTRY(localeCompare, LocaleCompare)
3838
INTL_ENTRY(pluralRulesSelect, PluralRulesSelect)
3939
INTL_ENTRY(pluralRulesKeywords, PluralRulesKeywords)
40+
INTL_ENTRY(minimizeLocale, MinimizeLocale)
41+
INTL_ENTRY(maximizeLocale, MaximizeLocale)
4042

4143
INTL_ENTRY(registerBuiltInFunction, RegisterBuiltInFunction)
4244
INTL_ENTRY(getHiddenObject, GetHiddenObject)

test/Intl/Locale.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//-------------------------------------------------------------------------------------------------------
2+
// Copyright (C) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
4+
//-------------------------------------------------------------------------------------------------------
5+
6+
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
7+
8+
testRunner.runTests([
9+
{
10+
name: "Basic functionality",
11+
body() {
12+
assert.areEqual("Locale", Intl.Locale.name);
13+
const locale = new Intl.Locale("en");
14+
assert.areEqual("en", locale.toString());
15+
assert.areEqual("[object Intl.Locale]", Object.prototype.toString.call(locale));
16+
},
17+
},
18+
{
19+
name: "Applying options",
20+
body() {
21+
function test(expected, langtag, options) {
22+
const locale = new Intl.Locale(langtag, options);
23+
assert.areEqual(expected, locale.toString());
24+
}
25+
26+
test("es", "es");
27+
test("de", "en", { language: "de" });
28+
test("de-Latn-DE-u-ca-chinese", "en", { language: "de", script: "Latn", region: "DE", calendar: "chinese" });
29+
test("ar-u-co-unihan", "ar-u-co-unihan");
30+
test("ar-u-co-unihan", "ar", { collation: "unihan" });
31+
}
32+
},
33+
{
34+
name: "Using an existing Locale object for the langtag argument",
35+
body() {
36+
const enUS = new Intl.Locale("en-US");
37+
assert.areEqual("en", enUS.language);
38+
assert.areEqual("US", enUS.region);
39+
40+
const enGB = new Intl.Locale(enUS, { region: "GB" });
41+
assert.areEqual("en", enGB.language);
42+
assert.areEqual("GB", enGB.region);
43+
44+
const deGB = new Intl.Locale(enGB, { language: "de" });
45+
assert.areEqual("de", deGB.language);
46+
assert.areEqual("GB", deGB.region);
47+
48+
const deLatnGB = new Intl.Locale(deGB, { script: "Latn" });
49+
assert.areEqual("de", deLatnGB.language);
50+
assert.areEqual("GB", deLatnGB.region);
51+
assert.areEqual("Latn", deLatnGB.script);
52+
53+
const dePhonebk = new Intl.Locale("de-u-co-phonebk");
54+
assert.areEqual("de", dePhonebk.language);
55+
assert.areEqual("phonebk", dePhonebk.collation);
56+
57+
const deUnihan = new Intl.Locale(dePhonebk, { collation: "unihan" });
58+
assert.areEqual("de", deUnihan.language);
59+
assert.areEqual("unihan", deUnihan.collation);
60+
61+
const esUnihanH24 = new Intl.Locale(deUnihan, { language: "es", hourCycle: "h24" });
62+
assert.areEqual("es", esUnihanH24.language);
63+
assert.areEqual("unihan", esUnihanH24.collation);
64+
assert.areEqual("h24", esUnihanH24.hourCycle);
65+
}
66+
},
67+
{
68+
name: "Maximizing and minimizing",
69+
body() {
70+
function test(input, expected, minimal, maximal) {
71+
const locale = new Intl.Locale(input);
72+
assert.areEqual(expected, locale.toString(), `Incorrect canonicalization of ${input}`);
73+
assert.areEqual(minimal, locale.minimize().toString(), `Incorrect minimization of ${input}`);
74+
assert.areEqual(maximal, locale.maximize().toString(), `Incorrect maximization of ${input}`);
75+
}
76+
77+
test("en", "en", "en", "en-Latn-US");
78+
test("DE-de", "de-DE", "de", "de-Latn-DE");
79+
80+
// cases inspired by the examples in https://www.unicode.org/reports/tr35/#Likely_Subtags
81+
test("zh", "zh", "zh", "zh-Hans-CN");
82+
test("zh-HanT", "zh-Hant", "zh-Hant", "zh-Hant-TW");
83+
test("zh-MO", "zh-MO", "zh-MO", "zh-Hant-MO");
84+
test("ZH-Hant-TW", "zh-Hant-TW", "zh-TW", "zh-Hant-TW");
85+
test("und-Hant", "und-Hant", "und-Hant", "zh-Hant-TW");
86+
87+
// the UTS35 example says the maximized version should be fa-Arab-AF?
88+
test("und-Arab-AF", "und-Arab-AF", "und-Arab-AF", "ar-Arab-AF");
89+
90+
// Chakra performs incorrect canonicalization, so the following cases don't pass.
91+
// test("sh-Arab-AQ", "sr-Arab-AQ", "sr-Arab-AQ", "sr-Arab-AQ");
92+
// test("ZH-ZZZZ-SG", "zh-SG", "zh-SG", "zh-Hans-SG");
93+
}
94+
},
95+
], { verbose: false })

test/Intl/common.js

+26-7
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
77

8-
const constructors = [Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat];
9-
10-
if (WScript.Platform.INTL_LIBRARY === "icu") {
11-
constructors.push(Intl.PluralRules);
12-
}
13-
148
testRunner.runTests([
159
{
1610
name: "OSS-Fuzz #6657: stress uloc_forLanguageTag status code and parsed length on duplicate variant subtags",
1711
body() {
12+
const constructors = [Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat];
13+
14+
if (WScript.Platform.INTL_LIBRARY === "icu") {
15+
constructors.push(Intl.PluralRules, Intl.Locale);
16+
}
17+
1818
function test(Ctor, locale) {
1919
assert.throws(() => new Ctor(locale), RangeError, `new Intl.${Ctor.name}("${locale}") should throw`);
2020
}
@@ -119,5 +119,24 @@ testRunner.runTests([
119119
testFallbackSymbol(Intl.DateTimeFormat, true);
120120
assert.throws(() => Intl.PluralRules.call(new Intl.PluralRules()), TypeError, "Intl.PluralRules requires `new`");
121121
}
122-
}
122+
},
123+
{
124+
name: "Intl.Locale as the language tag argument",
125+
body() {
126+
if (WScript.Platform.INTL_LIBRARY === "winglob") {
127+
return;
128+
}
129+
130+
const locale = new Intl.Locale("de");
131+
// At the time of writing, the draft spec does not allow Intl.Locale instances to be provided outside of an array
132+
// for the language tag argument. See https://github.com/tc39/proposal-intl-locale/issues/60
133+
assert.areEqual(0, Intl.getCanonicalLocales(locale).length);
134+
assert.areEqual("de", Intl.getCanonicalLocales([locale])[0]);
135+
assert.areEqual("de", Intl.getCanonicalLocales(["en", locale, "zh"])[1]);
136+
for (const Ctor of [Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat, Intl.PluralRules]) {
137+
assert.areEqual(new Ctor().resolvedOptions().locale, new Ctor(locale).resolvedOptions().locale, `${Ctor.name} should fall back to the default locale when an Intl.Locale instance is not passed in an array`);
138+
assert.areEqual("de", new Ctor([locale]).resolvedOptions().locale, `${Ctor.name} did not accept an Intl.Locale instance in an array as the language tag argument properly`);
139+
}
140+
}
141+
},
123142
], { verbose: false });

test/Intl/rlexe.xml

+6
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@
118118
<tags>Intl,exclude_windows</tags>
119119
</default>
120120
</test>
121+
<test>
122+
<default>
123+
<files>Locale.js</files>
124+
<tags>Intl,exclude_windows</tags>
125+
</default>
126+
</test>
121127

122128
<!-- Slow Tests -->
123129

0 commit comments

Comments
 (0)