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.
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
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.
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.
29 #include "IntlObject.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"
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>
56 STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(IntlObject);
62 struct MatcherResult {
65 size_t extensionIndex;
68 const ClassInfo IntlObject::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlObject) };
70 IntlObject::IntlObject(VM& vm, Structure* structure)
71 : JSNonFinalObject(vm, structure)
75 IntlObject* IntlObject::create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
77 IntlObject* object = new (NotNull, allocateCell<IntlObject>(vm.heap)) IntlObject(vm, structure);
78 object->finishCreation(vm, globalObject);
82 void IntlObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
84 Base::finishCreation(vm);
85 ASSERT(inherits(info()));
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);
92 collatorPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, collatorConstructor, DontEnum);
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);
99 numberFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, numberFormatConstructor, DontEnum);
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);
106 dateTimeFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, dateTimeFormatConstructor, DontEnum);
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);
114 Structure* IntlObject::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
116 return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
119 void convertICULocaleToBCP47LanguageTag(String& locale)
121 locale.replace('_', '-');
124 bool intlBooleanOption(ExecState& state, JSValue options, PropertyName property, bool& usesFallback)
126 // 9.2.9 GetOption (options, property, type, values, fallback)
127 // For type="boolean". values is always undefined.
129 // 1. Let opts be ToObject(options).
130 JSObject* opts = options.toObject(&state);
132 // 2. ReturnIfAbrupt(opts).
133 if (state.hadException())
136 // 3. Let value be Get(opts, property).
137 JSValue value = opts->get(&state, property);
139 // 4. ReturnIfAbrupt(value).
140 if (state.hadException())
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".
148 // b. If type is "boolean", then
149 // i. Let value be ToBoolean(value).
150 bool booleanValue = value.toBoolean(&state);
153 usesFallback = false;
157 // 6. Else return fallback.
158 // Because fallback can be undefined, we let the caller handle it instead.
163 String intlStringOption(ExecState& state, JSValue options, PropertyName property, std::initializer_list<const char*> values, const char* notFound, const char* fallback)
165 // 9.2.9 GetOption (options, property, type, values, fallback)
166 // For type="string".
168 // 1. Let opts be ToObject(options).
169 JSObject* opts = options.toObject(&state);
171 // 2. ReturnIfAbrupt(opts).
172 if (state.hadException())
175 // 3. Let value be Get(opts, property).
176 JSValue value = opts->get(&state, property);
178 // 4. ReturnIfAbrupt(value).
179 if (state.hadException())
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".
187 // c. If type is "string", then
188 // i. Let value be ToString(value).
189 String stringValue = value.toWTFString(&state);
191 // ii. ReturnIfAbrupt(value).
192 if (state.hadException())
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));
206 // 6. Else return fallback.
210 unsigned intlNumberOption(ExecState& state, JSValue options, PropertyName property, unsigned minimum, unsigned maximum, unsigned fallback)
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);
216 // 2. ReturnIfAbrupt(opts).
217 if (state.hadException())
220 // 3. Let value be Get(opts, property).
221 JSValue value = opts->get(&state, property);
223 // 4. ReturnIfAbrupt(value).
224 if (state.hadException())
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())
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"));
240 // c. Return floor(value).
241 return static_cast<unsigned>(doubleValue);
244 // 6. Else return fallback.
248 static String privateUseLangTag(const Vector<String>& parts, size_t startIndex)
250 size_t numParts = parts.size();
251 size_t currentIndex = startIndex;
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"));
263 if (currentIndex != startIndex)
264 privateuse.append('-');
267 unsigned numExtParts = 0;
268 privateuse.append('x');
269 while (currentIndex < numParts) {
270 const String& extPart = parts[currentIndex];
271 unsigned extPartLength = extPart.length();
273 bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
279 privateuse.append('-');
280 privateuse.append(extPart.convertToASCIILowercase());
283 // Requires at least one production.
288 // Leftovers makes it invalid.
289 if (currentIndex < numParts)
292 return privateuse.toString();
295 static String canonicalLangTag(const Vector<String>& parts)
297 ASSERT(!parts.isEmpty());
299 // Follows the grammar at https://www.rfc-editor.org/rfc/bcp/bcp47.txt
300 // langtag = language ["-" script] ["-" region] *("-" variant) *("-" extension) ["-" privateuse]
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)
314 StringBuilder canonical;
315 canonical.append(language.convertToASCIILowercase());
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>()) {
325 canonical.append('-');
326 canonical.append(extlang.convertToASCIILowercase());
334 if (currentIndex < numParts) {
335 const String& script = parts[currentIndex];
336 unsigned scriptLength = script.length();
337 if (scriptLength == 4 && script.isAllSpecialCharacters<isASCIIAlpha>()) {
339 canonical.append('-');
340 canonical.append(toASCIIUpper(script[0]));
341 canonical.append(script.substring(1, 3).convertToASCIILowercase());
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>())
356 canonical.append('-');
357 canonical.append(region.convertToASCIIUppercase());
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>())
374 // Cannot include duplicate subtags (case insensitive).
375 String lowerVariant = variant.convertToASCIILowercase();
376 if (!subtags.add(lowerVariant).isNewEntry)
381 // Reordering variant subtags is not required in the spec.
382 canonical.append('-');
383 canonical.append(lowerVariant);
386 // Check for extension.
387 // extension = singleton 1*("-" (2*8alphanum))
388 // singleton = alphanum except x or X
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)
398 // Cannot include duplicate singleton (case insensitive).
399 String singleton = possibleSingleton.convertToASCIILowercase();
400 if (!subtags.add(singleton).isNewEntry)
405 StringBuilder extension;
406 extension.append(singleton);
407 while (currentIndex < numParts) {
408 const String& extPart = parts[currentIndex];
409 unsigned extPartLength = extPart.length();
411 bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
417 extension.append('-');
418 extension.append(extPart.convertToASCIILowercase());
421 // Requires at least one production.
425 extensions.append(extension.toString());
428 // Add extensions to canonical sorted by singleton.
432 [] (const String& a, const String& b) -> bool {
436 size_t numExtenstions = extensions.size();
437 for (size_t i = 0; i < numExtenstions; ++i) {
438 canonical.append('-');
439 canonical.append(extensions[i]);
442 // Check for privateuse.
443 if (currentIndex < numParts) {
444 String privateuse = privateUseLangTag(parts, currentIndex);
445 if (privateuse.isNull())
447 canonical.append('-');
448 canonical.append(privateuse);
451 // FIXME: Replace subtags with their preferred values.
453 return canonical.toString();
456 static String grandfatheredLangTag(const String& locale)
458 // grandfathered = irregular / regular
459 // FIXME: convert to a compile time hash table if this is causing performance issues.
460 HashMap<String, String> tagMap = {
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") },
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") }
491 return tagMap.get(locale.convertToASCIILowercase());
494 static String canonicalizeLanguageTag(const String& locale)
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
501 // Language-Tag = langtag / privateuse / grandfathered
502 String grandfather = grandfatheredLangTag(locale);
503 if (!grandfather.isNull())
506 // FIXME: Replace redundant tags [RFC4647].
508 Vector<String> parts;
509 locale.split('-', true, parts);
510 if (!parts.isEmpty()) {
511 String langtag = canonicalLangTag(parts);
512 if (!langtag.isNull())
515 String privateuse = privateUseLangTag(parts, 0);
516 if (!privateuse.isNull())
523 Vector<String> canonicalizeLocaleList(ExecState& state, JSValue locales)
525 // 9.2.1 CanonicalizeLocaleList (locales)
527 JSGlobalObject* globalObject = state.callee()->globalObject();
530 // 1. If locales is undefined, then a. Return a new empty List.
531 if (locales.isUndefined())
534 // 2. Let seen be an empty List.
535 // Done before to also return in step 1, if needed.
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;
546 // 4. Let O be ToObject(aLocales).
547 localesObject = locales.toObject(&state);
550 // 5. ReturnIfAbrupt(O).
551 if (state.hadException())
552 return Vector<String>();
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>();
559 double length = lengthProperty.toLength(&state);
560 if (state.hadException())
561 return Vector<String>();
563 // Keep track of locales that have been added to the list.
564 HashSet<String> seenSet;
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.
572 // b. Let kPresent be HasProperty(O, Pk).
573 bool kPresent = localesObject->hasProperty(&state, k);
575 // c. ReturnIfAbrupt(kPresent).
576 if (state.hadException())
577 return Vector<String>();
579 // d. If kPresent is true, then
581 // i. Let kValue be Get(O, Pk).
582 JSValue kValue = localesObject->get(&state, k);
584 // ii. ReturnIfAbrupt(kValue).
585 if (state.hadException())
586 return Vector<String>();
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>();
594 // iv. Let tag be ToString(kValue).
595 JSString* tag = kValue.toString(&state);
597 // v. ReturnIfAbrupt(tag).
598 if (state.hadException())
599 return Vector<String>();
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>();
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);
613 // e. Increase k by 1.
619 String bestAvailableLocale(const HashSet<String>& availableLocales, const String& locale)
621 // 9.2.2 BestAvailableLocale (availableLocales, locale)
622 // 1. Let candidate be locale.
623 String candidate = locale;
626 while (!candidate.isEmpty()) {
627 // a. If availableLocales contains an element equal to candidate, then return candidate.
628 if (availableLocales.contains(candidate))
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('-');
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] == '-')
640 // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
641 candidate = candidate.substring(0, pos);
647 String defaultLocale(ExecState& state)
649 // 6.2.4 DefaultLocale ()
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);
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]);
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);
673 String removeUnicodeLocaleExtension(const String& locale)
675 Vector<String> parts;
676 locale.split('-', parts);
677 StringBuilder builder;
678 size_t partsSize = parts.size();
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)
689 builder.append(parts[p]);
692 return builder.toString();
695 static MatcherResult lookupMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
697 // 9.2.3 LookupMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
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);
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);
716 size_t extensionLength = locale.length() - extensionIndex;
717 size_t end = extensionIndex + 3;
718 while (end < locale.length()) {
719 end = locale.find('-', end);
722 if (end + 2 < locale.length() && locale[end + 2] == '-') {
723 extensionLength = end - extensionIndex;
728 result.extension = locale.substring(extensionIndex, extensionLength);
729 result.extensionIndex = extensionIndex;
732 result.locale = defaultLocale(state);
736 static MatcherResult bestFitMatcher(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
738 // 9.2.4 BestFitMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
739 // FIXME: Implement something better than lookup.
740 return lookupMatcher(state, availableLocales, requestedLocales);
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))
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"));
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;
755 // a. Let MatcherOperation be the abstract operation BestFitMatcher.
756 matcherOperation = bestFitMatcher;
759 // 4. Let r be MatcherOperation(availableLocales, requestedLocales).
760 MatcherResult matcherResult = matcherOperation(state, availableLocales, requestedLocales);
762 // 5. Let foundLocale be the value of r.[[locale]].
763 String foundLocale = matcherResult.locale;
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);
775 // 7. Let result be a new Record.
776 HashMap<String, String> result;
778 // 8. Set result.[[dataLocale]] to foundLocale.
779 result.add(ASCIILiteral("dataLocale"), foundLocale);
781 // 9. Let supportedExtension be "-u".
782 String supportedExtension = ASCIILiteral("-u");
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];
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);
800 // g. Let value be ToString(Get(keyLocaleData, "0")).
801 // h. ReturnIfAbrupt(value).
802 ASSERT(!keyLocaleData.isEmpty());
803 String value = keyLocaleData[0];
805 // i. Let supportedExtensionAddition be "".
806 String supportedExtensionAddition;
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);
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");
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();
845 // l. Set result.[[<key>]] to value.
846 result.add(key, value);
848 // m. Append supportedExtensionAddition to supportedExtension.
849 supportedExtension.append(supportedExtensionAddition);
851 // n. Increase k by 1.
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;
864 // 16. Set result.[[locale]] to foundLocale.
865 result.add(ASCIILiteral("locale"), foundLocale);
867 // 17. Return result.
871 static JSArray* lookupSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
873 // 9.2.6 LookupSupportedLocales (availableLocales, requestedLocales)
875 // 1. Let rLocales be CreateArrayFromList(requestedLocales).
878 // 2. Let len be ToLength(Get(rLocales, "length")).
879 size_t len = requestedLocales.size();
881 // 3. Let subset be an empty List.
883 JSGlobalObject* globalObject = state.callee()->globalObject();
884 JSArray* subset = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithUndecided), 0);
886 throwOutOfMemoryError(&state);
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];
898 // d. Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
899 String noExtensionsLocale = removeUnicodeLocaleExtension(locale);
901 // e. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
902 String availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
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));
908 // g. Increment k by 1.
915 static JSArray* bestFitSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
917 // 9.2.7 BestFitSupportedLocales (availableLocales, requestedLocales)
918 // FIXME: Implement something better than lookup.
919 return lookupSupportedLocales(state, availableLocales, requestedLocales);
922 JSValue supportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, JSValue options)
924 // 9.2.8 SupportedLocales (availableLocales, requestedLocales, options)
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();
936 // 2. Else, let matcher be "best fit".
937 matcher = ASCIILiteral("best fit");
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);
948 // a. Let MatcherOperation be the abstract operation LookupSupportedLocales.
949 // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
950 supportedLocales = lookupSupportedLocales(state, availableLocales, requestedLocales);
953 if (state.hadException())
954 return jsUndefined();
956 // 6. Let subset be CreateArrayFromList(supportedLocales).
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();
965 PropertyDescriptor desc;
966 desc.setConfigurable(false);
967 desc.setWritable(false);
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.
975 // b. Let status be DefinePropertyOrThrow(subset, P, desc).
976 supportedLocales->defineOwnProperty(supportedLocales, &state, keys[i], desc, true);
978 // c. Assert: status is not abrupt completion.
979 if (state.hadException())
980 return jsUndefined();
984 return supportedLocales;
987 Vector<String> numberingSystemsForLocale(const String& locale)
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));
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));
1002 uenum_close(numberingSystemNames);
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);
1011 Vector<String> numberingSystems({ defaultSystemName });
1012 numberingSystems.appendVector(availableNumberingSystems);
1013 return numberingSystems;
1018 #endif // ENABLE(INTL)