| // Copyright 2018 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef V8_INTL_SUPPORT |
| #error Internationalization is expected to be enabled. |
| #endif // V8_INTL_SUPPORT |
| |
| #include "src/objects/js-locale.h" |
| |
| #include <algorithm> |
| #include <cctype> |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "src/api/api.h" |
| #include "src/execution/isolate.h" |
| #include "src/heap/factory.h" |
| #include "src/objects/intl-objects.h" |
| #include "src/objects/js-locale-inl.h" |
| #include "src/objects/managed-inl.h" |
| #include "src/objects/objects-inl.h" |
| #include "src/objects/option-utils.h" |
| #include "unicode/calendar.h" |
| #include "unicode/char16ptr.h" |
| #include "unicode/coll.h" |
| #include "unicode/dtptngen.h" |
| #include "unicode/localebuilder.h" |
| #include "unicode/locid.h" |
| #include "unicode/ucal.h" |
| #include "unicode/uloc.h" |
| #include "unicode/ulocdata.h" |
| #include "unicode/unistr.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| |
| struct OptionData { |
| Handle<InternalizedString> (Factory::*object_key)(); |
| const char* key; |
| const std::span<const std::string_view> possible_values; |
| bool is_bool_value; |
| }; |
| struct ValueAndType { |
| const char* value; |
| const char* type; |
| }; |
| |
| // Inserts tags from options into locale string. |
| Maybe<bool> InsertOptionsIntoLocale(Isolate* isolate, |
| DirectHandle<JSReceiver> options, |
| icu::LocaleBuilder* builder) { |
| DCHECK(isolate); |
| |
| static const auto hour_cycle_values = |
| std::to_array<const std::string_view>({"h11", "h12", "h23", "h24"}); |
| static const auto case_first_values = |
| std::to_array<const std::string_view>({"upper", "lower", "false"}); |
| const auto empty_values = std::span<std::string_view>(); |
| const std::array<OptionData, 7> kOptionToUnicodeTagMap = { |
| {{&Factory::calendar_string, "ca", empty_values, false}, |
| {&Factory::collation_string, "co", empty_values, false}, |
| {&Factory::firstDayOfWeek_string, "fw", empty_values, false}, |
| {&Factory::hourCycle_string, "hc", hour_cycle_values, false}, |
| {&Factory::caseFirst_string, "kf", case_first_values, false}, |
| {&Factory::numeric_string, "kn", empty_values, true}, |
| {&Factory::numberingSystem_string, "nu", empty_values, false}}}; |
| |
| // TODO(cira): Pass in values as per the spec to make this to be |
| // spec compliant. |
| |
| for (const auto& option_to_bcp47 : kOptionToUnicodeTagMap) { |
| bool value_bool = false; |
| DirectHandle<String> name = |
| (isolate->factory()->*option_to_bcp47.object_key)(); |
| |
| bool found = false; |
| std::string_view value_str; |
| std::string owned; |
| if (option_to_bcp47.is_bool_value) { |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, found, |
| GetBoolOption(isolate, options, name, "locale", &value_bool), {}); |
| } else if (option_to_bcp47.possible_values.empty()) { |
| // We just wish to fetch the string |
| DirectHandle<String> output; |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, found, |
| GetStringOption(isolate, options, name, "locale", &output), {}); |
| if (found) { |
| owned = output->ToStdString(); |
| value_str = owned; |
| } |
| } else { |
| // The string is expected to be in a particular set. |
| ASSIGN_RETURN_ON_EXCEPTION_VALUE( |
| isolate, value_str, |
| GetStringOption<std::string_view>( |
| isolate, options, name, "locale", option_to_bcp47.possible_values, |
| option_to_bcp47.possible_values, std::string_view()), |
| {}); |
| if (!value_str.empty()) { |
| found = true; |
| } |
| } |
| |
| // TODO(cira): Use fallback value if value is not found to make |
| // this spec compliant. |
| if (!found) continue; |
| |
| const char* type = value_str.data(); |
| |
| if (strcmp(option_to_bcp47.key, "fw") == 0) { |
| const std::array<ValueAndType, 8> kFirstDayValuesAndTypes = { |
| {{"0", "sun"}, |
| {"1", "mon"}, |
| {"2", "tue"}, |
| {"3", "wed"}, |
| {"4", "thu"}, |
| {"5", "fri"}, |
| {"6", "sat"}, |
| {"7", "sun"}}}; |
| for (const auto& value_to_type : kFirstDayValuesAndTypes) { |
| if (strcmp(type, value_to_type.value) == 0) { |
| type = value_to_type.type; |
| break; |
| } |
| } |
| } else if (option_to_bcp47.is_bool_value) { |
| type = value_bool ? "true" : "false"; |
| } |
| DCHECK_NOT_NULL(type); |
| |
| // Overwrite existing, or insert new key-value to the locale string. |
| if (!uloc_toLegacyType(uloc_toLegacyKey(option_to_bcp47.key), type)) { |
| return Just(false); |
| } |
| builder->setUnicodeLocaleKeyword(option_to_bcp47.key, type); |
| } |
| return Just(true); |
| } |
| |
| DirectHandle<Object> UnicodeKeywordValue(Isolate* isolate, |
| DirectHandle<JSLocale> locale, |
| const char* key) { |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| UErrorCode status = U_ZERO_ERROR; |
| std::string value = |
| icu_locale->getUnicodeKeywordValue<std::string>(key, status); |
| if (status == U_ILLEGAL_ARGUMENT_ERROR || value.empty()) { |
| return isolate->factory()->undefined_value(); |
| } |
| if (value == "yes") { |
| value = "true"; |
| } |
| if (value == "true" && strcmp(key, "kf") == 0) { |
| return isolate->factory()->NewStringFromStaticChars(""); |
| } |
| return isolate->factory()->NewStringFromAsciiChecked(value.c_str()); |
| } |
| |
| bool IsCheckRange(std::string_view str, size_t min, size_t max, |
| bool(range_check_func)(char)) { |
| if (!base::IsInRange(str.length(), min, max)) return false; |
| for (size_t i = 0; i < str.length(); i++) { |
| if (!range_check_func(str[i])) return false; |
| } |
| return true; |
| } |
| bool IsAlpha(std::string_view str, size_t min, size_t max) { |
| return IsCheckRange(str, min, max, [](char c) -> bool { |
| return base::IsInRange(c, 'a', 'z') || base::IsInRange(c, 'A', 'Z'); |
| }); |
| } |
| |
| bool IsDigit(std::string_view str, size_t min, size_t max) { |
| return IsCheckRange(str, min, max, [](char c) -> bool { |
| return base::IsInRange(c, '0', '9'); |
| }); |
| } |
| |
| bool IsAlphanum(std::string_view str, size_t min, size_t max) { |
| return IsCheckRange(str, min, max, [](char c) -> bool { |
| return base::IsInRange(c, 'a', 'z') || base::IsInRange(c, 'A', 'Z') || |
| base::IsInRange(c, '0', '9'); |
| }); |
| } |
| |
| bool IsUnicodeLanguageSubtag(std::string_view value) { |
| // unicode_language_subtag = alpha{2,3} | alpha{5,8}; |
| return IsAlpha(value, 2, 3) || IsAlpha(value, 5, 8); |
| } |
| |
| bool IsUnicodeScriptSubtag(std::string_view value) { |
| // unicode_script_subtag = alpha{4} ; |
| return IsAlpha(value, 4, 4); |
| } |
| |
| bool IsUnicodeRegionSubtag(std::string_view value) { |
| // unicode_region_subtag = (alpha{2} | digit{3}); |
| return IsAlpha(value, 2, 2) || IsDigit(value, 3, 3); |
| } |
| |
| bool IsDigitAlphanum3(std::string_view value) { |
| return value.length() == 4 && base::IsInRange(value[0], '0', '9') && |
| IsAlphanum(value.substr(1), 3, 3); |
| } |
| |
| bool IsUnicodeVariantSubtag(std::string_view value) { |
| // unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3}) ; |
| return IsAlphanum(value, 5, 8) || IsDigitAlphanum3(value); |
| } |
| |
| bool IsExtensionSingleton(std::string_view value) { |
| return IsAlphanum(value, 1, 1); |
| } |
| |
| int32_t weekdayFromEDaysOfWeek(icu::Calendar::EDaysOfWeek eDaysOfWeek) { |
| return (eDaysOfWeek == icu::Calendar::SUNDAY) ? 7 : eDaysOfWeek - 1; |
| } |
| |
| } // namespace |
| |
| // Implemented as iteration instead of recursion to avoid stack overflow for |
| // very long input strings. |
| bool JSLocale::Is38AlphaNumList(std::string_view in) { |
| std::string_view value = in; |
| while (true) { |
| std::size_t found_dash = value.find('-'); |
| if (found_dash == std::string::npos) { |
| return IsAlphanum(value, 3, 8); |
| } |
| if (!IsAlphanum(value.substr(0, found_dash), 3, 8)) return false; |
| value = value.substr(found_dash + 1); |
| } |
| } |
| |
| bool JSLocale::Is3Alpha(std::string_view value) { return IsAlpha(value, 3, 3); } |
| |
| // TODO(ftang) Replace the following check w/ icu::LocaleBuilder |
| // once ICU64 land in March 2019. |
| bool JSLocale::StartsWithUnicodeLanguageId(std::string_view value) { |
| // unicode_language_id = |
| // unicode_language_subtag (sep unicode_script_subtag)? |
| // (sep unicode_region_subtag)? (sep unicode_variant_subtag)* ; |
| if (value.empty()) return false; |
| std::vector<std::string_view> tokens; |
| size_t token_start = 0; |
| size_t token_end; |
| for (token_end = 0; token_end < value.size(); ++token_end) { |
| if (value[token_end] == '-') { |
| tokens.emplace_back(&value[token_start], token_end - token_start); |
| token_start = token_end + 1; |
| } |
| } |
| if (token_start != token_end) { |
| tokens.emplace_back(&value[token_start], token_end - token_start); |
| } |
| DCHECK(!tokens.empty()); |
| |
| // length >= 1 |
| if (!IsUnicodeLanguageSubtag(tokens[0])) return false; |
| |
| if (tokens.size() == 1) return true; |
| |
| // length >= 2 |
| if (IsExtensionSingleton(tokens[1])) return true; |
| |
| size_t index = 1; |
| if (IsUnicodeScriptSubtag(tokens[index])) { |
| index++; |
| if (index == tokens.size()) return true; |
| } |
| if (IsUnicodeRegionSubtag(tokens[index])) { |
| index++; |
| } |
| while (index < tokens.size()) { |
| if (IsExtensionSingleton(tokens[index])) return true; |
| if (!IsUnicodeVariantSubtag(tokens[index])) return false; |
| index++; |
| } |
| return true; |
| } |
| |
| namespace { |
| // Return false if variants contain duplicate elements or invalid delimiters. |
| bool IsValidVariants(std::string_view variants) { |
| // The length of one unicode_variant_subtag is between 4-8. To have |
| // duplicate or multiple subtags, the length of the variants need to be >= |
| // 4+1+4 = 9. |
| if (variants.length() >= 9) { |
| std::set<std::string> set; |
| size_t start = 0; |
| for (size_t i = 0; i <= variants.length(); ++i) { |
| if (i == variants.length() || variants[i] == '-') { |
| std::string subtag(variants.substr(start, i - start)); |
| std::transform( |
| subtag.begin(), subtag.end(), subtag.begin(), |
| [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); |
| if (!set.insert(subtag).second) { |
| return false; |
| } |
| start = i + 1; |
| } else if (variants[i] == '_') { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| Maybe<bool> ApplyOptionsToTag(Isolate* isolate, DirectHandle<String> tag, |
| DirectHandle<JSReceiver> options, |
| icu::LocaleBuilder* builder) { |
| v8::Isolate* v8_isolate = reinterpret_cast<v8::Isolate*>(isolate); |
| if (tag->length() == 0) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kLocaleNotEmpty)); |
| } |
| |
| v8::String::Utf8Value bcp47_tag(v8_isolate, v8::Utils::ToLocal(tag)); |
| builder->setLanguageTag( |
| {*bcp47_tag, static_cast<int32_t>(bcp47_tag.length())}); |
| DCHECK_LT(0, bcp47_tag.length()); |
| DCHECK_NOT_NULL(*bcp47_tag); |
| // 2. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError |
| // exception. |
| if (!JSLocale::StartsWithUnicodeLanguageId(*bcp47_tag)) { |
| return Just(false); |
| } |
| UErrorCode status = U_ZERO_ERROR; |
| icu::Locale canonicalized = builder->build(status); |
| canonicalized.canonicalize(status); |
| if (U_FAILURE(status)) { |
| return Just(false); |
| } |
| builder->setLocale(canonicalized); |
| |
| // 3. Let language be ? GetOption(options, "language", "string", undefined, |
| // undefined). |
| DirectHandle<String> language_str; |
| Maybe<bool> maybe_language = |
| GetStringOption(isolate, options, isolate->factory()->language_string(), |
| "ApplyOptionsToTag", &language_str); |
| MAYBE_RETURN(maybe_language, Nothing<bool>()); |
| |
| // 4. If language is not undefined, then |
| if (maybe_language.FromJust()) { |
| std::string language_stdstr = language_str->ToStdString(); |
| builder->setLanguage(language_stdstr); |
| builder->build(status); |
| // a. If language does not match the unicode_language_subtag production, |
| // throw a RangeError exception. |
| if (U_FAILURE(status) || language_stdstr.empty() || |
| IsAlpha(language_stdstr, 4, 4)) { |
| return Just(false); |
| } |
| } |
| // 5. Let script be ? GetOption(options, "script", "string", undefined, |
| // undefined). |
| DirectHandle<String> script_str; |
| Maybe<bool> maybe_script = |
| GetStringOption(isolate, options, isolate->factory()->script_string(), |
| "ApplyOptionsToTag", &script_str); |
| MAYBE_RETURN(maybe_script, Nothing<bool>()); |
| // 6. If script is not undefined, then |
| if (maybe_script.FromJust()) { |
| std::string script_stdstr = script_str->ToStdString(); |
| builder->setScript(script_stdstr); |
| builder->build(status); |
| // a. If script does not match the unicode_script_subtag production, throw |
| // a RangeError exception. |
| if (U_FAILURE(status) || script_stdstr.empty()) { |
| return Just(false); |
| } |
| } |
| // 7. Let region be ? GetOption(options, "region", "string", undefined, |
| // undefined). |
| DirectHandle<String> region_str; |
| Maybe<bool> maybe_region = |
| GetStringOption(isolate, options, isolate->factory()->region_string(), |
| "ApplyOptionsToTag", ®ion_str); |
| MAYBE_RETURN(maybe_region, Nothing<bool>()); |
| // 8. If region is not undefined, then |
| if (maybe_region.FromJust()) { |
| std::string region_stdstr = region_str->ToStdString(); |
| // a. If region does not match the region production, throw a RangeError |
| // exception. |
| builder->setRegion(region_stdstr); |
| builder->build(status); |
| if (U_FAILURE(status) || region_stdstr.empty()) { |
| return Just(false); |
| } |
| } |
| |
| // 8. Let variants be ? GetOption(options, "variants", string, empty, |
| // GetLocaleVariants(baseName)). |
| DirectHandle<String> variants_str; |
| Maybe<bool> maybe_variants = |
| GetStringOption(isolate, options, isolate->factory()->variants_string(), |
| "ApplyOptionsToTag", &variants_str); |
| MAYBE_RETURN(maybe_variants, Nothing<bool>()); |
| // 9. If variants is not undefined, then |
| if (maybe_variants.FromJust()) { |
| // a. If variants is the empty String, throw a RangeError exception. |
| // b. Let lowerVariants be the ASCII-lowercase of variants. |
| std::string variants_stdstr = variants_str->ToStdString(); |
| // c. Let variantSubtags be StringSplitToList(lowerVariants, "-"). |
| // d. For each element variant of variantSubtags, do |
| // i. If variant cannot be matched by the unicode_variant_subtag Unicode |
| // locale nonterminal, throw a RangeError exception. |
| builder->setVariant(variants_stdstr); |
| builder->build(status); |
| if (U_FAILURE(status) || variants_stdstr.empty()) { |
| return Just(false); |
| } |
| // e. If variantSubtags contains any duplicate elements, throw a |
| // RangeError exception. |
| if (!IsValidVariants(variants_stdstr)) { |
| return Just(false); |
| } |
| } |
| |
| // 9. Set tag to CanonicalizeLanguageTag(tag). |
| // 10. If language is not undefined, |
| // a. Assert: tag matches the unicode_locale_id production. |
| // b. Set tag to tag with the substring corresponding to the |
| // unicode_language_subtag production replaced by the string language. |
| // 11. If script is not undefined, then |
| // a. If tag does not contain a unicode_script_subtag production, then |
| // i. Set tag to the concatenation of the unicode_language_subtag |
| // production of tag, "-", script, and the rest of tag. |
| // b. Else, |
| // i. Set tag to tag with the substring corresponding to the |
| // unicode_script_subtag production replaced by the string script. |
| // 12. If region is not undefined, then |
| // a. If tag does not contain a unicode_region_subtag production, then |
| // i. Set tag to the concatenation of the unicode_language_subtag |
| // production of tag, the substring corresponding to the "-" |
| // unicode_script_subtag production if present, "-", region, and |
| // the rest of tag. |
| // b. Else, |
| // i. Set tag to tag with the substring corresponding to the |
| // unicode_region_subtag production replaced by the string region. |
| // 13. Return CanonicalizeLanguageTag(tag). |
| return Just(true); |
| } |
| |
| } // namespace |
| |
| MaybeDirectHandle<JSLocale> JSLocale::New(Isolate* isolate, |
| DirectHandle<Map> map, |
| DirectHandle<String> locale_str, |
| DirectHandle<JSReceiver> options) { |
| icu::LocaleBuilder builder; |
| Maybe<bool> maybe_apply = |
| ApplyOptionsToTag(isolate, locale_str, options, &builder); |
| MAYBE_RETURN(maybe_apply, MaybeDirectHandle<JSLocale>()); |
| if (!maybe_apply.FromJust()) { |
| THROW_NEW_ERROR(isolate, |
| NewRangeError(MessageTemplate::kLocaleBadParameters)); |
| } |
| |
| Maybe<bool> maybe_insert = |
| InsertOptionsIntoLocale(isolate, options, &builder); |
| MAYBE_RETURN(maybe_insert, MaybeDirectHandle<JSLocale>()); |
| UErrorCode status = U_ZERO_ERROR; |
| icu::Locale icu_locale = builder.build(status); |
| |
| icu_locale.canonicalize(status); |
| |
| if (!maybe_insert.FromJust() || U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, |
| NewRangeError(MessageTemplate::kLocaleBadParameters)); |
| } |
| |
| // 31. Set locale.[[Locale]] to r.[[locale]]. |
| DirectHandle<Managed<icu::Locale>> managed_locale = |
| Managed<icu::Locale>::From( |
| isolate, 0, std::shared_ptr<icu::Locale>{icu_locale.clone()}); |
| |
| // Now all properties are ready, so we can allocate the result object. |
| DirectHandle<JSLocale> locale = |
| Cast<JSLocale>(isolate->factory()->NewFastOrSlowJSObjectFromMap(map)); |
| DisallowGarbageCollection no_gc; |
| locale->set_icu_locale(*managed_locale); |
| return locale; |
| } |
| |
| namespace { |
| |
| MaybeDirectHandle<JSLocale> Construct(Isolate* isolate, |
| const icu::Locale& icu_locale) { |
| DirectHandle<Managed<icu::Locale>> managed_locale = |
| Managed<icu::Locale>::From( |
| isolate, 0, std::shared_ptr<icu::Locale>{icu_locale.clone()}); |
| |
| DirectHandle<JSFunction> constructor( |
| isolate->native_context()->intl_locale_function(), isolate); |
| |
| DirectHandle<Map> map; |
| ASSIGN_RETURN_ON_EXCEPTION( |
| isolate, map, |
| JSFunction::GetDerivedMap(isolate, constructor, constructor)); |
| |
| DirectHandle<JSLocale> locale = |
| Cast<JSLocale>(isolate->factory()->NewFastOrSlowJSObjectFromMap(map)); |
| DisallowGarbageCollection no_gc; |
| locale->set_icu_locale(*managed_locale); |
| return locale; |
| } |
| |
| } // namespace |
| |
| MaybeDirectHandle<JSLocale> JSLocale::Maximize(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| // ICU has limitation on the length of the locale while addLikelySubtags |
| // is called. Work around the issue by only perform addLikelySubtags |
| // on the base locale and merge the extension if needed. |
| icu::Locale source(*(locale->icu_locale()->ptr())); |
| icu::Locale result = icu::Locale::createFromName(source.getBaseName()); |
| UErrorCode status = U_ZERO_ERROR; |
| result.addLikelySubtags(status); |
| if (strlen(source.getBaseName()) != strlen(result.getBaseName())) { |
| // Base name is changed |
| if (strlen(source.getBaseName()) != strlen(source.getName())) { |
| // the source has extensions, get the extensions from the source. |
| result = icu::LocaleBuilder() |
| .setLocale(source) |
| .setLanguage(result.getLanguage()) |
| .setRegion(result.getCountry()) |
| .setScript(result.getScript()) |
| .setVariant(result.getVariant()) |
| .build(status); |
| } |
| } else { |
| // Base name is not changed |
| result = source; |
| } |
| if (U_FAILURE(status) || result.isBogus()) { |
| // Due to https://unicode-org.atlassian.net/browse/ICU-21639 |
| // Valid but super long locale will fail. Just throw here for now. |
| THROW_NEW_ERROR(isolate, |
| NewRangeError(MessageTemplate::kLocaleBadParameters)); |
| } |
| return Construct(isolate, result); |
| } |
| |
| MaybeDirectHandle<JSLocale> JSLocale::Minimize(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| // ICU has limitation on the length of the locale while minimizeSubtags |
| // is called. Work around the issue by only perform addLikelySubtags |
| // on the base locale and merge the extension if needed. |
| icu::Locale source(*(locale->icu_locale()->ptr())); |
| icu::Locale result = icu::Locale::createFromName(source.getBaseName()); |
| UErrorCode status = U_ZERO_ERROR; |
| result.minimizeSubtags(status); |
| if (strcmp(source.getBaseName(), result.getBaseName()) != 0) { |
| // Base name is changed |
| if (strlen(source.getBaseName()) != strlen(source.getName())) { |
| // the source has extensions, get the extensions from the source. |
| result = icu::LocaleBuilder() |
| .setLocale(source) |
| .setLanguage(result.getLanguage()) |
| .setRegion(result.getCountry()) |
| .setScript(result.getScript()) |
| .setVariant(result.getVariant()) |
| .build(status); |
| } |
| } else { |
| // Base name is not changed |
| result = source; |
| } |
| if (U_FAILURE(status) || result.isBogus()) { |
| // Due to https://unicode-org.atlassian.net/browse/ICU-21639 |
| // Valid but super long locale will fail. Just throw here for now. |
| THROW_NEW_ERROR(isolate, |
| NewRangeError(MessageTemplate::kLocaleBadParameters)); |
| } |
| return Construct(isolate, result); |
| } |
| |
| template <typename T> |
| MaybeDirectHandle<JSArray> GetKeywordValuesFromLocale( |
| Isolate* isolate, const char* key, const char* unicode_key, |
| const icu::Locale& locale, bool (*removes)(const char*), bool commonly_used, |
| bool sort) { |
| Factory* factory = isolate->factory(); |
| UErrorCode status = U_ZERO_ERROR; |
| std::string ext = |
| locale.getUnicodeKeywordValue<std::string>(unicode_key, status); |
| if (!ext.empty()) { |
| DirectHandle<FixedArray> fixed_array = factory->NewFixedArray(1); |
| DirectHandle<String> str = factory->NewStringFromAsciiChecked(ext.c_str()); |
| fixed_array->set(0, *str); |
| return factory->NewJSArrayWithElements(fixed_array); |
| } |
| status = U_ZERO_ERROR; |
| std::unique_ptr<icu::StringEnumeration> enumeration( |
| T::getKeywordValuesForLocale(key, locale, commonly_used, status)); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| return Intl::ToJSArray( |
| isolate, |
| [unicode_key](const char* value) { |
| return std::string(uloc_toUnicodeLocaleType(unicode_key, value)); |
| }, |
| enumeration.get(), removes, sort); |
| } |
| |
| namespace { |
| |
| // https://tc39.es/ecma402/#sec-canonicalunicodesubdivision |
| std::string CanonicalUnicodeSubdivision(const icu::Locale& icu_locale, |
| const char* key) { |
| UErrorCode status = U_ZERO_ERROR; |
| // 1. Let subdivision be UnicodeExtensionValue(locale, key). |
| std::string subdivision = |
| icu_locale.getUnicodeKeywordValue<std::string>(key, status); |
| |
| // 2. If subdivision is ~empty~, return undefined. |
| if (U_FAILURE(status) || subdivision.empty()) return ""; |
| |
| // 3. If subdivision cannot be matched by the unicode_subdivision_id Unicode |
| // locale nonterminal, return undefined. |
| // unicode_subdivision_id = unicode_region_subtag unicode_subdivision_suffix |
| // unicode_region_subtag = (alpha{2} | digit{3}) |
| // unicode_subdivision_suffix = alphanum{1,4} |
| // |
| // 4. Let region be the longest prefix of subdivision matched by the |
| // unicode_region_subtag Unicode locale nonterminal. |
| // Real examples: "gbeng" -> region "GB", "usca" -> region "US", "001abc" -> |
| // region "001". |
| std::string_view sub_view(subdivision); |
| std::string_view region; |
| if (sub_view.length() >= 4 && IsDigit(sub_view.substr(0, 3), 3, 3)) { |
| if (!IsAlphanum(sub_view.substr(3), 1, 4)) return ""; |
| region = sub_view.substr(0, 3); |
| } else { |
| if (sub_view.length() < 3 || !IsAlpha(sub_view.substr(0, 2), 2, 2) || |
| !IsAlphanum(sub_view.substr(2), 1, 4)) { |
| return ""; |
| } |
| region = sub_view.substr(0, 2); |
| } |
| |
| // 5. Let regionLocale be the string-concatenation of "und-" and region. |
| // 6. Set regionLocale to CanonicalizeUnicodeLocaleId(regionLocale). |
| // 7. Return GetLocaleRegion(regionLocale). |
| std::string result(region); |
| std::transform(result.begin(), result.end(), result.begin(), |
| [](unsigned char c) { return std::toupper(c); }); |
| return result; |
| } |
| |
| struct RegionPreferenceRecord { |
| std::string region; |
| std::string region_override; |
| }; |
| |
| // https://tc39.es/ecma402/#sec-regionpreference |
| RegionPreferenceRecord RegionPreference(const icu::Locale& icu_locale) { |
| // 1. Let region be GetLocaleRegion(locale). |
| const char* country = icu_locale.getCountry(); |
| std::string region = (country != nullptr) ? country : ""; |
| |
| // 2. If region is undefined, then |
| if (region.empty()) { |
| // a. Set region to CanonicalUnicodeSubdivision(locale, "sd"). |
| region = CanonicalUnicodeSubdivision(icu_locale, "sd"); |
| |
| // b. If region is undefined, then |
| if (region.empty()) { |
| // i. Let maximal be the result of the Add Likely Subtags algorithm |
| // applied to locale. If an error is signaled, set maximal to locale. |
| // ii. Set maximal to CanonicalizeUnicodeLocaleId(maximal). |
| UErrorCode status = U_ZERO_ERROR; |
| icu::Locale maximal = icu_locale; |
| maximal.addLikelySubtags(status); |
| if (U_FAILURE(status)) maximal = icu_locale; |
| |
| // iii. Set region to GetLocaleRegion(maximal). |
| const char* max_country = maximal.getCountry(); |
| region = (max_country != nullptr) ? max_country : ""; |
| |
| // iv. If region is undefined, then |
| if (region.empty()) { |
| // 1. Set region to "001". |
| region = "001"; |
| } |
| } |
| } |
| |
| // 3. Let regionOverride be CanonicalUnicodeSubdivision(locale, "rg"). |
| std::string region_override = CanonicalUnicodeSubdivision(icu_locale, "rg"); |
| |
| // 4. Return { [[Region]]: region, [[RegionOverride]]: regionOverride }. |
| return {region, region_override}; |
| } |
| |
| // Applies RegionPreference(icu_locale) to construct an icu::Locale with the |
| // preferred lookup region. Implements region resolution for: |
| // - (CalendarsOfLocale) 2-3. Let preference be |
| // RegionPreference(loc.[[Locale]]). |
| // If preference.[[RegionOverride]] is not undefined, let preferredRegions be |
| // « preference.[[RegionOverride]], preference.[[Region]] »; else ... |
| // - (HourCyclesOfLocale) 2-3. Let preference be |
| // RegionPreference(loc.[[Locale]]). |
| // If preference.[[RegionOverride]] is not undefined, let preferredRegions be |
| // « preference.[[RegionOverride]], preference.[[Region]] »; else ... |
| // - (WeekInfoOfLocale) 1-5. Let preference be RegionPreference(loc.[[Locale]]). |
| // Select lookupRegion based on regionOverride or region. |
| icu::Locale ApplyRegionPreference(const icu::Locale& icu_locale) { |
| RegionPreferenceRecord pref = RegionPreference(icu_locale); |
| std::string lookup_region = |
| !pref.region_override.empty() ? pref.region_override : pref.region; |
| UErrorCode status = U_ZERO_ERROR; |
| icu::Locale res = icu::LocaleBuilder() |
| .setLocale(icu_locale) |
| .setRegion(lookup_region) |
| .build(status); |
| return U_SUCCESS(status) ? res : icu_locale; |
| } |
| |
| MaybeDirectHandle<JSArray> CalendarsForLocale(Isolate* isolate, |
| const icu::Locale& icu_locale, |
| bool commonly_used, bool sort) { |
| return GetKeywordValuesFromLocale<icu::Calendar>( |
| isolate, "calendar", "ca", icu_locale, nullptr, commonly_used, sort); |
| } |
| |
| } // namespace |
| |
| MaybeDirectHandle<JSArray> JSLocale::GetCalendars( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| icu::Locale icu_locale(*(locale->icu_locale()->ptr())); |
| return CalendarsForLocale(isolate, ApplyRegionPreference(icu_locale), true, |
| false); |
| } |
| |
| MaybeDirectHandle<JSArray> Intl::AvailableCalendars(Isolate* isolate) { |
| icu::Locale icu_locale("und"); |
| return CalendarsForLocale(isolate, icu_locale, false, true); |
| } |
| |
| MaybeDirectHandle<JSArray> JSLocale::GetCollations( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| icu::Locale icu_locale(*(locale->icu_locale()->ptr())); |
| return GetKeywordValuesFromLocale<icu::Collator>( |
| isolate, "collations", "co", icu_locale, Intl::RemoveCollation, true, |
| true); |
| } |
| |
| MaybeDirectHandle<JSArray> JSLocale::GetHourCycles( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| // Let preferred be loc.[[HourCycle]]. |
| // Let locale be loc.[[Locale]]. |
| icu::Locale icu_locale(*(locale->icu_locale()->ptr())); |
| Factory* factory = isolate->factory(); |
| |
| // Assert: locale matches the unicode_locale_id production. |
| |
| // Let list be a List of 1 or more hour cycle identifiers, which must be |
| // String values indicating either the 12-hour format ("h11", "h12") or the |
| // 24-hour format ("h23", "h24"), sorted in descending preference of those in |
| // common use in the locale for date and time formatting. |
| |
| // Return CreateArrayFromListAndPreferred( list, preferred ). |
| DirectHandle<FixedArray> fixed_array = factory->NewFixedArray(1); |
| UErrorCode status = U_ZERO_ERROR; |
| std::string ext = |
| icu_locale.getUnicodeKeywordValue<std::string>("hc", status); |
| if (!ext.empty()) { |
| DirectHandle<String> str = factory->NewStringFromAsciiChecked(ext.c_str()); |
| fixed_array->set(0, *str); |
| return factory->NewJSArrayWithElements(fixed_array); |
| } |
| status = U_ZERO_ERROR; |
| std::unique_ptr<icu::DateTimePatternGenerator> generator( |
| icu::DateTimePatternGenerator::createInstance( |
| ApplyRegionPreference(icu_locale), status)); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| |
| UDateFormatHourCycle hc = generator->getDefaultHourCycle(status); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| DirectHandle<String> hour_cycle; |
| |
| switch (hc) { |
| case UDAT_HOUR_CYCLE_11: |
| hour_cycle = factory->h11_string(); |
| break; |
| case UDAT_HOUR_CYCLE_12: |
| hour_cycle = factory->h12_string(); |
| break; |
| case UDAT_HOUR_CYCLE_23: |
| hour_cycle = factory->h23_string(); |
| break; |
| case UDAT_HOUR_CYCLE_24: |
| hour_cycle = factory->h24_string(); |
| break; |
| default: |
| break; |
| } |
| fixed_array->set(0, *hour_cycle); |
| return factory->NewJSArrayWithElements(fixed_array); |
| } |
| |
| MaybeDirectHandle<JSArray> JSLocale::GetNumberingSystems( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| // Let preferred be loc.[[NumberingSystem]]. |
| |
| // Let locale be loc.[[Locale]]. |
| icu::Locale icu_locale(*(locale->icu_locale()->ptr())); |
| Factory* factory = isolate->factory(); |
| |
| // Assert: locale matches the unicode_locale_id production. |
| |
| // Let list be a List of 1 or more numbering system identifiers, which must be |
| // String values conforming to the type sequence from UTS 35 Unicode Locale |
| // Identifier, section 3.2, sorted in descending preference of those in common |
| // use in the locale for formatting numeric values. |
| |
| // Return CreateArrayFromListAndPreferred( list, preferred ). |
| UErrorCode status = U_ZERO_ERROR; |
| DirectHandle<FixedArray> fixed_array = factory->NewFixedArray(1); |
| std::string numbering_system = |
| icu_locale.getUnicodeKeywordValue<std::string>("nu", status); |
| if (numbering_system.empty()) { |
| numbering_system = Intl::GetNumberingSystem(icu_locale); |
| } |
| DirectHandle<String> str = |
| factory->NewStringFromAsciiChecked(numbering_system.c_str()); |
| |
| fixed_array->set(0, *str); |
| return factory->NewJSArrayWithElements(fixed_array); |
| } |
| |
| MaybeDirectHandle<Object> JSLocale::GetTimeZones( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| // Let loc be the this value. |
| |
| // Perform ? RequireInternalSlot(loc, [[InitializedLocale]]) |
| |
| // Let locale be loc.[[Locale]]. |
| icu::Locale icu_locale(*(locale->icu_locale()->ptr())); |
| Factory* factory = isolate->factory(); |
| |
| // If the unicode_language_id production of locale does not contain the |
| // ["-" unicode_region_subtag] sequence, return undefined. |
| const char* region = icu_locale.getCountry(); |
| if (region == nullptr || strlen(region) == 0) { |
| return factory->undefined_value(); |
| } |
| |
| // Return TimeZonesOfLocale(loc). |
| |
| // Let locale be loc.[[Locale]]. |
| |
| // Assert: locale matches the unicode_locale_id production. |
| |
| // Let region be the substring of locale corresponding to the |
| // unicode_region_subtag production of the unicode_language_id. |
| |
| // Let list be a List of 1 or more time zone identifiers, which must be String |
| // values indicating a Zone or Link name of the IANA Time Zone Database, |
| // sorted in descending preference of those in common use in region. |
| UErrorCode status = U_ZERO_ERROR; |
| std::unique_ptr<icu::StringEnumeration> enumeration( |
| icu::TimeZone::createTimeZoneIDEnumeration(UCAL_ZONE_TYPE_CANONICAL, |
| region, nullptr, status)); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| return Intl::ToJSArray(isolate, nullptr, enumeration.get(), nullptr, true); |
| } |
| |
| MaybeDirectHandle<JSObject> JSLocale::GetTextInfo( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| // Let loc be the this value. |
| |
| // Perform ? RequireInternalSlot(loc, [[InitializedLocale]]). |
| |
| // Let locale be loc.[[Locale]]. |
| |
| // Assert: locale matches the unicode_locale_id production. |
| |
| Factory* factory = isolate->factory(); |
| // Let info be ! ObjectCreate(%Object.prototype%). |
| DirectHandle<JSObject> info = |
| factory->NewJSObject(isolate->object_function()); |
| |
| // Let dir be "ltr". |
| DirectHandle<String> dir = locale->icu_locale()->ptr()->isRightToLeft() |
| ? factory->rtl_string() |
| : factory->ltr_string(); |
| |
| // Perform ! CreateDataPropertyOrThrow(info, "direction", dir). |
| CHECK(JSReceiver::CreateDataProperty( |
| isolate, info, factory->direction_string(), dir, Just(kDontThrow)) |
| .FromJust()); |
| |
| // Return info. |
| return info; |
| } |
| |
| MaybeDirectHandle<JSObject> JSLocale::GetWeekInfo( |
| Isolate* isolate, DirectHandle<JSLocale> locale) { |
| // Let loc be the this value. |
| |
| // Perform ? RequireInternalSlot(loc, [[InitializedLocale]]). |
| |
| // Let locale be loc.[[Locale]]. |
| |
| // Assert: locale matches the unicode_locale_id production. |
| Factory* factory = isolate->factory(); |
| |
| // Let info be ! ObjectCreate(%Object.prototype%). |
| DirectHandle<JSObject> info = |
| factory->NewJSObject(isolate->object_function()); |
| UErrorCode status = U_ZERO_ERROR; |
| std::unique_ptr<icu::Calendar> calendar(icu::Calendar::createInstance( |
| ApplyRegionPreference(*(locale->icu_locale()->ptr())), status)); |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| |
| // Let fd be the weekday value indicating which day of the week is considered |
| // the 'first' day, for calendar purposes, in the locale. |
| int32_t fd = weekdayFromEDaysOfWeek(calendar->getFirstDayOfWeek()); |
| |
| // Let wi be ! WeekInfoOfLocale(loc). |
| // Let we be ! CreateArrayFromList( wi.[[Weekend]] ). |
| Handle<FixedArray> wi = Cast<FixedArray>(factory->NewFixedArray(2)); |
| int32_t length = 0; |
| for (int32_t i = 1; i <= 7; i++) { |
| UCalendarDaysOfWeek day = |
| (i == 7) ? UCAL_SUNDAY : static_cast<UCalendarDaysOfWeek>(i + 1); |
| if (UCAL_WEEKDAY != calendar->getDayOfWeekType(day, status)) { |
| wi->set(length++, Smi::FromInt(i)); |
| CHECK_LE(length, 2); |
| } |
| } |
| if (length != 2) { |
| wi = wi->RightTrimOrEmpty(isolate, wi, length); |
| } |
| DirectHandle<JSArray> we = factory->NewJSArrayWithElements(wi); |
| |
| if (U_FAILURE(status)) { |
| THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError)); |
| } |
| |
| // Perform ! CreateDataPropertyOrThrow(info, "firstDay", fd). |
| CHECK(JSReceiver::CreateDataProperty( |
| isolate, info, factory->firstDay_string(), |
| factory->NewNumberFromInt(fd), Just(kDontThrow)) |
| .FromJust()); |
| |
| // Perform ! CreateDataPropertyOrThrow(info, "weekend", we). |
| CHECK(JSReceiver::CreateDataProperty(isolate, info, factory->weekend_string(), |
| we, Just(kDontThrow)) |
| .FromJust()); |
| |
| // Return info. |
| return info; |
| } |
| |
| DirectHandle<Object> JSLocale::Language(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| Factory* factory = isolate->factory(); |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| const char* language = icu_locale->getLanguage(); |
| constexpr const char kUnd[] = "und"; |
| if (strlen(language) == 0) { |
| language = kUnd; |
| } |
| return factory->NewStringFromAsciiChecked(language); |
| } |
| |
| DirectHandle<Object> JSLocale::Script(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| Factory* factory = isolate->factory(); |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| const char* script = icu_locale->getScript(); |
| if (strlen(script) == 0) return factory->undefined_value(); |
| return factory->NewStringFromAsciiChecked(script); |
| } |
| |
| DirectHandle<Object> JSLocale::Variants(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| Factory* factory = isolate->factory(); |
| std::string variants = locale->icu_locale()->ptr()->getVariant(); |
| if (variants.length() == 0) return factory->undefined_value(); |
| // icu::Locale::getVariants() return the variants in upper case characters |
| // with '_', we need to convert it to lower case and '-' before return. |
| std::transform(variants.begin(), variants.end(), variants.begin(), |
| [](unsigned char c) { |
| if (c == '_') return '-'; |
| return static_cast<char>(std::tolower(c)); |
| }); |
| |
| return factory->NewStringFromAsciiChecked(variants.c_str()); |
| } |
| |
| DirectHandle<Object> JSLocale::Region(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| Factory* factory = isolate->factory(); |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| const char* region = icu_locale->getCountry(); |
| if (strlen(region) == 0) return factory->undefined_value(); |
| return factory->NewStringFromAsciiChecked(region); |
| } |
| |
| DirectHandle<String> JSLocale::BaseName(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| icu::Locale icu_locale = |
| icu::Locale::createFromName(locale->icu_locale()->ptr()->getBaseName()); |
| std::string base_name = Intl::ToLanguageTag(icu_locale).FromJust(); |
| return isolate->factory()->NewStringFromAsciiChecked(base_name.c_str()); |
| } |
| |
| DirectHandle<Object> JSLocale::Calendar(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "ca"); |
| } |
| |
| DirectHandle<Object> JSLocale::CaseFirst(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "kf"); |
| } |
| |
| DirectHandle<Object> JSLocale::Collation(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "co"); |
| } |
| |
| DirectHandle<Object> JSLocale::FirstDayOfWeek(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "fw"); |
| } |
| DirectHandle<Object> JSLocale::HourCycle(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "hc"); |
| } |
| |
| DirectHandle<Object> JSLocale::Numeric(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| Factory* factory = isolate->factory(); |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| UErrorCode status = U_ZERO_ERROR; |
| std::string numeric = |
| icu_locale->getUnicodeKeywordValue<std::string>("kn", status); |
| return factory->ToBoolean(numeric == "true"); |
| } |
| |
| DirectHandle<Object> JSLocale::NumberingSystem(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| return UnicodeKeywordValue(isolate, locale, "nu"); |
| } |
| |
| std::string JSLocale::ToString(DirectHandle<JSLocale> locale) { |
| Managed<icu::Locale>::Ptr icu_locale = locale->icu_locale()->ptr(); |
| return Intl::ToLanguageTag(*icu_locale).FromJust(); |
| } |
| |
| DirectHandle<String> JSLocale::ToString(Isolate* isolate, |
| DirectHandle<JSLocale> locale) { |
| std::string locale_str = JSLocale::ToString(locale); |
| return isolate->factory()->NewStringFromAsciiChecked(locale_str.c_str()); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |