1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /* Intl.DisplayNames implementation. */
8 
9 #include "builtin/intl/DisplayNames.h"
10 
11 #include "mozilla/ArrayUtils.h"
12 #include "mozilla/Assertions.h"
13 #include "mozilla/Span.h"
14 #include "mozilla/TextUtils.h"
15 
16 #include <algorithm>
17 
18 #include "jsapi.h"
19 #include "jsfriendapi.h"
20 #include "jsnum.h"
21 #include "jspubtd.h"
22 
23 #include "builtin/intl/CommonFunctions.h"
24 #include "builtin/intl/LanguageTag.h"
25 #include "builtin/intl/ScopedICUObject.h"
26 #include "builtin/intl/SharedIntlData.h"
27 #include "gc/AllocKind.h"
28 #include "gc/FreeOp.h"
29 #include "gc/Rooting.h"
30 #include "js/CallArgs.h"
31 #include "js/Class.h"
32 #include "js/GCVector.h"
33 #include "js/PropertyDescriptor.h"
34 #include "js/PropertySpec.h"
35 #include "js/Result.h"
36 #include "js/RootingAPI.h"
37 #include "js/TypeDecls.h"
38 #include "js/Utility.h"
39 #include "unicode/ucal.h"
40 #include "unicode/ucurr.h"
41 #include "unicode/udat.h"
42 #include "unicode/udatpg.h"
43 #include "unicode/udisplaycontext.h"
44 #include "unicode/uldnames.h"
45 #include "unicode/uloc.h"
46 #include "unicode/umachine.h"
47 #include "unicode/utypes.h"
48 #include "vm/GlobalObject.h"
49 #include "vm/JSAtom.h"
50 #include "vm/JSContext.h"
51 #include "vm/JSObject.h"
52 #include "vm/List.h"
53 #include "vm/Printer.h"
54 #include "vm/Runtime.h"
55 #include "vm/SelfHosting.h"
56 #include "vm/Stack.h"
57 #include "vm/StringType.h"
58 
59 #include "vm/JSObject-inl.h"
60 #include "vm/List-inl.h"
61 #include "vm/NativeObject-inl.h"
62 
63 using namespace js;
64 
65 using js::intl::CallICU;
66 using js::intl::IcuLocale;
67 
68 const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */
69                                                   nullptr, /* delProperty */
70                                                   nullptr, /* enumerate */
71                                                   nullptr, /* newEnumerate */
72                                                   nullptr, /* resolve */
73                                                   nullptr, /* mayResolve */
74                                                   DisplayNamesObject::finalize};
75 
76 const JSClass DisplayNamesObject::class_ = {
77     js_Object_str,
78     JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) |
79         JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) |
80         JSCLASS_FOREGROUND_FINALIZE,
81     &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_};
82 
83 const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_;
84 
displayNames_toSource(JSContext * cx,unsigned argc,Value * vp)85 static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) {
86   CallArgs args = CallArgsFromVp(argc, vp);
87   args.rval().setString(cx->names().DisplayNames);
88   return true;
89 }
90 
91 static const JSFunctionSpec displayNames_static_methods[] = {
92     JS_SELF_HOSTED_FN("supportedLocalesOf",
93                       "Intl_DisplayNames_supportedLocalesOf", 1, 0),
94     JS_FS_END};
95 
96 static const JSFunctionSpec displayNames_methods[] = {
97     JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0),
98     JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0,
99                       0),
100     JS_FN(js_toSource_str, displayNames_toSource, 0, 0), JS_FS_END};
101 
102 static const JSPropertySpec displayNames_properties[] = {
103     JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY),
104     JS_PS_END};
105 
106 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp);
107 
108 const ClassSpec DisplayNamesObject::classSpec_ = {
109     GenericCreateConstructor<DisplayNames, 0, gc::AllocKind::FUNCTION>,
110     GenericCreatePrototype<DisplayNamesObject>,
111     displayNames_static_methods,
112     nullptr,
113     displayNames_methods,
114     displayNames_properties,
115     nullptr,
116     ClassSpec::DontDefineConstructor};
117 
118 enum class DisplayNamesOptions {
119   Standard,
120 
121   // Calendar display names are no longer available with the current spec
122   // proposal text, but may be re-enabled in the future. For our internal use
123   // we still need to have them present, so use a feature guard for now.
124   EnableMozExtensions,
125 };
126 
127 /**
128  * Initialize a new Intl.DisplayNames object using the named self-hosted
129  * function.
130  */
InitializeDisplayNamesObject(JSContext * cx,HandleObject obj,HandlePropertyName initializer,HandleValue locales,HandleValue options,DisplayNamesOptions dnoptions)131 static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj,
132                                          HandlePropertyName initializer,
133                                          HandleValue locales,
134                                          HandleValue options,
135                                          DisplayNamesOptions dnoptions) {
136   FixedInvokeArgs<4> args(cx);
137 
138   args[0].setObject(*obj);
139   args[1].set(locales);
140   args[2].set(options);
141   args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions);
142 
143   RootedValue ignored(cx);
144   if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args,
145                               &ignored)) {
146     return false;
147   }
148 
149   MOZ_ASSERT(ignored.isUndefined(),
150              "Unexpected return value from non-legacy Intl object initializer");
151   return true;
152 }
153 
154 /**
155  * Intl.DisplayNames ([ locales [ , options ]])
156  */
DisplayNames(JSContext * cx,const CallArgs & args,DisplayNamesOptions dnoptions)157 static bool DisplayNames(JSContext* cx, const CallArgs& args,
158                          DisplayNamesOptions dnoptions) {
159   // Step 1.
160   if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) {
161     return false;
162   }
163 
164   // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
165   RootedObject proto(cx);
166   if (dnoptions == DisplayNamesOptions::Standard) {
167     if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames,
168                                             &proto)) {
169       return false;
170     }
171   } else {
172     RootedObject newTarget(cx, &args.newTarget().toObject());
173     if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) {
174       return false;
175     }
176   }
177 
178   Rooted<DisplayNamesObject*> displayNames(cx);
179   displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto);
180   if (!displayNames) {
181     return false;
182   }
183 
184   HandleValue locales = args.get(0);
185   HandleValue options = args.get(1);
186 
187   // Steps 3-26.
188   if (!InitializeDisplayNamesObject(cx, displayNames,
189                                     cx->names().InitializeDisplayNames, locales,
190                                     options, dnoptions)) {
191     return false;
192   }
193 
194   // Step 27.
195   args.rval().setObject(*displayNames);
196   return true;
197 }
198 
DisplayNames(JSContext * cx,unsigned argc,Value * vp)199 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) {
200   CallArgs args = CallArgsFromVp(argc, vp);
201   return DisplayNames(cx, args, DisplayNamesOptions::Standard);
202 }
203 
MozDisplayNames(JSContext * cx,unsigned argc,Value * vp)204 static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
205   CallArgs args = CallArgsFromVp(argc, vp);
206   return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions);
207 }
208 
finalize(JSFreeOp * fop,JSObject * obj)209 void js::DisplayNamesObject::finalize(JSFreeOp* fop, JSObject* obj) {
210   MOZ_ASSERT(fop->onMainThread());
211 
212   if (ULocaleDisplayNames* ldn =
213           obj->as<DisplayNamesObject>().getLocaleDisplayNames()) {
214     intl::RemoveICUCellMemory(fop, obj, DisplayNamesObject::EstimatedMemoryUse);
215 
216     uldn_close(ldn);
217   }
218 }
219 
AddDisplayNamesConstructor(JSContext * cx,HandleObject intl)220 bool js::AddDisplayNamesConstructor(JSContext* cx, HandleObject intl) {
221   JSObject* ctor =
222       GlobalObject::getOrCreateConstructor(cx, JSProto_DisplayNames);
223   if (!ctor) {
224     return false;
225   }
226 
227   RootedValue ctorValue(cx, ObjectValue(*ctor));
228   return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0);
229 }
230 
AddMozDisplayNamesConstructor(JSContext * cx,HandleObject intl)231 bool js::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) {
232   RootedObject ctor(cx, GlobalObject::createConstructor(
233                             cx, MozDisplayNames, cx->names().DisplayNames, 0));
234   if (!ctor) {
235     return false;
236   }
237 
238   RootedObject proto(
239       cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
240   if (!proto) {
241     return false;
242   }
243 
244   if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
245     return false;
246   }
247 
248   if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) {
249     return false;
250   }
251 
252   if (!JS_DefineFunctions(cx, proto, displayNames_methods)) {
253     return false;
254   }
255 
256   if (!JS_DefineProperties(cx, proto, displayNames_properties)) {
257     return false;
258   }
259 
260   RootedValue ctorValue(cx, ObjectValue(*ctor));
261   return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0);
262 }
263 
264 enum class DisplayNamesStyle { Long, Short, Narrow };
265 
NewULocaleDisplayNames(JSContext * cx,const char * locale,DisplayNamesStyle displayStyle)266 static ULocaleDisplayNames* NewULocaleDisplayNames(
267     JSContext* cx, const char* locale, DisplayNamesStyle displayStyle) {
268   UErrorCode status = U_ZERO_ERROR;
269 
270   UDisplayContext contexts[] = {
271       // Use the standard names, not the dialect names.
272       // For example "English (GB)" instead of "British English".
273       UDISPCTX_STANDARD_NAMES,
274 
275       // Assume the display names are used in a stand-alone context.
276       UDISPCTX_CAPITALIZATION_FOR_STANDALONE,
277 
278       // Select either the long or short form. There's no separate narrow form
279       // available in ICU, therefore we equate "narrow"/"short" styles here.
280       displayStyle == DisplayNamesStyle::Long ? UDISPCTX_LENGTH_FULL
281                                               : UDISPCTX_LENGTH_SHORT,
282 
283       // Don't apply substitutes, because we need to apply our own fallbacks.
284       UDISPCTX_NO_SUBSTITUTE,
285   };
286 
287   ULocaleDisplayNames* ldn = uldn_openForContext(
288       IcuLocale(locale), contexts, mozilla::ArrayLength(contexts), &status);
289   if (U_FAILURE(status)) {
290     intl::ReportInternalError(cx);
291     return nullptr;
292   }
293   return ldn;
294 }
295 
GetOrCreateLocaleDisplayNames(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,DisplayNamesStyle displayStyle)296 static ULocaleDisplayNames* GetOrCreateLocaleDisplayNames(
297     JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
298     DisplayNamesStyle displayStyle) {
299   // Obtain a cached ULocaleDisplayNames object.
300   ULocaleDisplayNames* ldn = displayNames->getLocaleDisplayNames();
301   if (!ldn) {
302     ldn = NewULocaleDisplayNames(cx, locale, displayStyle);
303     if (!ldn) {
304       return nullptr;
305     }
306     displayNames->setLocaleDisplayNames(ldn);
307 
308     intl::AddICUCellMemory(displayNames,
309                            DisplayNamesObject::EstimatedMemoryUse);
310   }
311   return ldn;
312 }
313 
ReportInvalidOptionError(JSContext * cx,const char * type,HandleString option)314 static void ReportInvalidOptionError(JSContext* cx, const char* type,
315                                      HandleString option) {
316   if (UniqueChars str = QuoteString(cx, option, '"')) {
317     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
318                               JSMSG_INVALID_OPTION_VALUE, type, str.get());
319   }
320 }
321 
ReportInvalidOptionError(JSContext * cx,const char * type,double option)322 static void ReportInvalidOptionError(JSContext* cx, const char* type,
323                                      double option) {
324   ToCStringBuf cbuf;
325   if (const char* str = NumberToCString(cx, &cbuf, option)) {
326     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
327                               JSMSG_INVALID_DIGITS_VALUE, str);
328   }
329 }
330 
GetLanguageDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,DisplayNamesStyle displayStyle,HandleLinearString languageStr)331 static JSString* GetLanguageDisplayName(
332     JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
333     DisplayNamesStyle displayStyle, HandleLinearString languageStr) {
334   bool ok;
335   intl::LanguageTag tag(cx);
336   JS_TRY_VAR_OR_RETURN_NULL(
337       cx, ok, intl::LanguageTagParser::tryParseBaseName(cx, languageStr, tag));
338   if (!ok) {
339     ReportInvalidOptionError(cx, "language", languageStr);
340     return nullptr;
341   }
342 
343   // ICU always canonicalizes the input locale, but since we know that ICU's
344   // canonicalization is incomplete, we need to perform our own canonicalization
345   // to ensure consistent result.
346   // FIXME: spec issue - language tag canonicalisation not performed:
347   // https://github.com/tc39/proposal-intl-displaynames/issues/70
348   if (!tag.canonicalizeBaseName(cx)) {
349     return nullptr;
350   }
351 
352   UniqueChars languageChars = tag.toStringZ(cx);
353   if (!languageChars) {
354     return nullptr;
355   }
356 
357   ULocaleDisplayNames* ldn =
358       GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle);
359   if (!ldn) {
360     return nullptr;
361   }
362 
363   return CallICU(cx, [ldn, &languageChars](UChar* chars, uint32_t size,
364                                            UErrorCode* status) {
365     int32_t res =
366         uldn_localeDisplayName(ldn, languageChars.get(), chars, size, status);
367 
368     // |uldn_localeDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no
369     // display name was found.
370     if (*status == U_ILLEGAL_ARGUMENT_ERROR) {
371       *status = U_ZERO_ERROR;
372       res = 0;
373     }
374     return res;
375   });
376 }
377 
GetScriptDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,DisplayNamesStyle displayStyle,HandleLinearString scriptStr)378 static JSString* GetScriptDisplayName(JSContext* cx,
379                                       Handle<DisplayNamesObject*> displayNames,
380                                       const char* locale,
381                                       DisplayNamesStyle displayStyle,
382                                       HandleLinearString scriptStr) {
383   intl::ScriptSubtag script;
384   if (!intl::ParseStandaloneScriptTag(scriptStr, script)) {
385     ReportInvalidOptionError(cx, "script", scriptStr);
386     return nullptr;
387   }
388 
389   intl::LanguageTag tag(cx);
390   tag.setLanguage("und");
391   tag.setScript(script);
392 
393   // ICU always canonicalizes the input locale, but since we know that ICU's
394   // canonicalization is incomplete, we need to perform our own canonicalization
395   // to ensure consistent result.
396   // FIXME: spec issue - language tag canonicalisation not performed:
397   // https://github.com/tc39/proposal-intl-displaynames/issues/70
398   if (!tag.canonicalizeBaseName(cx)) {
399     return nullptr;
400   }
401   MOZ_ASSERT(tag.script().present());
402 
403   // |uldn_scriptDisplayName| doesn't use the stand-alone form for script
404   // subtags, so we're using |uloc_getDisplayScript| instead. (This only applies
405   // to the long form.)
406   //
407   // ICU bug: https://unicode-org.atlassian.net/browse/ICU-9301
408   if (displayStyle == DisplayNamesStyle::Long) {
409     // |uloc_getDisplayScript| expects a full locale identifier as its input.
410     UniqueChars scriptChars = tag.toStringZ(cx);
411     if (!scriptChars) {
412       return nullptr;
413     }
414 
415     return CallICU(cx, [locale, &scriptChars](UChar* chars, uint32_t size,
416                                               UErrorCode* status) {
417       int32_t res =
418           uloc_getDisplayScript(scriptChars.get(), locale, chars, size, status);
419 
420       // |uloc_getDisplayScript| reports U_USING_DEFAULT_WARNING when no display
421       // name was found.
422       if (*status == U_USING_DEFAULT_WARNING) {
423         *status = U_ZERO_ERROR;
424         res = 0;
425       }
426       return res;
427     });
428   }
429 
430   // Note: ICU requires the script subtag to be in canonical case.
431   const intl::ScriptSubtag& canonicalScript = tag.script();
432 
433   char scriptChars[intl::LanguageTagLimits::ScriptLength + 1];
434   std::copy_n(canonicalScript.span().data(), canonicalScript.length(),
435               scriptChars);
436   scriptChars[canonicalScript.length()] = '\0';
437 
438   ULocaleDisplayNames* ldn =
439       GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle);
440   if (!ldn) {
441     return nullptr;
442   }
443 
444   return CallICU(cx, [ldn, scriptChars](UChar* chars, uint32_t size,
445                                         UErrorCode* status) {
446     int32_t res = uldn_scriptDisplayName(ldn, scriptChars, chars, size, status);
447 
448     // |uldn_scriptDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no display
449     // name was found.
450     if (*status == U_ILLEGAL_ARGUMENT_ERROR) {
451       *status = U_ZERO_ERROR;
452       res = 0;
453     }
454     return res;
455   });
456 }
457 
GetRegionDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,DisplayNamesStyle displayStyle,HandleLinearString regionStr)458 static JSString* GetRegionDisplayName(JSContext* cx,
459                                       Handle<DisplayNamesObject*> displayNames,
460                                       const char* locale,
461                                       DisplayNamesStyle displayStyle,
462                                       HandleLinearString regionStr) {
463   intl::RegionSubtag region;
464   if (!intl::ParseStandaloneRegionTag(regionStr, region)) {
465     ReportInvalidOptionError(cx, "region", regionStr);
466     return nullptr;
467   }
468 
469   intl::LanguageTag tag(cx);
470   tag.setLanguage("und");
471   tag.setRegion(region);
472 
473   // ICU always canonicalizes the input locale, but since we know that ICU's
474   // canonicalization is incomplete, we need to perform our own canonicalization
475   // to ensure consistent result.
476   // FIXME: spec issue - language tag canonicalisation not performed:
477   // https://github.com/tc39/proposal-intl-displaynames/issues/70
478   if (!tag.canonicalizeBaseName(cx)) {
479     return nullptr;
480   }
481   MOZ_ASSERT(tag.region().present());
482 
483   // Note: ICU requires the region subtag to be in canonical case.
484   const intl::RegionSubtag& canonicalRegion = tag.region();
485 
486   char regionChars[intl::LanguageTagLimits::RegionLength + 1];
487   std::copy_n(canonicalRegion.span().data(), canonicalRegion.length(),
488               regionChars);
489   regionChars[canonicalRegion.length()] = '\0';
490 
491   ULocaleDisplayNames* ldn =
492       GetOrCreateLocaleDisplayNames(cx, displayNames, locale, displayStyle);
493   if (!ldn) {
494     return nullptr;
495   }
496 
497   return CallICU(cx, [ldn, regionChars](UChar* chars, uint32_t size,
498                                         UErrorCode* status) {
499     int32_t res = uldn_regionDisplayName(ldn, regionChars, chars, size, status);
500 
501     // |uldn_regionDisplayName| reports U_ILLEGAL_ARGUMENT_ERROR when no display
502     // name was found.
503     if (*status == U_ILLEGAL_ARGUMENT_ERROR) {
504       *status = U_ZERO_ERROR;
505       res = 0;
506     }
507     return res;
508   });
509 }
510 
GetCurrencyDisplayName(JSContext * cx,const char * locale,DisplayNamesStyle displayStyle,HandleLinearString currencyStr)511 static JSString* GetCurrencyDisplayName(JSContext* cx, const char* locale,
512                                         DisplayNamesStyle displayStyle,
513                                         HandleLinearString currencyStr) {
514   // Inlined implementation of `IsWellFormedCurrencyCode ( currency )`.
515   if (currencyStr->length() != 3) {
516     ReportInvalidOptionError(cx, "currency", currencyStr);
517     return nullptr;
518   }
519 
520   char16_t currency[] = {currencyStr->latin1OrTwoByteChar(0),
521                          currencyStr->latin1OrTwoByteChar(1),
522                          currencyStr->latin1OrTwoByteChar(2), '\0'};
523 
524   if (!mozilla::IsAsciiAlpha(currency[0]) ||
525       !mozilla::IsAsciiAlpha(currency[1]) ||
526       !mozilla::IsAsciiAlpha(currency[2])) {
527     ReportInvalidOptionError(cx, "currency", currencyStr);
528     return nullptr;
529   }
530 
531   UCurrNameStyle currencyStyle;
532   switch (displayStyle) {
533     case DisplayNamesStyle::Long:
534       currencyStyle = UCURR_LONG_NAME;
535       break;
536     case DisplayNamesStyle::Short:
537       currencyStyle = UCURR_SYMBOL_NAME;
538       break;
539     case DisplayNamesStyle::Narrow:
540       currencyStyle = UCURR_NARROW_SYMBOL_NAME;
541       break;
542   }
543 
544   int32_t length = 0;
545   UErrorCode status = U_ZERO_ERROR;
546   const char16_t* name =
547       ucurr_getName(currency, locale, currencyStyle, nullptr, &length, &status);
548   if (U_FAILURE(status)) {
549     intl::ReportInternalError(cx);
550     return nullptr;
551   }
552   MOZ_ASSERT(length >= 0);
553 
554   if (status == U_USING_DEFAULT_WARNING) {
555     return cx->emptyString();
556   }
557 
558   return NewStringCopyN<CanGC>(cx, name, size_t(length));
559 }
560 
GetDateTimeDisplayNames(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,HandleLinearString calendar,UDateFormatSymbolType symbolType,mozilla::Span<const int32_t> indices)561 static ListObject* GetDateTimeDisplayNames(
562     JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
563     HandleLinearString calendar, UDateFormatSymbolType symbolType,
564     mozilla::Span<const int32_t> indices) {
565   if (auto* names = displayNames->getDateTimeNames()) {
566     return names;
567   }
568 
569   intl::LanguageTag tag(cx);
570   if (!intl::LanguageTagParser::parse(cx, mozilla::MakeStringSpan(locale),
571                                       tag)) {
572     return nullptr;
573   }
574 
575   JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
576   if (!keywords.emplaceBack("ca", calendar)) {
577     return nullptr;
578   }
579 
580   if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
581     return nullptr;
582   }
583 
584   UniqueChars localeWithCalendar = tag.toStringZ(cx);
585   if (!localeWithCalendar) {
586     return nullptr;
587   }
588 
589   constexpr char16_t* timeZone = nullptr;
590   constexpr int32_t timeZoneLength = 0;
591 
592   constexpr char16_t* pattern = nullptr;
593   constexpr int32_t patternLength = 0;
594 
595   UErrorCode status = U_ZERO_ERROR;
596   UDateFormat* fmt =
597       udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(localeWithCalendar.get()),
598                 timeZone, timeZoneLength, pattern, patternLength, &status);
599   if (U_FAILURE(status)) {
600     intl::ReportInternalError(cx);
601     return nullptr;
602   }
603   ScopedICUObject<UDateFormat, udat_close> datToClose(fmt);
604 
605   Rooted<ListObject*> names(cx, ListObject::create(cx));
606   if (!names) {
607     return nullptr;
608   }
609 
610   RootedValue value(cx);
611   for (uint32_t i = 0; i < indices.size(); i++) {
612     uint32_t index = indices[i];
613     JSString* name =
614         CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size,
615                                              UErrorCode* status) {
616           return udat_getSymbols(fmt, symbolType, index, chars, size, status);
617         });
618     if (!name) {
619       return nullptr;
620     }
621 
622     value.setString(name);
623     if (!names->append(cx, value)) {
624       return nullptr;
625     }
626   }
627 
628   displayNames->setDateTimeNames(names);
629   return names;
630 }
631 
GetWeekdayDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,HandleLinearString calendar,DisplayNamesStyle displayStyle,HandleLinearString code)632 static JSString* GetWeekdayDisplayName(JSContext* cx,
633                                        Handle<DisplayNamesObject*> displayNames,
634                                        const char* locale,
635                                        HandleLinearString calendar,
636                                        DisplayNamesStyle displayStyle,
637                                        HandleLinearString code) {
638   double weekday;
639   if (!StringToNumber(cx, code, &weekday)) {
640     return nullptr;
641   }
642 
643   // Inlined implementation of `IsValidWeekdayCode ( weekday )`.
644   if (!IsInteger(weekday) || weekday < 1 || weekday > 7) {
645     ReportInvalidOptionError(cx, "weekday", weekday);
646     return nullptr;
647   }
648 
649   UDateFormatSymbolType symbolType;
650   switch (displayStyle) {
651     case DisplayNamesStyle::Long:
652       symbolType = UDAT_STANDALONE_WEEKDAYS;
653       break;
654 
655     case DisplayNamesStyle::Short:
656       // ICU "short" is CLDR "abbreviated"; "shorter" is CLDR "short" format.
657       symbolType = UDAT_STANDALONE_SHORTER_WEEKDAYS;
658       break;
659 
660     case DisplayNamesStyle::Narrow:
661       symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS;
662       break;
663   }
664 
665   static constexpr int32_t indices[] = {
666       UCAL_MONDAY, UCAL_TUESDAY,  UCAL_WEDNESDAY, UCAL_THURSDAY,
667       UCAL_FRIDAY, UCAL_SATURDAY, UCAL_SUNDAY};
668 
669   ListObject* names =
670       GetDateTimeDisplayNames(cx, displayNames, locale, calendar, symbolType,
671                               mozilla::MakeSpan(indices));
672   if (!names) {
673     return nullptr;
674   }
675   MOZ_ASSERT(names->length() == mozilla::ArrayLength(indices));
676 
677   return names->get(int32_t(weekday) - 1).toString();
678 }
679 
GetMonthDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,HandleLinearString calendar,DisplayNamesStyle displayStyle,HandleLinearString code)680 static JSString* GetMonthDisplayName(JSContext* cx,
681                                      Handle<DisplayNamesObject*> displayNames,
682                                      const char* locale,
683                                      HandleLinearString calendar,
684                                      DisplayNamesStyle displayStyle,
685                                      HandleLinearString code) {
686   double month;
687   if (!StringToNumber(cx, code, &month)) {
688     return nullptr;
689   }
690 
691   // Inlined implementation of `IsValidMonthCode ( month )`.
692   if (!IsInteger(month) || month < 1 || month > 13) {
693     ReportInvalidOptionError(cx, "month", month);
694     return nullptr;
695   }
696 
697   UDateFormatSymbolType symbolType;
698   switch (displayStyle) {
699     case DisplayNamesStyle::Long:
700       symbolType = UDAT_STANDALONE_MONTHS;
701       break;
702 
703     case DisplayNamesStyle::Short:
704       symbolType = UDAT_STANDALONE_SHORT_MONTHS;
705       break;
706 
707     case DisplayNamesStyle::Narrow:
708       symbolType = UDAT_STANDALONE_NARROW_MONTHS;
709       break;
710   }
711 
712   static constexpr int32_t indices[] = {
713       UCAL_JANUARY,   UCAL_FEBRUARY, UCAL_MARCH,    UCAL_APRIL,
714       UCAL_MAY,       UCAL_JUNE,     UCAL_JULY,     UCAL_AUGUST,
715       UCAL_SEPTEMBER, UCAL_OCTOBER,  UCAL_NOVEMBER, UCAL_DECEMBER,
716       UCAL_UNDECIMBER};
717 
718   ListObject* names =
719       GetDateTimeDisplayNames(cx, displayNames, locale, calendar, symbolType,
720                               mozilla::MakeSpan(indices));
721   if (!names) {
722     return nullptr;
723   }
724   MOZ_ASSERT(names->length() == mozilla::ArrayLength(indices));
725 
726   return names->get(int32_t(month) - 1).toString();
727 }
728 
GetQuarterDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,HandleLinearString calendar,DisplayNamesStyle displayStyle,HandleLinearString code)729 static JSString* GetQuarterDisplayName(JSContext* cx,
730                                        Handle<DisplayNamesObject*> displayNames,
731                                        const char* locale,
732                                        HandleLinearString calendar,
733                                        DisplayNamesStyle displayStyle,
734                                        HandleLinearString code) {
735   double quarter;
736   if (!StringToNumber(cx, code, &quarter)) {
737     return nullptr;
738   }
739 
740   // Inlined implementation of `IsValidQuarterCode ( quarter )`.
741   if (!IsInteger(quarter) || quarter < 1 || quarter > 4) {
742     ReportInvalidOptionError(cx, "quarter", quarter);
743     return nullptr;
744   }
745 
746   UDateFormatSymbolType symbolType;
747   switch (displayStyle) {
748     case DisplayNamesStyle::Long:
749       symbolType = UDAT_STANDALONE_QUARTERS;
750       break;
751 
752     case DisplayNamesStyle::Short:
753     case DisplayNamesStyle::Narrow:
754       // CLDR "narrow" style not supported in ICU.
755       symbolType = UDAT_STANDALONE_SHORT_QUARTERS;
756       break;
757   }
758 
759   // ICU doesn't provide an enum for quarters.
760   static constexpr int32_t indices[] = {0, 1, 2, 3};
761 
762   ListObject* names =
763       GetDateTimeDisplayNames(cx, displayNames, locale, calendar, symbolType,
764                               mozilla::MakeSpan(indices));
765   if (!names) {
766     return nullptr;
767   }
768   MOZ_ASSERT(names->length() == mozilla::ArrayLength(indices));
769 
770   return names->get(int32_t(quarter) - 1).toString();
771 }
772 
GetDayPeriodDisplayName(JSContext * cx,Handle<DisplayNamesObject * > displayNames,const char * locale,HandleLinearString calendar,HandleLinearString dayPeriod)773 static JSString* GetDayPeriodDisplayName(
774     JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale,
775     HandleLinearString calendar, HandleLinearString dayPeriod) {
776   // Inlined implementation of `IsValidDayPeriodCode ( dayperiod )`.
777   uint32_t index;
778   if (StringEqualsLiteral(dayPeriod, "am")) {
779     index = 0;
780   } else if (StringEqualsLiteral(dayPeriod, "pm")) {
781     index = 1;
782   } else {
783     ReportInvalidOptionError(cx, "dayPeriod", dayPeriod);
784     return nullptr;
785   }
786 
787   UDateFormatSymbolType symbolType = UDAT_AM_PMS;
788 
789   static constexpr int32_t indices[] = {UCAL_AM, UCAL_PM};
790 
791   ListObject* names =
792       GetDateTimeDisplayNames(cx, displayNames, locale, calendar, symbolType,
793                               mozilla::MakeSpan(indices));
794   if (!names) {
795     return nullptr;
796   }
797   MOZ_ASSERT(names->length() == mozilla::ArrayLength(indices));
798 
799   return names->get(index).toString();
800 }
801 
GetDateTimeFieldDisplayName(JSContext * cx,const char * locale,DisplayNamesStyle displayStyle,HandleLinearString dateTimeField)802 static JSString* GetDateTimeFieldDisplayName(JSContext* cx, const char* locale,
803                                              DisplayNamesStyle displayStyle,
804                                              HandleLinearString dateTimeField) {
805   // Inlined implementation of `IsValidDateTimeFieldCode ( field )`.
806   UDateTimePatternField field;
807   if (StringEqualsLiteral(dateTimeField, "era")) {
808     field = UDATPG_ERA_FIELD;
809   } else if (StringEqualsLiteral(dateTimeField, "year")) {
810     field = UDATPG_YEAR_FIELD;
811   } else if (StringEqualsLiteral(dateTimeField, "quarter")) {
812     field = UDATPG_QUARTER_FIELD;
813   } else if (StringEqualsLiteral(dateTimeField, "month")) {
814     field = UDATPG_MONTH_FIELD;
815   } else if (StringEqualsLiteral(dateTimeField, "weekOfYear")) {
816     field = UDATPG_WEEK_OF_YEAR_FIELD;
817   } else if (StringEqualsLiteral(dateTimeField, "weekday")) {
818     field = UDATPG_WEEKDAY_FIELD;
819   } else if (StringEqualsLiteral(dateTimeField, "day")) {
820     field = UDATPG_DAY_FIELD;
821   } else if (StringEqualsLiteral(dateTimeField, "dayPeriod")) {
822     field = UDATPG_DAYPERIOD_FIELD;
823   } else if (StringEqualsLiteral(dateTimeField, "hour")) {
824     field = UDATPG_HOUR_FIELD;
825   } else if (StringEqualsLiteral(dateTimeField, "minute")) {
826     field = UDATPG_MINUTE_FIELD;
827   } else if (StringEqualsLiteral(dateTimeField, "second")) {
828     field = UDATPG_SECOND_FIELD;
829   } else if (StringEqualsLiteral(dateTimeField, "timeZoneName")) {
830     field = UDATPG_ZONE_FIELD;
831   } else {
832     ReportInvalidOptionError(cx, "dateTimeField", dateTimeField);
833     return nullptr;
834   }
835 
836   UDateTimePGDisplayWidth width;
837   switch (displayStyle) {
838     case DisplayNamesStyle::Long:
839       width = UDATPG_WIDE;
840       break;
841     case DisplayNamesStyle::Short:
842       width = UDATPG_ABBREVIATED;
843       break;
844     case DisplayNamesStyle::Narrow:
845       width = UDATPG_NARROW;
846       break;
847   }
848 
849   intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
850   UDateTimePatternGenerator* dtpg =
851       sharedIntlData.getDateTimePatternGenerator(cx, locale);
852   if (!dtpg) {
853     return nullptr;
854   }
855 
856   return intl::CallICU(cx, [dtpg, field, width](UChar* chars, uint32_t size,
857                                                 UErrorCode* status) {
858     return udatpg_getFieldDisplayName(dtpg, field, width, chars, size, status);
859   });
860 }
861 
862 /**
863  * intl_ComputeDisplayName(displayNames, locale, calendar, style, type, code)
864  */
intl_ComputeDisplayName(JSContext * cx,unsigned argc,Value * vp)865 bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) {
866   CallArgs args = CallArgsFromVp(argc, vp);
867   MOZ_ASSERT(args.length() == 6);
868 
869   Rooted<DisplayNamesObject*> displayNames(
870       cx, &args[0].toObject().as<DisplayNamesObject>());
871 
872   UniqueChars locale = intl::EncodeLocale(cx, args[1].toString());
873   if (!locale) {
874     return false;
875   }
876 
877   RootedLinearString calendar(cx, args[2].toString()->ensureLinear(cx));
878   if (!calendar) {
879     return false;
880   }
881 
882   RootedLinearString code(cx, args[5].toString()->ensureLinear(cx));
883   if (!code) {
884     return false;
885   }
886 
887   DisplayNamesStyle displayStyle;
888   {
889     JSLinearString* style = args[3].toString()->ensureLinear(cx);
890     if (!style) {
891       return false;
892     }
893 
894     if (StringEqualsLiteral(style, "long")) {
895       displayStyle = DisplayNamesStyle::Long;
896     } else if (StringEqualsLiteral(style, "short")) {
897       displayStyle = DisplayNamesStyle::Short;
898     } else {
899       MOZ_ASSERT(StringEqualsLiteral(style, "narrow"));
900       displayStyle = DisplayNamesStyle::Narrow;
901     }
902   }
903 
904   JSLinearString* type = args[4].toString()->ensureLinear(cx);
905   if (!type) {
906     return false;
907   }
908 
909   JSString* result;
910   if (StringEqualsLiteral(type, "language")) {
911     result = GetLanguageDisplayName(cx, displayNames, locale.get(),
912                                     displayStyle, code);
913   } else if (StringEqualsLiteral(type, "script")) {
914     result = GetScriptDisplayName(cx, displayNames, locale.get(), displayStyle,
915                                   code);
916   } else if (StringEqualsLiteral(type, "region")) {
917     result = GetRegionDisplayName(cx, displayNames, locale.get(), displayStyle,
918                                   code);
919   } else if (StringEqualsLiteral(type, "currency")) {
920     result = GetCurrencyDisplayName(cx, locale.get(), displayStyle, code);
921   } else if (StringEqualsLiteral(type, "weekday")) {
922     result = GetWeekdayDisplayName(cx, displayNames, locale.get(), calendar,
923                                    displayStyle, code);
924   } else if (StringEqualsLiteral(type, "month")) {
925     result = GetMonthDisplayName(cx, displayNames, locale.get(), calendar,
926                                  displayStyle, code);
927   } else if (StringEqualsLiteral(type, "quarter")) {
928     result = GetQuarterDisplayName(cx, displayNames, locale.get(), calendar,
929                                    displayStyle, code);
930   } else if (StringEqualsLiteral(type, "dayPeriod")) {
931     result =
932         GetDayPeriodDisplayName(cx, displayNames, locale.get(), calendar, code);
933   } else {
934     MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField"));
935     result = GetDateTimeFieldDisplayName(cx, locale.get(), displayStyle, code);
936   }
937   if (!result) {
938     return false;
939   }
940 
941   args.rval().setString(result);
942   return true;
943 }
944