1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * vim: set ts=8 sts=4 et sw=4 tw=99:
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 /* Implementation of the Intl object and its non-constructor properties. */
8
9 #include "builtin/intl/IntlObject.h"
10
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Likely.h"
13 #include "mozilla/Range.h"
14
15 #include "jsapi.h"
16
17 #include "builtin/intl/Collator.h"
18 #include "builtin/intl/CommonFunctions.h"
19 #include "builtin/intl/DateTimeFormat.h"
20 #include "builtin/intl/ICUStubs.h"
21 #include "builtin/intl/NumberFormat.h"
22 #include "builtin/intl/PluralRules.h"
23 #include "builtin/intl/ScopedICUObject.h"
24 #include "js/Class.h"
25 #include "vm/GlobalObject.h"
26 #include "vm/JSContext.h"
27 #include "vm/JSObject.h"
28 #include "vm/StringType.h"
29
30 #include "vm/JSObject-inl.h"
31
32 using namespace js;
33
34 using mozilla::Range;
35 using mozilla::RangedPtr;
36
37 using js::intl::CallICU;
38 using js::intl::DateTimeFormatOptions;
39 using js::intl::IcuLocale;
40
41 /******************** Intl ********************/
42
intl_GetCalendarInfo(JSContext * cx,unsigned argc,Value * vp)43 bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) {
44 CallArgs args = CallArgsFromVp(argc, vp);
45 MOZ_ASSERT(args.length() == 1);
46
47 JSAutoByteString locale(cx, args[0].toString());
48 if (!locale) return false;
49
50 UErrorCode status = U_ZERO_ERROR;
51 const UChar* uTimeZone = nullptr;
52 int32_t uTimeZoneLength = 0;
53 UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.ptr(),
54 UCAL_DEFAULT, &status);
55 if (U_FAILURE(status)) {
56 intl::ReportInternalError(cx);
57 return false;
58 }
59 ScopedICUObject<UCalendar, ucal_close> toClose(cal);
60
61 RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx));
62 if (!info) return false;
63
64 RootedValue v(cx);
65 int32_t firstDayOfWeek = ucal_getAttribute(cal, UCAL_FIRST_DAY_OF_WEEK);
66 v.setInt32(firstDayOfWeek);
67
68 if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v))
69 return false;
70
71 int32_t minDays = ucal_getAttribute(cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK);
72 v.setInt32(minDays);
73 if (!DefineDataProperty(cx, info, cx->names().minDays, v)) return false;
74
75 UCalendarWeekdayType prevDayType =
76 ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status);
77 if (U_FAILURE(status)) {
78 intl::ReportInternalError(cx);
79 return false;
80 }
81
82 RootedValue weekendStart(cx), weekendEnd(cx);
83
84 for (int i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) {
85 UCalendarDaysOfWeek dayOfWeek = static_cast<UCalendarDaysOfWeek>(i);
86 UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status);
87 if (U_FAILURE(status)) {
88 intl::ReportInternalError(cx);
89 return false;
90 }
91
92 if (prevDayType != type) {
93 switch (type) {
94 case UCAL_WEEKDAY:
95 // If the first Weekday after Weekend is Sunday (1),
96 // then the last Weekend day is Saturday (7).
97 // Otherwise we'll just take the previous days number.
98 weekendEnd.setInt32(i == 1 ? 7 : i - 1);
99 break;
100 case UCAL_WEEKEND:
101 weekendStart.setInt32(i);
102 break;
103 case UCAL_WEEKEND_ONSET:
104 case UCAL_WEEKEND_CEASE:
105 // At the time this code was added, ICU apparently never behaves this
106 // way, so just throw, so that users will report a bug and we can
107 // decide what to do.
108 intl::ReportInternalError(cx);
109 return false;
110 default:
111 break;
112 }
113 }
114
115 prevDayType = type;
116 }
117
118 MOZ_ASSERT(weekendStart.isInt32());
119 MOZ_ASSERT(weekendEnd.isInt32());
120
121 if (!DefineDataProperty(cx, info, cx->names().weekendStart, weekendStart))
122 return false;
123
124 if (!DefineDataProperty(cx, info, cx->names().weekendEnd, weekendEnd))
125 return false;
126
127 args.rval().setObject(*info);
128 return true;
129 }
130
ReportBadKey(JSContext * cx,const Range<const JS::Latin1Char> & range)131 static void ReportBadKey(JSContext* cx,
132 const Range<const JS::Latin1Char>& range) {
133 JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
134 range.begin().get());
135 }
136
ReportBadKey(JSContext * cx,const Range<const char16_t> & range)137 static void ReportBadKey(JSContext* cx, const Range<const char16_t>& range) {
138 JS_ReportErrorNumberUC(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
139 range.begin().get());
140 }
141
142 template <typename ConstChar>
MatchPart(RangedPtr<ConstChar> iter,const RangedPtr<ConstChar> end,const char * part,size_t partlen)143 static bool MatchPart(RangedPtr<ConstChar> iter, const RangedPtr<ConstChar> end,
144 const char* part, size_t partlen) {
145 for (size_t i = 0; i < partlen; iter++, i++) {
146 if (iter == end || *iter != part[i]) return false;
147 }
148
149 return true;
150 }
151
152 template <typename ConstChar, size_t N>
MatchPart(RangedPtr<ConstChar> * iter,const RangedPtr<ConstChar> end,const char (& part)[N])153 inline bool MatchPart(RangedPtr<ConstChar>* iter,
154 const RangedPtr<ConstChar> end, const char (&part)[N]) {
155 if (!MatchPart(*iter, end, part, N - 1)) return false;
156
157 *iter += N - 1;
158 return true;
159 }
160
161 enum class DisplayNameStyle {
162 Narrow,
163 Short,
164 Long,
165 };
166
167 template <typename ConstChar>
ComputeSingleDisplayName(JSContext * cx,UDateFormat * fmt,UDateTimePatternGenerator * dtpg,DisplayNameStyle style,const Range<ConstChar> & pattern)168 static JSString* ComputeSingleDisplayName(JSContext* cx, UDateFormat* fmt,
169 UDateTimePatternGenerator* dtpg,
170 DisplayNameStyle style,
171 const Range<ConstChar>& pattern) {
172 RangedPtr<ConstChar> iter = pattern.begin();
173 const RangedPtr<ConstChar> end = pattern.end();
174
175 auto MatchSlash = [cx, pattern, &iter, end]() {
176 if (MOZ_LIKELY(iter != end && *iter == '/')) {
177 iter++;
178 return true;
179 }
180
181 ReportBadKey(cx, pattern);
182 return false;
183 };
184
185 if (!MatchPart(&iter, end, "dates")) {
186 ReportBadKey(cx, pattern);
187 return nullptr;
188 }
189
190 if (!MatchSlash()) return nullptr;
191
192 if (MatchPart(&iter, end, "fields")) {
193 if (!MatchSlash()) return nullptr;
194
195 UDateTimePatternField fieldType;
196
197 if (MatchPart(&iter, end, "year")) {
198 fieldType = UDATPG_YEAR_FIELD;
199 } else if (MatchPart(&iter, end, "month")) {
200 fieldType = UDATPG_MONTH_FIELD;
201 } else if (MatchPart(&iter, end, "week")) {
202 fieldType = UDATPG_WEEK_OF_YEAR_FIELD;
203 } else if (MatchPart(&iter, end, "day")) {
204 fieldType = UDATPG_DAY_FIELD;
205 } else {
206 ReportBadKey(cx, pattern);
207 return nullptr;
208 }
209
210 // This part must be the final part with no trailing data.
211 if (iter != end) {
212 ReportBadKey(cx, pattern);
213 return nullptr;
214 }
215
216 int32_t resultSize;
217 const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize);
218 MOZ_ASSERT(resultSize >= 0);
219
220 return NewStringCopyN<CanGC>(cx, value, size_t(resultSize));
221 }
222
223 if (MatchPart(&iter, end, "gregorian")) {
224 if (!MatchSlash()) return nullptr;
225
226 UDateFormatSymbolType symbolType;
227 int32_t index;
228
229 if (MatchPart(&iter, end, "months")) {
230 if (!MatchSlash()) return nullptr;
231
232 switch (style) {
233 case DisplayNameStyle::Narrow:
234 symbolType = UDAT_STANDALONE_NARROW_MONTHS;
235 break;
236
237 case DisplayNameStyle::Short:
238 symbolType = UDAT_STANDALONE_SHORT_MONTHS;
239 break;
240
241 case DisplayNameStyle::Long:
242 symbolType = UDAT_STANDALONE_MONTHS;
243 break;
244 }
245
246 if (MatchPart(&iter, end, "january")) {
247 index = UCAL_JANUARY;
248 } else if (MatchPart(&iter, end, "february")) {
249 index = UCAL_FEBRUARY;
250 } else if (MatchPart(&iter, end, "march")) {
251 index = UCAL_MARCH;
252 } else if (MatchPart(&iter, end, "april")) {
253 index = UCAL_APRIL;
254 } else if (MatchPart(&iter, end, "may")) {
255 index = UCAL_MAY;
256 } else if (MatchPart(&iter, end, "june")) {
257 index = UCAL_JUNE;
258 } else if (MatchPart(&iter, end, "july")) {
259 index = UCAL_JULY;
260 } else if (MatchPart(&iter, end, "august")) {
261 index = UCAL_AUGUST;
262 } else if (MatchPart(&iter, end, "september")) {
263 index = UCAL_SEPTEMBER;
264 } else if (MatchPart(&iter, end, "october")) {
265 index = UCAL_OCTOBER;
266 } else if (MatchPart(&iter, end, "november")) {
267 index = UCAL_NOVEMBER;
268 } else if (MatchPart(&iter, end, "december")) {
269 index = UCAL_DECEMBER;
270 } else {
271 ReportBadKey(cx, pattern);
272 return nullptr;
273 }
274 } else if (MatchPart(&iter, end, "weekdays")) {
275 if (!MatchSlash()) return nullptr;
276
277 switch (style) {
278 case DisplayNameStyle::Narrow:
279 symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS;
280 break;
281
282 case DisplayNameStyle::Short:
283 symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS;
284 break;
285
286 case DisplayNameStyle::Long:
287 symbolType = UDAT_STANDALONE_WEEKDAYS;
288 break;
289 }
290
291 if (MatchPart(&iter, end, "monday")) {
292 index = UCAL_MONDAY;
293 } else if (MatchPart(&iter, end, "tuesday")) {
294 index = UCAL_TUESDAY;
295 } else if (MatchPart(&iter, end, "wednesday")) {
296 index = UCAL_WEDNESDAY;
297 } else if (MatchPart(&iter, end, "thursday")) {
298 index = UCAL_THURSDAY;
299 } else if (MatchPart(&iter, end, "friday")) {
300 index = UCAL_FRIDAY;
301 } else if (MatchPart(&iter, end, "saturday")) {
302 index = UCAL_SATURDAY;
303 } else if (MatchPart(&iter, end, "sunday")) {
304 index = UCAL_SUNDAY;
305 } else {
306 ReportBadKey(cx, pattern);
307 return nullptr;
308 }
309 } else if (MatchPart(&iter, end, "dayperiods")) {
310 if (!MatchSlash()) return nullptr;
311
312 symbolType = UDAT_AM_PMS;
313
314 if (MatchPart(&iter, end, "am")) {
315 index = UCAL_AM;
316 } else if (MatchPart(&iter, end, "pm")) {
317 index = UCAL_PM;
318 } else {
319 ReportBadKey(cx, pattern);
320 return nullptr;
321 }
322 } else {
323 ReportBadKey(cx, pattern);
324 return nullptr;
325 }
326
327 // This part must be the final part with no trailing data.
328 if (iter != end) {
329 ReportBadKey(cx, pattern);
330 return nullptr;
331 }
332
333 return CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size,
334 UErrorCode* status) {
335 return udat_getSymbols(fmt, symbolType, index, chars, size, status);
336 });
337 }
338
339 ReportBadKey(cx, pattern);
340 return nullptr;
341 }
342
intl_ComputeDisplayNames(JSContext * cx,unsigned argc,Value * vp)343 bool js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) {
344 CallArgs args = CallArgsFromVp(argc, vp);
345 MOZ_ASSERT(args.length() == 3);
346
347 // 1. Assert: locale is a string.
348 RootedString str(cx, args[0].toString());
349 JSAutoByteString locale;
350 if (!locale.encodeUtf8(cx, str)) return false;
351
352 // 2. Assert: style is a string.
353 DisplayNameStyle dnStyle;
354 {
355 JSLinearString* style = args[1].toString()->ensureLinear(cx);
356 if (!style) return false;
357
358 if (StringEqualsAscii(style, "narrow")) {
359 dnStyle = DisplayNameStyle::Narrow;
360 } else if (StringEqualsAscii(style, "short")) {
361 dnStyle = DisplayNameStyle::Short;
362 } else {
363 MOZ_ASSERT(StringEqualsAscii(style, "long"));
364 dnStyle = DisplayNameStyle::Long;
365 }
366 }
367
368 // 3. Assert: keys is an Array.
369 RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>());
370 if (!keys) return false;
371
372 // 4. Let result be ArrayCreate(0).
373 RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length()));
374 if (!result) return false;
375
376 UErrorCode status = U_ZERO_ERROR;
377
378 UDateFormat* fmt =
379 udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(locale.ptr()), nullptr, 0,
380 nullptr, 0, &status);
381 if (U_FAILURE(status)) {
382 intl::ReportInternalError(cx);
383 return false;
384 }
385 ScopedICUObject<UDateFormat, udat_close> datToClose(fmt);
386
387 // UDateTimePatternGenerator will be needed for translations of date and
388 // time fields like "month", "week", "day" etc.
389 UDateTimePatternGenerator* dtpg =
390 udatpg_open(IcuLocale(locale.ptr()), &status);
391 if (U_FAILURE(status)) {
392 intl::ReportInternalError(cx);
393 return false;
394 }
395 ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg);
396
397 // 5. For each element of keys,
398 RootedString keyValStr(cx);
399 RootedValue v(cx);
400 for (uint32_t i = 0; i < keys->length(); i++) {
401 if (!GetElement(cx, keys, keys, i, &v)) return false;
402
403 keyValStr = v.toString();
404
405 AutoStableStringChars stablePatternChars(cx);
406 if (!stablePatternChars.init(cx, keyValStr)) return false;
407
408 // 5.a. Perform an implementation dependent algorithm to map a key to a
409 // corresponding display name.
410 JSString* displayName =
411 stablePatternChars.isLatin1()
412 ? ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle,
413 stablePatternChars.latin1Range())
414 : ComputeSingleDisplayName(cx, fmt, dtpg, dnStyle,
415 stablePatternChars.twoByteRange());
416 if (!displayName) return false;
417
418 // 5.b. Append the result string to result.
419 v.setString(displayName);
420 if (!DefineDataElement(cx, result, i, v)) return false;
421 }
422
423 // 6. Return result.
424 args.rval().setObject(*result);
425 return true;
426 }
427
intl_GetLocaleInfo(JSContext * cx,unsigned argc,Value * vp)428 bool js::intl_GetLocaleInfo(JSContext* cx, unsigned argc, Value* vp) {
429 CallArgs args = CallArgsFromVp(argc, vp);
430 MOZ_ASSERT(args.length() == 1);
431
432 JSAutoByteString locale(cx, args[0].toString());
433 if (!locale) return false;
434
435 RootedObject info(cx, NewBuiltinClassInstance<PlainObject>(cx));
436 if (!info) return false;
437
438 if (!DefineDataProperty(cx, info, cx->names().locale, args[0])) return false;
439
440 bool rtl = uloc_isRightToLeft(IcuLocale(locale.ptr()));
441
442 RootedValue dir(cx, StringValue(rtl ? cx->names().rtl : cx->names().ltr));
443
444 if (!DefineDataProperty(cx, info, cx->names().direction, dir)) return false;
445
446 args.rval().setObject(*info);
447 return true;
448 }
449
450 const Class js::IntlClass = {js_Object_str,
451 JSCLASS_HAS_CACHED_PROTO(JSProto_Intl)};
452
intl_toSource(JSContext * cx,unsigned argc,Value * vp)453 static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) {
454 CallArgs args = CallArgsFromVp(argc, vp);
455 args.rval().setString(cx->names().Intl);
456 return true;
457 }
458
459 static const JSFunctionSpec intl_static_methods[] = {
460 JS_FN(js_toSource_str, intl_toSource, 0, 0),
461 JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0),
462 JS_FS_END};
463
464 /**
465 * Initializes the Intl Object and its standard built-in properties.
466 * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1
467 */
initIntlObject(JSContext * cx,Handle<GlobalObject * > global)468 /* static */ bool GlobalObject::initIntlObject(JSContext* cx,
469 Handle<GlobalObject*> global) {
470 RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global));
471 if (!proto) return false;
472
473 // The |Intl| object is just a plain object with some "static" function
474 // properties and some constructor properties.
475 RootedObject intl(
476 cx, NewObjectWithGivenProto(cx, &IntlClass, proto, SingletonObject));
477 if (!intl) return false;
478
479 // Add the static functions.
480 if (!JS_DefineFunctions(cx, intl, intl_static_methods)) return false;
481
482 // Add the constructor properties, computing and returning the relevant
483 // prototype objects needed below.
484 RootedObject collatorProto(cx, CreateCollatorPrototype(cx, intl, global));
485 if (!collatorProto) return false;
486 RootedObject dateTimeFormatProto(cx), dateTimeFormat(cx);
487 dateTimeFormatProto = CreateDateTimeFormatPrototype(
488 cx, intl, global, &dateTimeFormat, DateTimeFormatOptions::Standard);
489 if (!dateTimeFormatProto) return false;
490 RootedObject numberFormatProto(cx), numberFormat(cx);
491 numberFormatProto =
492 CreateNumberFormatPrototype(cx, intl, global, &numberFormat);
493 if (!numberFormatProto) return false;
494 RootedObject pluralRulesProto(cx,
495 CreatePluralRulesPrototype(cx, intl, global));
496 if (!pluralRulesProto) return false;
497
498 // The |Intl| object is fully set up now, so define the global property.
499 RootedValue intlValue(cx, ObjectValue(*intl));
500 if (!DefineDataProperty(cx, global, cx->names().Intl, intlValue,
501 JSPROP_RESOLVING))
502 return false;
503
504 // Now that the |Intl| object is successfully added, we can OOM-safely fill
505 // in all relevant reserved global slots.
506
507 // Cache the various prototypes, for use in creating instances of these
508 // objects with the proper [[Prototype]] as "the original value of
509 // |Intl.Collator.prototype|" and similar. For builtin classes like
510 // |String.prototype| we have |JSProto_*| that enables
511 // |getPrototype(JSProto_*)|, but that has global-object-property-related
512 // baggage we don't need or want, so we use one-off reserved slots.
513 global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto));
514 global->setReservedSlot(DATE_TIME_FORMAT, ObjectValue(*dateTimeFormat));
515 global->setReservedSlot(DATE_TIME_FORMAT_PROTO,
516 ObjectValue(*dateTimeFormatProto));
517 global->setReservedSlot(NUMBER_FORMAT, ObjectValue(*numberFormat));
518 global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto));
519 global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto));
520
521 // Also cache |Intl| to implement spec language that conditions behavior
522 // based on values being equal to "the standard built-in |Intl| object".
523 // Use |setConstructor| to correspond with |JSProto_Intl|.
524 //
525 // XXX We should possibly do a one-off reserved slot like above.
526 global->setConstructor(JSProto_Intl, ObjectValue(*intl));
527 return true;
528 }
529
InitIntlClass(JSContext * cx,HandleObject obj)530 JSObject* js::InitIntlClass(JSContext* cx, HandleObject obj) {
531 Handle<GlobalObject*> global = obj.as<GlobalObject>();
532 if (!GlobalObject::initIntlObject(cx, global)) return nullptr;
533
534 return &global->getConstructor(JSProto_Intl).toObject();
535 }
536