/ runtime / IntlDisplayNames.cpp
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