IntlDisplayNames.cpp
1 /* 2 * Copyright (C) 2020 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 #include "config.h" 27 #include "IntlDisplayNames.h" 28 29 #include "IntlObjectInlines.h" 30 #include "JSCInlines.h" 31 #include "ObjectConstructor.h" 32 #include <unicode/ucurr.h> 33 #include <unicode/uloc.h> 34 #include <wtf/unicode/icu/ICUHelpers.h> 35 36 namespace JSC { 37 38 const ClassInfo IntlDisplayNames::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDisplayNames) }; 39 40 IntlDisplayNames* IntlDisplayNames::create(VM& vm, Structure* structure) 41 { 42 auto* object = new (NotNull, allocateCell<IntlDisplayNames>(vm.heap)) IntlDisplayNames(vm, structure); 43 object->finishCreation(vm); 44 return object; 45 } 46 47 Structure* IntlDisplayNames::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) 48 { 49 return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); 50 } 51 52 IntlDisplayNames::IntlDisplayNames(VM& vm, Structure* structure) 53 : Base(vm, structure) 54 { 55 } 56 57 void IntlDisplayNames::finishCreation(VM& vm) 58 { 59 Base::finishCreation(vm); 60 ASSERT(inherits(vm, info())); 61 } 62 63 // https://tc39.es/ecma402/#sec-Intl.DisplayNames 64 void IntlDisplayNames::initializeDisplayNames(JSGlobalObject* globalObject, JSValue locales, JSValue optionsValue) 65 { 66 VM& vm = globalObject->vm(); 67 auto scope = DECLARE_THROW_SCOPE(vm); 68 69 auto requestedLocales = canonicalizeLocaleList(globalObject, locales); 70 RETURN_IF_EXCEPTION(scope, void()); 71 72 JSObject* options = optionsValue.toObject(globalObject); 73 RETURN_IF_EXCEPTION(scope, void()); 74 75 // Does not set either of "ca" or "nu". 76 // https://tc39.es/proposal-intl-displaynames/#sec-intl-displaynames-constructor 77 ResolveLocaleOptions localeOptions; 78 79 LocaleMatcher localeMatcher = intlOption<LocaleMatcher>(globalObject, options, vm.propertyNames->localeMatcher, { { "lookup"_s, LocaleMatcher::Lookup }, { "best fit"_s, LocaleMatcher::BestFit } }, "localeMatcher must be either \"lookup\" or \"best fit\""_s, LocaleMatcher::BestFit); 80 RETURN_IF_EXCEPTION(scope, void()); 81 82 auto localeData = [](const String&, RelevantExtensionKey) -> Vector<String> { 83 return { }; 84 }; 85 86 auto& availableLocales = intlDisplayNamesAvailableLocales(); 87 auto resolved = resolveLocale(globalObject, availableLocales, requestedLocales, localeMatcher, localeOptions, { }, localeData); 88 89 m_locale = resolved.locale; 90 if (m_locale.isEmpty()) { 91 throwTypeError(globalObject, scope, "failed to initialize DisplayNames due to invalid locale"_s); 92 return; 93 } 94 95 m_style = intlOption<Style>(globalObject, options, vm.propertyNames->style, { { "narrow"_s, Style::Narrow }, { "short"_s, Style::Short }, { "long"_s, Style::Long } }, "style must be either \"narrow\", \"short\", or \"long\""_s, Style::Long); 96 RETURN_IF_EXCEPTION(scope, void()); 97 98 auto type = intlOption<Optional<Type>>(globalObject, options, vm.propertyNames->type, { { "language"_s, Type::Language }, { "region"_s, Type::Region }, { "script"_s, Type::Script }, { "currency"_s, Type::Currency } }, "type must be either \"language\", \"region\", \"script\", or \"currency\""_s, WTF::nullopt); 99 RETURN_IF_EXCEPTION(scope, void()); 100 if (!type) { 101 throwTypeError(globalObject, scope, "type must not be undefined"_s); 102 return; 103 } 104 m_type = type.value(); 105 106 m_fallback = intlOption<Fallback>(globalObject, options, Identifier::fromString(vm, "fallback"), { { "code"_s, Fallback::Code }, { "none"_s, Fallback::None } }, "fallback must be either \"code\" or \"none\""_s, Fallback::Code); 107 RETURN_IF_EXCEPTION(scope, void()); 108 109 #if HAVE(ICU_U_LOCALE_DISPLAY_NAMES) 110 UErrorCode status = U_ZERO_ERROR; 111 112 UDisplayContext contexts[] = { 113 // en_GB displays as 'English (United Kingdom)' (Standard Names) or 'British English' (Dialect Names). 114 // We use Dialect Names here, aligned to the examples in the spec draft and V8's behavior. 115 // https://github.com/tc39/proposal-intl-displaynames#language-display-names 116 UDISPCTX_DIALECT_NAMES, 117 118 // Capitailization mode can be picked from several options. Possibly either UDISPCTX_CAPITALIZATION_NONE or UDISPCTX_CAPITALIZATION_FOR_STANDALONE is 119 // preferable in Intl.DisplayNames. We use UDISPCTX_CAPITALIZATION_FOR_STANDALONE because it makes standalone date format better (fr "Juillet 2008" in ICU test suites), 120 // and DisplayNames will support date formats too. 121 UDISPCTX_CAPITALIZATION_FOR_STANDALONE, 122 123 // Narrow becomes UDISPCTX_LENGTH_SHORT. But in currency case, we handle differently instead of using ULocaleDisplayNames. 124 m_style == Style::Long ? UDISPCTX_LENGTH_FULL : UDISPCTX_LENGTH_SHORT, 125 126 // Always disable ICU SUBSTITUTE since it does not match against what the spec defines. ICU has some special substitute rules, for example, language "en-AA" 127 // returns "English (AA)" (while AA country code is not defined), but we would like to return either input value or undefined, so we do not want to have ICU substitute rules. 128 // Note that this is effective after ICU 65. 129 // https://github.com/unicode-org/icu/commit/53dd621e3a5cff3b78b557c405f1b1d6f125b468 130 UDISPCTX_NO_SUBSTITUTE, 131 }; 132 m_localeCString = m_locale.utf8(); 133 m_displayNames = std::unique_ptr<ULocaleDisplayNames, ULocaleDisplayNamesDeleter>(uldn_openForContext(m_localeCString.data(), contexts, WTF_ARRAY_LENGTH(contexts), &status)); 134 if (U_FAILURE(status)) { 135 throwTypeError(globalObject, scope, "failed to initialize DisplayNames"_s); 136 return; 137 } 138 #else 139 throwTypeError(globalObject, scope, "Failed to initialize Intl.DisplayNames since used feature is not supported in the linked ICU version"_s); 140 return; 141 #endif 142 } 143 144 // https://tc39.es/proposal-intl-displaynames/#sec-Intl.DisplayNames.prototype.of 145 JSValue IntlDisplayNames::of(JSGlobalObject* globalObject, JSValue codeValue) const 146 { 147 148 VM& vm = globalObject->vm(); 149 auto scope = DECLARE_THROW_SCOPE(vm); 150 151 #if HAVE(ICU_U_LOCALE_DISPLAY_NAMES) 152 ASSERT(m_displayNames); 153 auto code = codeValue.toWTFString(globalObject); 154 RETURN_IF_EXCEPTION(scope, { }); 155 156 Vector<UChar, 32> buffer; 157 UErrorCode status = U_ZERO_ERROR; 158 159 if (m_type == Type::Currency) { 160 // We do not use uldn_keyValueDisplayName + "currency". This is because of the following reasons. 161 // 1. ICU does not respect UDISPCTX_LENGTH_FULL / UDISPCTX_LENGTH_SHORT in its implementation. 162 // 2. There is no way to set "narrow" style in ULocaleDisplayNames while currency have "narrow" symbol style. 163 164 // CanonicalCodeForDisplayNames 165 // https://tc39.es/proposal-intl-displaynames/#sec-canonicalcodefordisplaynames 166 // 5. If ! IsWellFormedCurrencyCode(code) is false, throw a RangeError exception. 167 if (!isWellFormedCurrencyCode(code)) { 168 throwRangeError(globalObject, scope, "argument is not a well-formed currency code"_s); 169 return { }; 170 } 171 ASSERT(code.isAllASCII()); 172 173 UCurrNameStyle style = UCURR_LONG_NAME; 174 switch (m_style) { 175 case Style::Long: 176 style = UCURR_LONG_NAME; 177 break; 178 case Style::Short: 179 style = UCURR_SYMBOL_NAME; 180 break; 181 case Style::Narrow: 182 style = UCURR_NARROW_SYMBOL_NAME; 183 break; 184 } 185 186 // 6. Let code be the result of mapping code to upper case as described in 6.1. 187 const UChar currency[4] = { 188 toASCIIUpper(code[0]), 189 toASCIIUpper(code[1]), 190 toASCIIUpper(code[2]), 191 u'\0' 192 }; 193 // The result of ucurr_getName is static string so that we do not need to free the result. 194 int32_t length = 0; 195 UBool isChoiceFormat = false; // We need to pass this, otherwise, we will see crash in ICU 64. 196 const UChar* result = ucurr_getName(currency, m_localeCString.data(), style, &isChoiceFormat, &length, &status); 197 if (U_FAILURE(status)) 198 return throwTypeError(globalObject, scope, "Failed to query a display name."_s); 199 // ucurr_getName returns U_USING_DEFAULT_WARNING if the display-name is not found. But U_USING_DEFAULT_WARNING is returned even if 200 // narrow and short results are the same: narrow "USD" is "$" with U_USING_DEFAULT_WARNING since short "USD" is also "$". We need to check 201 // result == currency to check whether ICU actually failed to find the corresponding display-name. This pointer comparison is ensured by 202 // ICU API document. 203 // > Returns pointer to display string of 'len' UChars. If the resource data contains no entry for 'currency', then 'currency' itself is returned. 204 if (status == U_USING_DEFAULT_WARNING && result == currency) 205 return (m_fallback == Fallback::None) ? jsUndefined() : codeValue; 206 return jsString(vm, String(result, length)); 207 } 208 209 // https://tc39.es/proposal-intl-displaynames/#sec-canonicalcodefordisplaynames 210 auto canonicalizeCodeForDisplayNames = [](Type type, const String& code) -> CString { 211 ASSERT(code.isAllASCII()); 212 auto result = code.ascii(); 213 char* mutableData = result.mutableData(); 214 switch (type) { 215 case Type::Language: { 216 // Let code be the result of mapping code to lower case as described in 6.1. 217 for (unsigned index = 0; index < result.length(); ++index) 218 mutableData[index] = toASCIILower(mutableData[index]); 219 break; 220 } 221 case Type::Region: { 222 // Let code be the result of mapping code to upper case as described in 6.1. 223 for (unsigned index = 0; index < result.length(); ++index) 224 mutableData[index] = toASCIIUpper(mutableData[index]); 225 break; 226 } 227 case Type::Script: { 228 // Let code be the result of mapping the first character in code to upper case, and mapping the second, third and fourth character in code to lower case, as described in 6.1. 229 if (result.length() >= 1) 230 mutableData[0] = toASCIIUpper(mutableData[0]); 231 for (unsigned index = 1; index < result.length(); ++index) 232 mutableData[index] = toASCIILower(mutableData[index]); 233 break; 234 } 235 case Type::Currency: 236 ASSERT_NOT_REACHED(); 237 break; 238 } 239 return result; 240 }; 241 242 switch (m_type) { 243 case Type::Language: { 244 // If code does not matches the unicode_language_id production, throw a RangeError exception 245 if (!isUnicodeLanguageId(code)) { 246 throwRangeError(globalObject, scope, "argument is not a language id"_s); 247 return { }; 248 } 249 auto language = canonicalizeCodeForDisplayNames(m_type, code); 250 // Do not use uldn_languageDisplayName since it is not expected one for this "language" type. It returns "en-US" for "en-US" code, instead of "American English". 251 status = callBufferProducingFunction(uldn_localeDisplayName, m_displayNames.get(), language.data(), buffer); 252 break; 253 } 254 case Type::Region: { 255 // If code does not matches the unicode_region_subtag production, throw a RangeError exception 256 if (!isUnicodeRegionSubtag(code)) { 257 throwRangeError(globalObject, scope, "argument is not a region subtag"_s); 258 return { }; 259 } 260 auto region = canonicalizeCodeForDisplayNames(m_type, code); 261 status = callBufferProducingFunction(uldn_regionDisplayName, m_displayNames.get(), region.data(), buffer); 262 break; 263 } 264 case Type::Script: { 265 // If code does not matches the unicode_script_subtag production, throw a RangeError exception 266 if (!isUnicodeScriptSubtag(code)) { 267 throwRangeError(globalObject, scope, "argument is not a script subtag"_s); 268 return { }; 269 } 270 auto script = canonicalizeCodeForDisplayNames(m_type, code); 271 status = callBufferProducingFunction(uldn_scriptDisplayName, m_displayNames.get(), script.data(), buffer); 272 break; 273 } 274 case Type::Currency: 275 ASSERT_NOT_REACHED(); 276 break; 277 } 278 if (U_FAILURE(status)) { 279 // uldn_localeDisplayName, uldn_regionDisplayName, and uldn_scriptDisplayName return U_ILLEGAL_ARGUMENT_ERROR if the display-name is not found. 280 // We should return undefined if fallback is "none". Otherwise, we should return input value. 281 if (status == U_ILLEGAL_ARGUMENT_ERROR) 282 return (m_fallback == Fallback::None) ? jsUndefined() : codeValue; 283 return throwTypeError(globalObject, scope, "Failed to query a display name."_s); 284 } 285 return jsString(vm, String(buffer)); 286 #else 287 UNUSED_PARAM(codeValue); 288 throwTypeError(globalObject, scope, "Failed to initialize Intl.DisplayNames since used feature is not supported in the linked ICU version"_s); 289 return { }; 290 #endif 291 } 292 293 // https://tc39.es/proposal-intl-displaynames/#sec-Intl.DisplayNames.prototype.resolvedOptions 294 JSObject* IntlDisplayNames::resolvedOptions(JSGlobalObject* globalObject) const 295 { 296 VM& vm = globalObject->vm(); 297 JSObject* options = constructEmptyObject(globalObject); 298 options->putDirect(vm, vm.propertyNames->locale, jsString(vm, m_locale)); 299 options->putDirect(vm, vm.propertyNames->style, jsNontrivialString(vm, styleString(m_style))); 300 options->putDirect(vm, vm.propertyNames->type, jsNontrivialString(vm, typeString(m_type))); 301 options->putDirect(vm, Identifier::fromString(vm, "fallback"), jsNontrivialString(vm, fallbackString(m_fallback))); 302 return options; 303 } 304 305 ASCIILiteral IntlDisplayNames::styleString(Style style) 306 { 307 switch (style) { 308 case Style::Narrow: 309 return "narrow"_s; 310 case Style::Short: 311 return "short"_s; 312 case Style::Long: 313 return "long"_s; 314 } 315 ASSERT_NOT_REACHED(); 316 return ASCIILiteral::null(); 317 } 318 319 ASCIILiteral IntlDisplayNames::typeString(Type type) 320 { 321 switch (type) { 322 case Type::Language: 323 return "language"_s; 324 case Type::Region: 325 return "region"_s; 326 case Type::Script: 327 return "script"_s; 328 case Type::Currency: 329 return "currency"_s; 330 } 331 ASSERT_NOT_REACHED(); 332 return ASCIILiteral::null(); 333 } 334 335 ASCIILiteral IntlDisplayNames::fallbackString(Fallback fallback) 336 { 337 switch (fallback) { 338 case Fallback::Code: 339 return "code"_s; 340 case Fallback::None: 341 return "none"_s; 342 } 343 ASSERT_NOT_REACHED(); 344 return ASCIILiteral::null(); 345 } 346 347 } // namespace JSC