JSC should detect the right default locale even when it's not embedded in WebCore
[WebKit-https.git] / Source / JavaScriptCore / runtime / IntlObject.cpp
1 /*
2  * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com)
3  * Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com)
4  * Copyright (C) 2016 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
16  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
19  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
25  * THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 #include "config.h"
29 #include "IntlObject.h"
30
31 #if ENABLE(INTL)
32
33 #include "Error.h"
34 #include "FunctionPrototype.h"
35 #include "IntlCollator.h"
36 #include "IntlCollatorConstructor.h"
37 #include "IntlCollatorPrototype.h"
38 #include "IntlDateTimeFormat.h"
39 #include "IntlDateTimeFormatConstructor.h"
40 #include "IntlDateTimeFormatPrototype.h"
41 #include "IntlNumberFormat.h"
42 #include "IntlNumberFormatConstructor.h"
43 #include "IntlNumberFormatPrototype.h"
44 #include "JSCInlines.h"
45 #include "JSCJSValueInlines.h"
46 #include "Lookup.h"
47 #include "ObjectPrototype.h"
48 #include <unicode/uloc.h>
49 #include <unicode/unumsys.h>
50 #include <wtf/Assertions.h>
51 #include <wtf/NeverDestroyed.h>
52 #include <wtf/PlatformUserPreferredLanguages.h>
53
54 namespace JSC {
55
56 STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(IntlObject);
57
58 }
59
60 namespace JSC {
61
62 struct MatcherResult {
63     String locale;
64     String extension;
65     size_t extensionIndex;
66 };
67
68 const ClassInfo IntlObject::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlObject) };
69
70 IntlObject::IntlObject(VM& vm, Structure* structure)
71     : JSNonFinalObject(vm, structure)
72 {
73 }
74
75 IntlObject* IntlObject::create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
76 {
77     IntlObject* object = new (NotNull, allocateCell<IntlObject>(vm.heap)) IntlObject(vm, structure);
78     object->finishCreation(vm, globalObject);
79     return object;
80 }
81
82 void IntlObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
83 {
84     Base::finishCreation(vm);
85     ASSERT(inherits(info()));
86
87     // Set up Collator.
88     IntlCollatorPrototype* collatorPrototype = IntlCollatorPrototype::create(vm, globalObject, IntlCollatorPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
89     Structure* collatorStructure = IntlCollator::createStructure(vm, globalObject, collatorPrototype);
90     IntlCollatorConstructor* collatorConstructor = IntlCollatorConstructor::create(vm, IntlCollatorConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), collatorPrototype, collatorStructure);
91
92     collatorPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, collatorConstructor, DontEnum);
93
94     // Set up NumberFormat.
95     IntlNumberFormatPrototype* numberFormatPrototype = IntlNumberFormatPrototype::create(vm, globalObject, IntlNumberFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
96     Structure* numberFormatStructure = IntlNumberFormat::createStructure(vm, globalObject, numberFormatPrototype);
97     IntlNumberFormatConstructor* numberFormatConstructor = IntlNumberFormatConstructor::create(vm, IntlNumberFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), numberFormatPrototype, numberFormatStructure);
98
99     numberFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, numberFormatConstructor, DontEnum);
100
101     // Set up DateTimeFormat.
102     IntlDateTimeFormatPrototype* dateTimeFormatPrototype = IntlDateTimeFormatPrototype::create(vm, globalObject, IntlDateTimeFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
103     Structure* dateTimeFormatStructure = IntlDateTimeFormat::createStructure(vm, globalObject, dateTimeFormatPrototype);
104     IntlDateTimeFormatConstructor* dateTimeFormatConstructor = IntlDateTimeFormatConstructor::create(vm, IntlDateTimeFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), dateTimeFormatPrototype, dateTimeFormatStructure);
105
106     dateTimeFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, dateTimeFormatConstructor, DontEnum);
107
108     // 8.1 Properties of the Intl Object (ECMA-402 2.0)
109     putDirectWithoutTransition(vm, vm.propertyNames->Collator, collatorConstructor, DontEnum);
110     putDirectWithoutTransition(vm, vm.propertyNames->NumberFormat, numberFormatConstructor, DontEnum);
111     putDirectWithoutTransition(vm, vm.propertyNames->DateTimeFormat, dateTimeFormatConstructor, DontEnum);
112 }
113
114 Structure* IntlObject::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
115 {
116     return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
117 }
118
119 void convertICULocaleToBCP47LanguageTag(String& locale)
120 {
121     locale.replace('_', '-');
122 }
123
124 bool intlBooleanOption(ExecState& state, JSValue options, PropertyName property, bool& usesFallback)
125 {
126     // 9.2.9 GetOption (options, property, type, values, fallback)
127     // For type="boolean". values is always undefined.
128
129     // 1. Let opts be ToObject(options).
130     JSObject* opts = options.toObject(&state);
131
132     // 2. ReturnIfAbrupt(opts).
133     if (state.hadException())
134         return false;
135
136     // 3. Let value be Get(opts, property).
137     JSValue value = opts->get(&state, property);
138
139     // 4. ReturnIfAbrupt(value).
140     if (state.hadException())
141         return false;
142
143     // 5. If value is not undefined, then
144     if (!value.isUndefined()) {
145         // a. Assert: type is "boolean" or "string".
146         // Function dedicated to "boolean".
147
148         // b. If type is "boolean", then
149         // i. Let value be ToBoolean(value).
150         bool booleanValue = value.toBoolean(&state);
151
152         // e. Return value.
153         usesFallback = false;
154         return booleanValue;
155     }
156
157     // 6. Else return fallback.
158     // Because fallback can be undefined, we let the caller handle it instead.
159     usesFallback = true;
160     return false;
161 }
162
163 String intlStringOption(ExecState& state, JSValue options, PropertyName property, std::initializer_list<const char*> values, const char* notFound, const char* fallback)
164 {
165     // 9.2.9 GetOption (options, property, type, values, fallback)
166     // For type="string".
167
168     // 1. Let opts be ToObject(options).
169     JSObject* opts = options.toObject(&state);
170
171     // 2. ReturnIfAbrupt(opts).
172     if (state.hadException())
173         return { };
174
175     // 3. Let value be Get(opts, property).
176     JSValue value = opts->get(&state, property);
177
178     // 4. ReturnIfAbrupt(value).
179     if (state.hadException())
180         return { };
181
182     // 5. If value is not undefined, then
183     if (!value.isUndefined()) {
184         // a. Assert: type is "boolean" or "string".
185         // Function dedicated to "string".
186
187         // c. If type is "string", then
188         // i. Let value be ToString(value).
189         String stringValue = value.toWTFString(&state);
190
191         // ii. ReturnIfAbrupt(value).
192         if (state.hadException())
193             return { };
194
195         // d. If values is not undefined, then
196         // i. If values does not contain an element equal to value, throw a RangeError exception.
197         if (values.size() && std::find(values.begin(), values.end(), stringValue) == values.end()) {
198             state.vm().throwException(&state, createRangeError(&state, notFound));
199             return { };
200         }
201
202         // e. Return value.
203         return stringValue;
204     }
205
206     // 6. Else return fallback.
207     return fallback;
208 }
209
210 unsigned intlNumberOption(ExecState& state, JSValue options, PropertyName property, unsigned minimum, unsigned maximum, unsigned fallback)
211 {
212     // 9.2.9 GetNumberOption (options, property, minimum, maximum, fallback) (ECMA-402 2.0)
213     // 1. Let opts be ToObject(options).
214     JSObject* opts = options.toObject(&state);
215
216     // 2. ReturnIfAbrupt(opts).
217     if (state.hadException())
218         return 0;
219
220     // 3. Let value be Get(opts, property).
221     JSValue value = opts->get(&state, property);
222
223     // 4. ReturnIfAbrupt(value).
224     if (state.hadException())
225         return 0;
226
227     // 5. If value is not undefined, then
228     if (!value.isUndefined()) {
229         // a. Let value be ToNumber(value).
230         double doubleValue = value.toNumber(&state);
231         // b. ReturnIfAbrupt(value).
232         if (state.hadException())
233             return 0;
234         // 1. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
235         if (!(doubleValue >= minimum && doubleValue <= maximum)) {
236             state.vm().throwException(&state, createRangeError(&state, *property.publicName() + " is out of range"));
237             return 0;
238         }
239
240         // c. Return floor(value).
241         return static_cast<unsigned>(doubleValue);
242     }
243
244     // 6. Else return fallback.
245     return fallback;
246 }
247
248 static String privateUseLangTag(const Vector<String>& parts, size_t startIndex)
249 {
250     size_t numParts = parts.size();
251     size_t currentIndex = startIndex;
252
253     // Check for privateuse.
254     // privateuse = "x" 1*("-" (2*8alphanum))
255     StringBuilder privateuse;
256     while (currentIndex < numParts) {
257         const String& singleton = parts[currentIndex];
258         unsigned singletonLength = singleton.length();
259         bool isValid = (singletonLength == 1 && (singleton == "x" || singleton == "X"));
260         if (!isValid)
261             break;
262
263         if (currentIndex != startIndex)
264             privateuse.append('-');
265
266         ++currentIndex;
267         unsigned numExtParts = 0;
268         privateuse.append('x');
269         while (currentIndex < numParts) {
270             const String& extPart = parts[currentIndex];
271             unsigned extPartLength = extPart.length();
272
273             bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
274             if (!isValid)
275                 break;
276
277             ++currentIndex;
278             ++numExtParts;
279             privateuse.append('-');
280             privateuse.append(extPart.convertToASCIILowercase());
281         }
282
283         // Requires at least one production.
284         if (!numExtParts)
285             return String();
286     }
287
288     // Leftovers makes it invalid.
289     if (currentIndex < numParts)
290         return String();
291
292     return privateuse.toString();
293 }
294
295 static String canonicalLangTag(const Vector<String>& parts)
296 {
297     ASSERT(!parts.isEmpty());
298
299     // Follows the grammar at https://www.rfc-editor.org/rfc/bcp/bcp47.txt
300     // langtag = language ["-" script] ["-" region] *("-" variant) *("-" extension) ["-" privateuse]
301
302     size_t numParts = parts.size();
303     // Check for language.
304     // language = 2*3ALPHA ["-" extlang] / 4ALPHA / 5*8ALPHA
305     size_t currentIndex = 0;
306     const String& language = parts[currentIndex];
307     unsigned languageLength = language.length();
308     bool canHaveExtlang = languageLength >= 2 && languageLength <= 3;
309     bool isValidLanguage = languageLength >= 2 && languageLength <= 8 && language.isAllSpecialCharacters<isASCIIAlpha>();
310     if (!isValidLanguage)
311         return String();
312
313     ++currentIndex;
314     StringBuilder canonical;
315     canonical.append(language.convertToASCIILowercase());
316
317     // Check for extlang.
318     // extlang = 3ALPHA *2("-" 3ALPHA)
319     if (canHaveExtlang) {
320         for (unsigned times = 0; times < 3 && currentIndex < numParts; ++times) {
321             const String& extlang = parts[currentIndex];
322             unsigned extlangLength = extlang.length();
323             if (extlangLength == 3 && extlang.isAllSpecialCharacters<isASCIIAlpha>()) {
324                 ++currentIndex;
325                 canonical.append('-');
326                 canonical.append(extlang.convertToASCIILowercase());
327             } else
328                 break;
329         }
330     }
331
332     // Check for script.
333     // script = 4ALPHA
334     if (currentIndex < numParts) {
335         const String& script = parts[currentIndex];
336         unsigned scriptLength = script.length();
337         if (scriptLength == 4 && script.isAllSpecialCharacters<isASCIIAlpha>()) {
338             ++currentIndex;
339             canonical.append('-');
340             canonical.append(toASCIIUpper(script[0]));
341             canonical.append(script.substring(1, 3).convertToASCIILowercase());
342         }
343     }
344
345     // Check for region.
346     // region = 2ALPHA / 3DIGIT
347     if (currentIndex < numParts) {
348         const String& region = parts[currentIndex];
349         unsigned regionLength = region.length();
350         bool isValidRegion = (
351             (regionLength == 2 && region.isAllSpecialCharacters<isASCIIAlpha>())
352             || (regionLength == 3 && region.isAllSpecialCharacters<isASCIIDigit>())
353         );
354         if (isValidRegion) {
355             ++currentIndex;
356             canonical.append('-');
357             canonical.append(region.convertToASCIIUppercase());
358         }
359     }
360
361     // Check for variant.
362     // variant = 5*8alphanum / (DIGIT 3alphanum)
363     HashSet<String> subtags;
364     while (currentIndex < numParts) {
365         const String& variant = parts[currentIndex];
366         unsigned variantLength = variant.length();
367         bool isValidVariant = (
368             (variantLength >= 5 && variantLength <= 8 && variant.isAllSpecialCharacters<isASCIIAlphanumeric>())
369             || (variantLength == 4 && isASCIIDigit(variant[0]) && variant.substring(1, 3).isAllSpecialCharacters<isASCIIAlphanumeric>())
370         );
371         if (!isValidVariant)
372             break;
373
374         // Cannot include duplicate subtags (case insensitive).
375         String lowerVariant = variant.convertToASCIILowercase();
376         if (!subtags.add(lowerVariant).isNewEntry)
377             return String();
378
379         ++currentIndex;
380
381         // Reordering variant subtags is not required in the spec.
382         canonical.append('-');
383         canonical.append(lowerVariant);
384     }
385
386     // Check for extension.
387     // extension = singleton 1*("-" (2*8alphanum))
388     // singleton = alphanum except x or X
389     subtags.clear();
390     Vector<String> extensions;
391     while (currentIndex < numParts) {
392         const String& possibleSingleton = parts[currentIndex];
393         unsigned singletonLength = possibleSingleton.length();
394         bool isValidSingleton = (singletonLength == 1 && possibleSingleton != "x" && possibleSingleton != "X" && isASCIIAlphanumeric(possibleSingleton[0]));
395         if (!isValidSingleton)
396             break;
397
398         // Cannot include duplicate singleton (case insensitive).
399         String singleton = possibleSingleton.convertToASCIILowercase();
400         if (!subtags.add(singleton).isNewEntry)
401             return String();
402
403         ++currentIndex;
404         int numExtParts = 0;
405         StringBuilder extension;
406         extension.append(singleton);
407         while (currentIndex < numParts) {
408             const String& extPart = parts[currentIndex];
409             unsigned extPartLength = extPart.length();
410
411             bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
412             if (!isValid)
413                 break;
414
415             ++currentIndex;
416             ++numExtParts;
417             extension.append('-');
418             extension.append(extPart.convertToASCIILowercase());
419         }
420
421         // Requires at least one production.
422         if (!numExtParts)
423             return String();
424
425         extensions.append(extension.toString());
426     }
427
428     // Add extensions to canonical sorted by singleton.
429     std::sort(
430         extensions.begin(),
431         extensions.end(),
432         [] (const String& a, const String& b) -> bool {
433             return a[0] < b[0];
434         }
435     );
436     size_t numExtenstions = extensions.size();
437     for (size_t i = 0; i < numExtenstions; ++i) {
438         canonical.append('-');
439         canonical.append(extensions[i]);
440     }
441
442     // Check for privateuse.
443     if (currentIndex < numParts) {
444         String privateuse = privateUseLangTag(parts, currentIndex);
445         if (privateuse.isNull())
446             return String();
447         canonical.append('-');
448         canonical.append(privateuse);
449     }
450
451     // FIXME: Replace subtags with their preferred values.
452
453     return canonical.toString();
454 }
455
456 static String grandfatheredLangTag(const String& locale)
457 {
458     // grandfathered = irregular / regular
459     // FIXME: convert to a compile time hash table if this is causing performance issues.
460     HashMap<String, String> tagMap = {
461         // Irregular.
462         { ASCIILiteral("en-gb-oed"), ASCIILiteral("en-GB-oed") },
463         { ASCIILiteral("i-ami"), ASCIILiteral("ami") },
464         { ASCIILiteral("i-bnn"), ASCIILiteral("bnn") },
465         { ASCIILiteral("i-default"), ASCIILiteral("i-default") },
466         { ASCIILiteral("i-enochian"), ASCIILiteral("i-enochian") },
467         { ASCIILiteral("i-hak"), ASCIILiteral("hak") },
468         { ASCIILiteral("i-klingon"), ASCIILiteral("tlh") },
469         { ASCIILiteral("i-lux"), ASCIILiteral("lb") },
470         { ASCIILiteral("i-mingo"), ASCIILiteral("i-mingo") },
471         { ASCIILiteral("i-navajo"), ASCIILiteral("nv") },
472         { ASCIILiteral("i-pwn"), ASCIILiteral("pwn") },
473         { ASCIILiteral("i-tao"), ASCIILiteral("tao") },
474         { ASCIILiteral("i-tay"), ASCIILiteral("tay") },
475         { ASCIILiteral("i-tsu"), ASCIILiteral("tsu") },
476         { ASCIILiteral("sgn-be-fr"), ASCIILiteral("sfb") },
477         { ASCIILiteral("sgn-be-nl"), ASCIILiteral("vgt") },
478         { ASCIILiteral("sgn-ch-de"), ASCIILiteral("sgg") },
479         // Regular.
480         { ASCIILiteral("art-lojban"), ASCIILiteral("jbo") },
481         { ASCIILiteral("cel-gaulish"), ASCIILiteral("cel-gaulish") },
482         { ASCIILiteral("no-bok"), ASCIILiteral("nb") },
483         { ASCIILiteral("no-nyn"), ASCIILiteral("nn") },
484         { ASCIILiteral("zh-guoyu"), ASCIILiteral("cmn") },
485         { ASCIILiteral("zh-hakka"), ASCIILiteral("hak") },
486         { ASCIILiteral("zh-min"), ASCIILiteral("zh-min") },
487         { ASCIILiteral("zh-min-nan"), ASCIILiteral("nan") },
488         { ASCIILiteral("zh-xiang"), ASCIILiteral("hsn") }
489     };
490
491     return tagMap.get(locale.convertToASCIILowercase());
492 }
493
494 static String canonicalizeLanguageTag(const String& locale)
495 {
496     // 6.2.2 IsStructurallyValidLanguageTag (locale)
497     // 6.2.3 CanonicalizeLanguageTag (locale)
498     // These are done one after another in CanonicalizeLocaleList, so they are combined here to reduce duplication.
499     // https://www.rfc-editor.org/rfc/bcp/bcp47.txt
500
501     // Language-Tag = langtag / privateuse / grandfathered
502     String grandfather = grandfatheredLangTag(locale);
503     if (!grandfather.isNull())
504         return grandfather;
505
506     // FIXME: Replace redundant tags [RFC4647].
507
508     Vector<String> parts;
509     locale.split('-', true, parts);
510     if (!parts.isEmpty()) {
511         String langtag = canonicalLangTag(parts);
512         if (!langtag.isNull())
513             return langtag;
514
515         String privateuse = privateUseLangTag(parts, 0);
516         if (!privateuse.isNull())
517             return privateuse;
518     }
519
520     return String();
521 }
522
523 Vector<String> canonicalizeLocaleList(ExecState& state, JSValue locales)
524 {
525     // 9.2.1 CanonicalizeLocaleList (locales)
526     VM& vm = state.vm();
527     JSGlobalObject* globalObject = state.callee()->globalObject();
528     Vector<String> seen;
529
530     // 1. If locales is undefined, then a. Return a new empty List.
531     if (locales.isUndefined())
532         return seen;
533
534     // 2. Let seen be an empty List.
535     // Done before to also return in step 1, if needed.
536
537     // 3. If Type(locales) is String, then
538     JSObject* localesObject;
539     if (locales.isString()) {
540         //  a. Let aLocales be CreateArrayFromList(«locales»).
541         JSArray* localesArray = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 1);
542         localesArray->initializeIndex(vm, 0, locales);
543         // 4. Let O be ToObject(aLocales).
544         localesObject = localesArray;
545     } else {
546         // 4. Let O be ToObject(aLocales).
547         localesObject = locales.toObject(&state);
548     }
549
550     // 5. ReturnIfAbrupt(O).
551     if (state.hadException())
552         return Vector<String>();
553
554     // 6. Let len be ToLength(Get(O, "length")).
555     JSValue lengthProperty = localesObject->get(&state, vm.propertyNames->length);
556     if (state.hadException())
557         return Vector<String>();
558
559     double length = lengthProperty.toLength(&state);
560     if (state.hadException())
561         return Vector<String>();
562
563     // Keep track of locales that have been added to the list.
564     HashSet<String> seenSet;
565
566     // 7. Let k be 0.
567     // 8. Repeat, while k < len
568     for (double k = 0; k < length; ++k) {
569         // a. Let Pk be ToString(k).
570         // Not needed because hasProperty and get take an int for numeric key.
571
572         // b. Let kPresent be HasProperty(O, Pk).
573         bool kPresent = localesObject->hasProperty(&state, k);
574
575         // c. ReturnIfAbrupt(kPresent).
576         if (state.hadException())
577             return Vector<String>();
578
579         // d. If kPresent is true, then
580         if (kPresent) {
581             // i. Let kValue be Get(O, Pk).
582             JSValue kValue = localesObject->get(&state, k);
583
584             // ii. ReturnIfAbrupt(kValue).
585             if (state.hadException())
586                 return Vector<String>();
587
588             // iii. If Type(kValue) is not String or Object, throw a TypeError exception.
589             if (!kValue.isString() && !kValue.isObject()) {
590                 throwTypeError(&state, ASCIILiteral("locale value must be a string or object"));
591                 return Vector<String>();
592             }
593
594             // iv. Let tag be ToString(kValue).
595             JSString* tag = kValue.toString(&state);
596
597             // v. ReturnIfAbrupt(tag).
598             if (state.hadException())
599                 return Vector<String>();
600
601             // vi. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
602             // vii. Let canonicalizedTag be CanonicalizeLanguageTag(tag).
603             String canonicalizedTag = canonicalizeLanguageTag(tag->value(&state));
604             if (canonicalizedTag.isNull()) {
605                 state.vm().throwException(&state, createRangeError(&state, String::format("invalid language tag: %s", tag->value(&state).utf8().data())));
606                 return Vector<String>();
607             }
608
609             // viii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
610             if (seenSet.add(canonicalizedTag).isNewEntry)
611                 seen.append(canonicalizedTag);
612         }
613         // e. Increase k by 1.
614     }
615
616     return seen;
617 }
618
619 String bestAvailableLocale(const HashSet<String>& availableLocales, const String& locale)
620 {
621     // 9.2.2 BestAvailableLocale (availableLocales, locale)
622     // 1. Let candidate be locale.
623     String candidate = locale;
624
625     // 2. Repeat
626     while (!candidate.isEmpty()) {
627         // a. If availableLocales contains an element equal to candidate, then return candidate.
628         if (availableLocales.contains(candidate))
629             return candidate;
630
631         // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
632         size_t pos = candidate.reverseFind('-');
633         if (pos == notFound)
634             return String();
635
636         // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, then decrease pos by 2.
637         if (pos >= 2 && candidate[pos - 2] == '-')
638             pos -= 2;
639
640         // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
641         candidate = candidate.substring(0, pos);
642     }
643
644     return String();
645 }
646
647 String defaultLocale(ExecState& state)
648 {
649     // 6.2.4 DefaultLocale ()
650     
651     // WebCore's global objects will have their own ideas of how to determine the language. It may
652     // be determined by WebCore-specific logic like some WK settings. Usually this will return the
653     // same thing as platformUserPreferredLanguages()[0].
654     if (auto defaultLanguage = state.callee()->globalObject()->globalObjectMethodTable()->defaultLanguage) {
655         String locale = defaultLanguage();
656         if (!locale.isEmpty())
657             return canonicalizeLanguageTag(locale);
658     }
659     
660     // If WebCore isn't around to tell us how to get the language then fall back to our own way of
661     // doing it, which mostly follows what WebCore would have done.
662     Vector<String> languages = platformUserPreferredLanguages();
663     if (!languages.isEmpty() && !languages[0].isEmpty())
664         return canonicalizeLanguageTag(languages[0]);
665     
666     // If all else fails, ask ICU. It will probably say something bogus like en_us even if the user
667     // has configured some other language, but being wrong is better than crashing.
668     String locale = uloc_getDefault();
669     convertICULocaleToBCP47LanguageTag(locale);
670     return locale;
671 }
672
673 String removeUnicodeLocaleExtension(const String& locale)
674 {
675     Vector<String> parts;
676     locale.split('-', parts);
677     StringBuilder builder;
678     size_t partsSize = parts.size();
679     if (partsSize > 0)
680         builder.append(parts[0]);
681     for (size_t p = 1; p < partsSize; ++p) {
682         if (parts[p] == "u") {
683             // Skip the u- and anything that follows until another singleton.
684             // While the next part is part of the unicode extension, skip it.
685             while (p + 1 < partsSize && parts[p + 1].length() > 1)
686                 ++p;
687         } else {
688             builder.append('-');
689             builder.append(parts[p]);
690         }
691     }
692     return builder.toString();
693 }
694
695 static MatcherResult lookupMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
696 {
697     // 9.2.3 LookupMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
698     String locale;
699     String noExtensionsLocale;
700     String availableLocale;
701     for (size_t i = 0; i < requestedLocales.size() && availableLocale.isNull(); ++i) {
702         locale = requestedLocales[i];
703         noExtensionsLocale = removeUnicodeLocaleExtension(locale);
704         availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
705     }
706
707     MatcherResult result;
708     if (!availableLocale.isNull()) {
709         result.locale = availableLocale;
710         if (locale != noExtensionsLocale) {
711             // i. Let extension be the String value consisting of the first substring of locale that is a Unicode locale extension sequence.
712             // ii. Let extensionIndex be the character position of the initial "-" extension sequence within locale.
713             size_t extensionIndex = locale.find("-u-");
714             RELEASE_ASSERT(extensionIndex != notFound);
715
716             size_t extensionLength = locale.length() - extensionIndex;
717             size_t end = extensionIndex + 3;
718             while (end < locale.length()) {
719                 end = locale.find('-', end);
720                 if (end == notFound)
721                     break;
722                 if (end + 2 < locale.length() && locale[end + 2] == '-') {
723                     extensionLength = end - extensionIndex;
724                     break;
725                 }
726                 end++;
727             }
728             result.extension = locale.substring(extensionIndex, extensionLength);
729             result.extensionIndex = extensionIndex;
730         }
731     } else
732         result.locale = defaultLocale(state);
733     return result;
734 }
735
736 static MatcherResult bestFitMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
737 {
738     // 9.2.4 BestFitMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
739     // FIXME: Implement something better than lookup.
740     return lookupMatcher(state, availableLocales, requestedLocales);
741 }
742
743 HashMap<String, String> resolveLocale(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, const HashMap<String, String>& options, const char* const relevantExtensionKeys[], size_t relevantExtensionKeyCount, Vector<String> (*localeData)(const String&, size_t))
744 {
745     // 9.2.5 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) (ECMA-402 2.0)
746     // 1. Let matcher be the value of options.[[localeMatcher]].
747     const String& matcher = options.get(ASCIILiteral("localeMatcher"));
748
749     // 2. If matcher is "lookup", then
750     MatcherResult (*matcherOperation)(ExecState&, const HashSet<String>&, const Vector<String>&);
751     if (matcher == "lookup") {
752         // a. Let MatcherOperation be the abstract operation LookupMatcher.
753         matcherOperation = lookupMatcher;
754     } else { // 3. Else
755         // a. Let MatcherOperation be the abstract operation BestFitMatcher.
756         matcherOperation = bestFitMatcher;
757     }
758
759     // 4. Let r be MatcherOperation(availableLocales, requestedLocales).
760     MatcherResult matcherResult = matcherOperation(state, availableLocales, requestedLocales);
761
762     // 5. Let foundLocale be the value of r.[[locale]].
763     String foundLocale = matcherResult.locale;
764
765     // 6. If r has an [[extension]] field, then
766     Vector<String> extensionSubtags;
767     if (!matcherResult.extension.isNull()) {
768         // a. Let extension be the value of r.[[extension]].
769         // b. Let extensionIndex be the value of r.[[extensionIndex]].
770         // c. Let extensionSubtags be Call(%StringProto_split%, extension, «"-"») .
771         // d. Let extensionSubtagsLength be Get(CreateArrayFromList(extensionSubtags), "length").
772         matcherResult.extension.split('-', extensionSubtags);
773     }
774
775     // 7. Let result be a new Record.
776     HashMap<String, String> result;
777
778     // 8. Set result.[[dataLocale]] to foundLocale.
779     result.add(ASCIILiteral("dataLocale"), foundLocale);
780
781     // 9. Let supportedExtension be "-u".
782     String supportedExtension = ASCIILiteral("-u");
783
784     // 10. Let k be 0.
785     // 11. Let rExtensionKeys be ToObject(CreateArrayFromList(relevantExtensionKeys)).
786     // 12. ReturnIfAbrupt(rExtensionKeys).
787     // 13. Let len be ToLength(Get(rExtensionKeys, "length")).
788     // 14. Repeat while k < len
789     for (size_t keyIndex = 0; keyIndex < relevantExtensionKeyCount; ++keyIndex) {
790         // a. Let key be Get(rExtensionKeys, ToString(k)).
791         // b. ReturnIfAbrupt(key).
792         const char* key = relevantExtensionKeys[keyIndex];
793
794         // c. Let foundLocaleData be Get(localeData, foundLocale).
795         // d. ReturnIfAbrupt(foundLocaleData).
796         // e. Let keyLocaleData be ToObject(Get(foundLocaleData, key)).
797         // f. ReturnIfAbrupt(keyLocaleData).
798         Vector<String> keyLocaleData = localeData(foundLocale, keyIndex);
799
800         // g. Let value be ToString(Get(keyLocaleData, "0")).
801         // h. ReturnIfAbrupt(value).
802         ASSERT(!keyLocaleData.isEmpty());
803         String value = keyLocaleData[0];
804
805         // i. Let supportedExtensionAddition be "".
806         String supportedExtensionAddition;
807
808         // j. If extensionSubtags is not undefined, then
809         if (!extensionSubtags.isEmpty()) {
810             // i. Let keyPos be Call(%ArrayProto_indexOf%, extensionSubtags, «key») .
811             size_t keyPos = extensionSubtags.find(key);
812             // ii. If keyPos != -1, then
813             if (keyPos != notFound) {
814                 // FIXME: https://github.com/tc39/ecma402/issues/59
815                 // 1. If keyPos + 1 < extensionSubtagsLength and the length of the result of Get(extensionSubtags, ToString(keyPos +1)) is greater than 2, then
816                 if (keyPos + 1 < extensionSubtags.size() && extensionSubtags[keyPos + 1].length() > 2) {
817                     const String& requestedValue = extensionSubtags[keyPos + 1];
818                     if (keyLocaleData.contains(requestedValue)) {
819                         value = requestedValue;
820                         supportedExtensionAddition = makeString('-', key, '-', value);
821                     }
822                 } else if (keyLocaleData.contains(static_cast<String>(ASCIILiteral("true")))) {
823                     // 2. Else, if the result of Call(%StringProto_includes%, keyLocaleData, «"true"») is true, then
824                     value = ASCIILiteral("true");
825                 }
826             }
827         }
828
829         // k. If options has a field [[<key>]], then
830         HashMap<String, String>::const_iterator iterator = options.find(key);
831         if (iterator != options.end()) {
832             // i. Let optionsValue be the value of ToString(options.[[<key>]]).
833             // ii. ReturnIfAbrupt(optionsValue).
834             const String& optionsValue = iterator->value;
835             // iii. If the result of Call(%StringProto_includes%, keyLocaleData, «optionsValue») is true, then
836             if (!optionsValue.isNull() && keyLocaleData.contains(optionsValue)) {
837                 // 1. If optionsValue is not equal to value, then
838                 if (optionsValue != value) {
839                     value = optionsValue;
840                     supportedExtensionAddition = String();
841                 }
842             }
843         }
844
845         // l. Set result.[[<key>]] to value.
846         result.add(key, value);
847
848         // m. Append supportedExtensionAddition to supportedExtension.
849         supportedExtension.append(supportedExtensionAddition);
850
851         // n. Increase k by 1.
852     }
853
854     // 15. If the number of elements in supportedExtension is greater than 2, then
855     if (supportedExtension.length() > 2) {
856         // a. Let preExtension be the substring of foundLocale from position 0, inclusive, to position extensionIndex, exclusive.
857         // b. Let postExtension be the substring of foundLocale from position extensionIndex to the end of the string.
858         // c. Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension.
859         String preExtension = foundLocale.substring(0, matcherResult.extensionIndex);
860         String postExtension = foundLocale.substring(matcherResult.extensionIndex);
861         foundLocale = preExtension + supportedExtension + postExtension;
862     }
863
864     // 16. Set result.[[locale]] to foundLocale.
865     result.add(ASCIILiteral("locale"), foundLocale);
866
867     // 17. Return result.
868     return result;
869 }
870
871 static JSArray* lookupSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
872 {
873     // 9.2.6 LookupSupportedLocales (availableLocales, requestedLocales)
874
875     // 1. Let rLocales be CreateArrayFromList(requestedLocales).
876     // Already an array.
877
878     // 2. Let len be ToLength(Get(rLocales, "length")).
879     size_t len = requestedLocales.size();
880
881     // 3. Let subset be an empty List.
882     VM& vm = state.vm();
883     JSGlobalObject* globalObject = state.callee()->globalObject();
884     JSArray* subset = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithUndecided), 0);
885     if (!subset) {
886         throwOutOfMemoryError(&state);
887         return nullptr;
888     }
889
890     // 4. Let k be 0.
891     // 5. Repeat while k < len
892     for (size_t k = 0; k < len; ++k) {
893         // a. Let Pk be ToString(k).
894         // b. Let locale be Get(rLocales, Pk).
895         // c. ReturnIfAbrupt(locale).
896         const String& locale = requestedLocales[k];
897
898         // d. Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
899         String noExtensionsLocale = removeUnicodeLocaleExtension(locale);
900
901         // e. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
902         String availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
903
904         // f. If availableLocale is not undefined, then append locale to the end of subset.
905         if (!availableLocale.isNull())
906             subset->push(&state, jsString(&state, locale));
907
908         // g. Increment k by 1.
909     }
910
911     // 6. Return subset.
912     return subset;
913 }
914
915 static JSArray* bestFitSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
916 {
917     // 9.2.7 BestFitSupportedLocales (availableLocales, requestedLocales)
918     // FIXME: Implement something better than lookup.
919     return lookupSupportedLocales(state, availableLocales, requestedLocales);
920 }
921
922 JSValue supportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, JSValue options)
923 {
924     // 9.2.8 SupportedLocales (availableLocales, requestedLocales, options)
925     VM& vm = state.vm();
926     String matcher;
927
928     // 1. If options is not undefined, then
929     if (!options.isUndefined()) {
930         // a. Let matcher be GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
931         matcher = intlStringOption(state, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
932         // b. ReturnIfAbrupt(matcher).
933         if (state.hadException())
934             return jsUndefined();
935     } else {
936         // 2. Else, let matcher be "best fit".
937         matcher = ASCIILiteral("best fit");
938     }
939
940     JSArray* supportedLocales;
941     // 3. If matcher is "best fit",
942     if (matcher == "best fit") {
943         // a. Let MatcherOperation be the abstract operation BestFitSupportedLocales.
944         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
945         supportedLocales = bestFitSupportedLocales(state, availableLocales, requestedLocales);
946     } else {
947         // 4. Else
948         // a. Let MatcherOperation be the abstract operation LookupSupportedLocales.
949         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
950         supportedLocales = lookupSupportedLocales(state, availableLocales, requestedLocales);
951     }
952
953     if (state.hadException())
954         return jsUndefined();
955
956     // 6. Let subset be CreateArrayFromList(supportedLocales).
957     // Already an array.
958
959     // 7. Let keys be subset.[[OwnPropertyKeys]]().
960     PropertyNameArray keys(&state, PropertyNameMode::Strings);
961     supportedLocales->getOwnPropertyNames(supportedLocales, &state, keys, EnumerationMode());
962     if (state.hadException())
963         return jsUndefined();
964
965     PropertyDescriptor desc;
966     desc.setConfigurable(false);
967     desc.setWritable(false);
968
969     // 8. Repeat for each element P of keys in List order,
970     size_t len = keys.size();
971     for (size_t i = 0; i < len; ++i) {
972         // a. Let desc be PropertyDescriptor { [[Configurable]]: false, [[Writable]]: false }.
973         // Created above for reuse.
974
975         // b. Let status be DefinePropertyOrThrow(subset, P, desc).
976         supportedLocales->defineOwnProperty(supportedLocales, &state, keys[i], desc, true);
977
978         // c. Assert: status is not abrupt completion.
979         if (state.hadException())
980             return jsUndefined();
981     }
982
983     // 9. Return subset.
984     return supportedLocales;
985 }
986
987 Vector<String> numberingSystemsForLocale(const String& locale)
988 {
989     static NeverDestroyed<Vector<String>> cachedNumberingSystems;
990     Vector<String>& availableNumberingSystems = cachedNumberingSystems.get();
991     if (availableNumberingSystems.isEmpty()) {
992         UErrorCode status = U_ZERO_ERROR;
993         UEnumeration* numberingSystemNames = unumsys_openAvailableNames(&status);
994         ASSERT(U_SUCCESS(status));
995
996         int32_t resultLength;
997         // Numbering system names are always ASCII, so use char[].
998         while (const char* result = uenum_next(numberingSystemNames, &resultLength, &status)) {
999             ASSERT(U_SUCCESS(status));
1000             availableNumberingSystems.append(String(result, resultLength));
1001         }
1002         uenum_close(numberingSystemNames);
1003     }
1004
1005     UErrorCode status = U_ZERO_ERROR;
1006     UNumberingSystem* defaultSystem = unumsys_open(locale.utf8().data(), &status);
1007     ASSERT(U_SUCCESS(status));
1008     String defaultSystemName(unumsys_getName(defaultSystem));
1009     unumsys_close(defaultSystem);
1010
1011     Vector<String> numberingSystems({ defaultSystemName });
1012     numberingSystems.appendVector(availableNumberingSystems);
1013     return numberingSystems;
1014 }
1015
1016 } // namespace JSC
1017
1018 #endif // ENABLE(INTL)