Bug 1287677 - Add mozIntl.getDisplayNames API. r?waldo draft
authorZibi Braniecki <gandalf@mozilla.com>
Mon, 28 Nov 2016 12:06:20 -0800
changeset 445150 8ecb6f54624803dbb7e7b5d3d1b263ffb5301dcb
parent 444725 8387a4ada9a5c4cab059d8fafe0f8c933e83c149
child 538454 a91092d210e187f0c84cae0f4b6ac3ad6c9c310f
push id37435
push userzbraniecki@mozilla.com
push dateTue, 29 Nov 2016 07:07:33 +0000
reviewerswaldo
bugs1287677
milestone53.0a1
Bug 1287677 - Add mozIntl.getDisplayNames API. r?waldo MozReview-Commit-ID: GYVlvSv3Yd9
js/src/builtin/Intl.cpp
js/src/builtin/Intl.h
js/src/builtin/Intl.js
js/src/js.msg
js/src/shell/js.cpp
js/src/tests/Intl/getDisplayNames.js
js/src/vm/SelfHosting.cpp
toolkit/components/mozintl/MozIntl.cpp
toolkit/components/mozintl/mozIMozIntl.idl
toolkit/components/mozintl/test/test_mozintl.js
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -99,16 +99,28 @@ Char16ToUChar(const char16_t* chars)
 }
 
 inline UChar*
 Char16ToUChar(char16_t* chars)
 {
     MOZ_CRASH("Char16ToUChar: Intl API disabled");
 }
 
+inline char16_t*
+UCharToChar16(UChar* chars)
+{
+    MOZ_CRASH("UCharToChar16: Intl API disabled");
+}
+
+inline const char16_t*
+UCharToChar16(const UChar* chars)
+{
+    MOZ_CRASH("UCharToChar16: Intl API disabled");
+}
+
 struct UEnumeration;
 
 int32_t
 uenum_count(UEnumeration* en, UErrorCode* status)
 {
     MOZ_CRASH("uenum_count: Intl API disabled");
 }
 
@@ -347,16 +359,37 @@ enum UCalendarDateFields {
     UCAL_EXTENDED_YEAR,
     UCAL_JULIAN_DAY,
     UCAL_MILLISECONDS_IN_DAY,
     UCAL_IS_LEAP_MONTH,
     UCAL_FIELD_COUNT,
     UCAL_DAY_OF_MONTH = UCAL_DATE
 };
 
+enum UCalendarMonths {
+  UCAL_JANUARY,
+  UCAL_FEBRUARY,
+  UCAL_MARCH,
+  UCAL_APRIL,
+  UCAL_MAY,
+  UCAL_JUNE,
+  UCAL_JULY,
+  UCAL_AUGUST,
+  UCAL_SEPTEMBER,
+  UCAL_OCTOBER,
+  UCAL_NOVEMBER,
+  UCAL_DECEMBER,
+  UCAL_UNDECIMBER
+};
+
+enum UCalendarAMPMs {
+  UCAL_AM,
+  UCAL_PM
+};
+
 UCalendar*
 ucal_open(const UChar* zoneID, int32_t len, const char* locale,
           UCalendarType type, UErrorCode* status)
 {
     MOZ_CRASH("ucal_open: Intl API disabled");
 }
 
 const char*
@@ -411,32 +444,47 @@ ucal_getCanonicalTimeZoneID(const UChar*
 }
 
 int32_t
 ucal_getDefaultTimeZone(UChar* result, int32_t resultCapacity, UErrorCode* status)
 {
     MOZ_CRASH("ucal_getDefaultTimeZone: Intl API disabled");
 }
 
+enum UDateTimePatternField {
+    UDATPG_YEAR_FIELD,
+    UDATPG_MONTH_FIELD,
+    UDATPG_WEEK_OF_YEAR_FIELD,
+    UDATPG_DAY_FIELD,
+};
+
 typedef void* UDateTimePatternGenerator;
 
 UDateTimePatternGenerator*
 udatpg_open(const char* locale, UErrorCode* pErrorCode)
 {
     MOZ_CRASH("udatpg_open: Intl API disabled");
 }
 
 int32_t
 udatpg_getBestPattern(UDateTimePatternGenerator* dtpg, const UChar* skeleton,
                       int32_t length, UChar* bestPattern, int32_t capacity,
                       UErrorCode* pErrorCode)
 {
     MOZ_CRASH("udatpg_getBestPattern: Intl API disabled");
 }
 
+static const UChar *
+udatpg_getAppendItemName(const UDateTimePatternGenerator *dtpg,
+                         UDateTimePatternField field,
+                         int32_t *pLength)
+{
+    MOZ_CRASH("udatpg_getAppendItemName: Intl API disabled");
+}
+
 void
 udatpg_close(UDateTimePatternGenerator* dtpg)
 {
     MOZ_CRASH("udatpg_close: Intl API disabled");
 }
 
 typedef void* UCalendar;
 typedef void* UDateFormat;
@@ -479,20 +527,56 @@ enum UDateFormatField {
     UDAT_RELATED_YEAR_FIELD = 34,
     UDAT_AM_PM_MIDNIGHT_NOON_FIELD = 35,
     UDAT_FLEXIBLE_DAY_PERIOD_FIELD = 36,
     UDAT_TIME_SEPARATOR_FIELD = 37,
     UDAT_FIELD_COUNT = 38
 };
 
 enum UDateFormatStyle {
+    UDAT_FULL,
+    UDAT_LONG,
+    UDAT_MEDIUM,
+    UDAT_SHORT,
+    UDAT_DEFAULT = UDAT_MEDIUM,
     UDAT_PATTERN = -2,
     UDAT_IGNORE = UDAT_PATTERN
 };
 
+enum UDateFormatSymbolType {
+    UDAT_ERAS,
+    UDAT_MONTHS,
+    UDAT_SHORT_MONTHS,
+    UDAT_WEEKDAYS,
+    UDAT_SHORT_WEEKDAYS,
+    UDAT_AM_PMS,
+    UDAT_LOCALIZED_CHARS,
+    UDAT_ERA_NAMES,
+    UDAT_NARROW_MONTHS,
+    UDAT_NARROW_WEEKDAYS,
+    UDAT_STANDALONE_MONTHS,
+    UDAT_STANDALONE_SHORT_MONTHS,
+    UDAT_STANDALONE_NARROW_MONTHS,
+    UDAT_STANDALONE_WEEKDAYS,
+    UDAT_STANDALONE_SHORT_WEEKDAYS,
+    UDAT_STANDALONE_NARROW_WEEKDAYS,
+    UDAT_QUARTERS,
+    UDAT_SHORT_QUARTERS,
+    UDAT_STANDALONE_QUARTERS,
+    UDAT_STANDALONE_SHORT_QUARTERS,
+    UDAT_SHORTER_WEEKDAYS,
+    UDAT_STANDALONE_SHORTER_WEEKDAYS,
+    UDAT_CYCLIC_YEARS_WIDE,
+    UDAT_CYCLIC_YEARS_ABBREVIATED,
+    UDAT_CYCLIC_YEARS_NARROW,
+    UDAT_ZODIAC_NAMES_WIDE,
+    UDAT_ZODIAC_NAMES_ABBREVIATED,
+    UDAT_ZODIAC_NAMES_NARROW
+};
+
 int32_t
 udat_countAvailable()
 {
     MOZ_CRASH("udat_countAvailable: Intl API disabled");
 }
 
 const char*
 udat_getAvailable(int32_t localeIndex)
@@ -556,16 +640,23 @@ ufieldpositer_next(UFieldPositionIterato
 void
 udat_close(UDateFormat* format)
 {
     MOZ_CRASH("udat_close: Intl API disabled");
 }
 
 } // anonymous namespace
 
+static int32_t
+udat_getSymbols(const UDateFormat *fmt, UDateFormatSymbolType type, int32_t symbolIndex,
+                UChar *result, int32_t resultLength, UErrorCode *status)
+{
+    MOZ_CRASH("udat_getSymbols: Intl API disabled");
+}
+
 #endif
 
 
 /******************** Common to Intl constructors ********************/
 
 static bool
 IntlInitialize(JSContext* cx, HandleObject obj, Handle<PropertyName*> initializer,
                HandleValue locales, HandleValue options)
@@ -2917,16 +3008,306 @@ js::intl_GetCalendarInfo(JSContext* cx, 
 
     if (!DefineProperty(cx, info, cx->names().weekendEnd, weekendEnd))
         return false;
 
     args.rval().setObject(*info);
     return true;
 }
 
+template<size_t N>
+inline bool
+MatchPart(const char** pattern, const char (&part)[N])
+{
+    if (strncmp(*pattern, part, N - 1))
+        return false;
+
+    *pattern += N - 1;
+    return true;
+}
+
+bool
+js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() == 3);
+    // 1. Assert: locale is a string.
+    MOZ_ASSERT(args[0].isString());
+    // 2. Assert: style is a string.
+    MOZ_ASSERT(args[1].isString());
+    // 3. Assert: keys is an Array.
+    MOZ_ASSERT(args[2].isObject());
+
+    JSAutoByteString locale(cx, args[0].toString());
+    if (!locale)
+        return false;
+
+    JSAutoByteString style(cx, args[1].toString());
+    if (!style)
+        return false;
+
+    RootedArrayObject keys(cx, &args[2].toObject().as<ArrayObject>());
+    if (!keys)
+        return false;
+
+    // 4. Let result be ArrayCreate(0).
+    RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length()));
+    if (!result)
+        return false;
+
+    UErrorCode status = U_ZERO_ERROR;
+
+    UDateFormat* fmt =
+        udat_open(UDAT_DEFAULT, UDAT_DEFAULT, icuLocale(locale.ptr()),
+        nullptr, 0, nullptr, 0, &status);
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+    ScopedICUObject<UDateFormat, udat_close> datToClose(fmt);
+
+    // UDateTimePatternGenerator will be needed for translations of date and
+    // time fields like "month", "week", "day" etc.
+    UDateTimePatternGenerator* dtpg = udatpg_open(icuLocale(locale.ptr()), &status);
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+    ScopedICUObject<UDateTimePatternGenerator, udatpg_close> datPgToClose(dtpg);
+
+    RootedValue keyValue(cx);
+    RootedString keyValStr(cx);
+    RootedValue wordVal(cx);
+    Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
+    if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE))
+        return false;
+
+    // 5. For each element of keys,
+    for (uint32_t i = 0; i < keys->length(); i++) {
+        /**
+         * We iterate over keys array looking for paths that we have code
+         * branches for.
+         *
+         * For any unknown path branch, the wordVal will keep NullValue and
+         * we'll throw at the end.
+         */
+
+        if (!GetElement(cx, keys, keys, i, &keyValue))
+            return false;
+
+        JSAutoByteString pattern;
+        keyValStr = keyValue.toString();
+        if (!pattern.encodeUtf8(cx, keyValStr))
+            return false;
+
+        wordVal.setNull();
+
+        // 5.a. Perform an implementation dependent algorithm to map a key to a
+        //      corresponding display name.
+        const char* pat = pattern.ptr();
+
+        if (!MatchPart(&pat, "dates")) {
+            JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+            return false;
+        }
+
+        if (!MatchPart(&pat, "/")) {
+            JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+            return false;
+        }
+
+        if (MatchPart(&pat, "fields")) {
+            if (!MatchPart(&pat, "/")) {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            UDateTimePatternField fieldType;
+
+            if (MatchPart(&pat, "year")) {
+                fieldType = UDATPG_YEAR_FIELD;
+            } else if (MatchPart(&pat, "month")) {
+                fieldType = UDATPG_MONTH_FIELD;
+            } else if (MatchPart(&pat, "week")) {
+                fieldType = UDATPG_WEEK_OF_YEAR_FIELD;
+            } else if (MatchPart(&pat, "day")) {
+                fieldType = UDATPG_DAY_FIELD;
+            } else {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            // This part must be the final part with no trailing data.
+            if (*pat != '\0') {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            int32_t resultSize;
+
+            const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize);
+            if (U_FAILURE(status)) {
+                JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+                return false;
+            }
+
+            JSString* word = NewStringCopyN<CanGC>(cx, UCharToChar16(value), resultSize);
+            if (!word)
+                return false;
+
+            wordVal.setString(word);
+        } else if (MatchPart(&pat, "gregorian")) {
+            if (!MatchPart(&pat, "/")) {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            UDateFormatSymbolType symbolType;
+            int32_t index;
+
+            if (MatchPart(&pat, "months")) {
+                if (!MatchPart(&pat, "/")) {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+
+                if (equal(style, "narrow")) {
+                    symbolType = UDAT_STANDALONE_NARROW_MONTHS;
+                } else if (equal(style, "short")) {
+                    symbolType = UDAT_STANDALONE_SHORT_MONTHS;
+                } else {
+                    MOZ_ASSERT(equal(style, "long"));
+                    symbolType = UDAT_STANDALONE_MONTHS;
+                }
+
+                if (MatchPart(&pat, "january")) {
+                    index = UCAL_JANUARY;
+                } else if (MatchPart(&pat, "february")) {
+                    index = UCAL_FEBRUARY;
+                } else if (MatchPart(&pat, "march")) {
+                    index = UCAL_MARCH;
+                } else if (MatchPart(&pat, "april")) {
+                    index = UCAL_APRIL;
+                } else if (MatchPart(&pat, "may")) {
+                    index = UCAL_MAY;
+                } else if (MatchPart(&pat, "june")) {
+                    index = UCAL_JUNE;
+                } else if (MatchPart(&pat, "july")) {
+                    index = UCAL_JULY;
+                } else if (MatchPart(&pat, "august")) {
+                    index = UCAL_AUGUST;
+                } else if (MatchPart(&pat, "september")) {
+                    index = UCAL_SEPTEMBER;
+                } else if (MatchPart(&pat, "october")) {
+                    index = UCAL_OCTOBER;
+                } else if (MatchPart(&pat, "november")) {
+                    index = UCAL_NOVEMBER;
+                } else if (MatchPart(&pat, "december")) {
+                    index = UCAL_DECEMBER;
+                } else {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+            } else if (MatchPart(&pat, "weekdays")) {
+                if (!MatchPart(&pat, "/")) {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+
+                if (equal(style, "narrow")) {
+                    symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS;
+                } else if (equal(style, "short")) {
+                    symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS;
+                } else {
+                    MOZ_ASSERT(equal(style, "long"));
+                    symbolType = UDAT_STANDALONE_WEEKDAYS;
+                }
+
+                if (MatchPart(&pat, "monday")) {
+                    index = UCAL_MONDAY;
+                } else if (MatchPart(&pat, "tuesday")) {
+                    index = UCAL_TUESDAY;
+                } else if (MatchPart(&pat, "wednesday")) {
+                    index = UCAL_WEDNESDAY;
+                } else if (MatchPart(&pat, "thursday")) {
+                    index = UCAL_THURSDAY;
+                } else if (MatchPart(&pat, "friday")) {
+                    index = UCAL_FRIDAY;
+                } else if (MatchPart(&pat, "saturday")) {
+                    index = UCAL_SATURDAY;
+                } else if (MatchPart(&pat, "sunday")) {
+                    index = UCAL_SUNDAY;
+                } else {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+            } else if (MatchPart(&pat, "dayperiods")) {
+                if (!MatchPart(&pat, "/")) {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+
+                symbolType = UDAT_AM_PMS;
+
+                if (MatchPart(&pat, "am")) {
+                    index = UCAL_AM;
+                } else if (MatchPart(&pat, "pm")) {
+                    index = UCAL_PM;
+                } else {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                    return false;
+                }
+            } else {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            // This part must be the final part with no trailing data.
+            if (*pat != '\0') {
+                JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+                return false;
+            }
+
+            int32_t resultSize =
+                udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()),
+                                INITIAL_CHAR_BUFFER_SIZE, &status);
+            if (status == U_BUFFER_OVERFLOW_ERROR) {
+                if (!chars.resize(resultSize))
+                    return false;
+                status = U_ZERO_ERROR;
+                udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()),
+                                resultSize, &status);
+            }
+            if (U_FAILURE(status)) {
+                JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+                return false;
+            }
+
+            JSString* word = NewStringCopyN<CanGC>(cx, chars.begin(), resultSize);
+            if (!word)
+                return false;
+
+            wordVal.setString(word);
+        } else {
+            JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr());
+            return false;
+        }
+
+        MOZ_ASSERT(wordVal.isString());
+
+        // 5.b. Append the result string to result.
+        if (!DefineElement(cx, result, i, wordVal))
+            return false;
+    }
+
+    // 6. Return result.
+    args.rval().setObject(*result);
+    return true;
+}
+
 /******************** Intl ********************/
 
 const Class js::IntlClass = {
     js_Object_str,
     JSCLASS_HAS_CACHED_PROTO(JSProto_Intl)
 };
 
 #if JS_HAS_TOSOURCE
--- a/js/src/builtin/Intl.h
+++ b/js/src/builtin/Intl.h
@@ -384,28 +384,82 @@ intl_FormatDateTime(JSContext* cx, unsig
  *     considered the end of a weekend, e.g. 1 for en-US, 1 for en-GB,
  *     1 for bn-IN (note that "weekend" is *not* necessarily two days)
  *
  * NOTE: "calendar" and "locale" properties are *not* added to the object.
  */
 extern MOZ_MUST_USE bool
 intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp);
 
+/**
+ * Returns an Array with CLDR-based fields display names.
+ * The function takes three arguments:
+ *
+ *   locale
+ *     BCP47 compliant locale string
+ *   style
+ *     A string with values: long or short or narrow
+ *   keys
+ *     An array or path-like strings that identify keys to be returned
+ *     At the moment the following types of keys are supported:
+ *
+ *       'dates/fields/{year|month|week|day}'
+ *       'dates/gregorian/months/{january|...|december}'
+ *       'dates/gregorian/weekdays/{sunday|...|saturday}'
+ *       'dates/gregorian/dayperiods/{am|pm}'
+ *
+ * Example:
+ *
+ * let info = intl_ComputeDisplayNames(
+ *   'en-US',
+ *   'long',
+ *   [
+ *     'dates/fields/year',
+ *     'dates/gregorian/months/january',
+ *     'dates/gregorian/weekdays/monday',
+ *     'dates/gregorian/dayperiods/am',
+ *   ]
+ * );
+ *
+ * Returned value:
+ *
+ * [
+ *   'year',
+ *   'January',
+ *   'Monday',
+ *   'AM'
+ * ]
+ */
+extern MOZ_MUST_USE bool
+intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp);
+
 #if ENABLE_INTL_API
 /**
  * Cast char16_t* strings to UChar* strings used by ICU.
  */
 inline const UChar*
 Char16ToUChar(const char16_t* chars)
 {
   return reinterpret_cast<const UChar*>(chars);
 }
 
 inline UChar*
 Char16ToUChar(char16_t* chars)
 {
   return reinterpret_cast<UChar*>(chars);
 }
+
+inline char16_t*
+UCharToChar16(UChar* chars)
+{
+  return reinterpret_cast<char16_t*>(chars);
+}
+
+inline const char16_t*
+UCharToChar16(const UChar* chars)
+{
+  return reinterpret_cast<const char16_t*>(chars);
+}
 #endif // ENABLE_INTL_API
 
 } // namespace js
 
 #endif /* builtin_Intl_h */
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -4,17 +4,18 @@
 
 /* Portions Copyright Norbert Lindenberg 2011-2012. */
 
 /*global JSMSG_INTL_OBJECT_NOT_INITED: false, JSMSG_INVALID_LOCALES_ELEMENT: false,
          JSMSG_INVALID_LANGUAGE_TAG: false, JSMSG_INVALID_LOCALE_MATCHER: false,
          JSMSG_INVALID_OPTION_VALUE: false, JSMSG_INVALID_DIGITS_VALUE: false,
          JSMSG_INTL_OBJECT_REINITED: false, JSMSG_INVALID_CURRENCY_CODE: false,
          JSMSG_UNDEFINED_CURRENCY: false, JSMSG_INVALID_TIME_ZONE: false,
-         JSMSG_DATE_NOT_FINITE: false,
+         JSMSG_DATE_NOT_FINITE: false, JSMSG_INVALID_KEYS_TYPE: false,
+         JSMSG_INVALID_KEY: false,
          intl_Collator_availableLocales: false,
          intl_availableCollations: false,
          intl_CompareStrings: false,
          intl_NumberFormat_availableLocales: false,
          intl_numberingSystem: false,
          intl_FormatNumber: false,
          intl_DateTimeFormat_availableLocales: false,
          intl_availableCalendars: false,
@@ -2999,8 +3000,131 @@ function Intl_getCalendarInfo(locales) {
                           localeData);
 
   const result = intl_GetCalendarInfo(r.locale);
   result.calendar = r.ca;
   result.locale = r.locale;
 
   return result;
 }
+
+/**
+ * This function is a custom method designed after Intl API, but currently
+ * not part of the spec or spec proposal.
+ * We want to use it internally to retrieve translated values from CLDR in
+ * order to ensure they're aligned with what Intl API returns.
+ *
+ * This API may one day be a foundation for an ECMA402 API spec proposal.
+ *
+ * The function takes two arguments - locales which is a list of locale strings
+ * and options which is an object with two optional properties:
+ *
+ *   keys:
+ *     an Array of string values that are paths to individual terms
+ *
+ *   style:
+ *     a String with a value "long", "short" or "narrow"
+ *
+ * It returns an object with properties:
+ *
+ *   locale:
+ *     a negotiated locale string
+ *
+ *   style:
+ *     negotiated style
+ *
+ *   values:
+ *     A key-value pair list of requested keys and corresponding
+ *     translated values
+ *
+ */
+function Intl_getDisplayNames(locales, options) {
+    // 1. Let requestLocales be ? CanonicalizeLocaleList(locales).
+    const requestedLocales = CanonicalizeLocaleList(locales);
+
+    // 2. If options is undefined, then
+    if (options === undefined)
+        // a. Let options be ObjectCreate(%ObjectPrototype%).
+        options = {};
+    // 3. Else,
+    else
+        // a. Let options be ? ToObject(options).
+        options = ToObject(options);
+
+    const DateTimeFormat = dateTimeFormatInternalProperties;
+
+    // 4. Let localeData be %DateTimeFormat%.[[localeData]].
+    const localeData = DateTimeFormat.localeData;
+
+    // 5. Let opt be a new Record.
+    const localeOpt = new Record();
+    // 6. Set localeOpt.[[localeMatcher]] to "best fit".
+    localeOpt.localeMatcher = "best fit";
+
+    // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt,
+    //    %DateTimeFormat%.[[relevantExtensionKeys]], localeData).
+    const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat),
+                          requestedLocales,
+                          localeOpt,
+                          DateTimeFormat.relevantExtensionKeys,
+                          localeData);
+
+    // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long").
+    const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long");
+    // 9. Let keys be ? Get(options, "keys").
+    let keys = options.keys;
+
+    // 10. If keys is undefined,
+    if (keys === undefined) {
+        // a. Let keys be ArrayCreate(0).
+        keys = [];
+    } else if (!IsObject(keys)) {
+        // 11. Else,
+        //   a. If Type(keys) is not Object, throw a TypeError exception.
+        ThrowTypeError(JSMSG_INVALID_KEYS_TYPE);
+    }
+
+    // 12. Let processedKeys be ArrayCreate(0).
+    // (This really should be a List, but we use an Array here in order that
+    // |intl_ComputeDisplayNames| may infallibly access the list's length via
+    // |ArrayObject::length|.)
+    let processedKeys = [];
+    // 13. Let len be ? ToLength(? Get(keys, "length")).
+    let len = ToLength(keys.length);
+    // 14. Let i be 0.
+    // 15. Repeat, while i < len
+    for (let i = 0; i < len; i++) {
+        // a. Let processedKey be ? ToString(? Get(keys, i)).
+        // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey).
+        callFunction(std_Array_push, processedKeys, ToString(keys[i]));
+    }
+
+    // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys).
+    const names = intl_ComputeDisplayNames(r.locale, style, processedKeys);
+
+    // 17. Let values be ObjectCreate(%ObjectPrototype%).
+    const values = {};
+
+    // 18. Set i to 0.
+    // 19. Repeat, while i < len
+    for (let i = 0; i < len; i++) {
+        // a. Let key be ? Get(processedKeys, i).
+        const key = processedKeys[i];
+        // b. Let name be ? Get(names, i).
+        const name = names[i];
+        // c. Assert: Type(name) is string.
+        assert(typeof name === "string", "unexpected non-string value");
+        // d. Assert: the length of name is greater than zero.
+        assert(name.length > 0, "empty string value");
+        // e. Perform ? DefinePropertyOrThrow(values, key, name).
+        _DefineDataProperty(values, key, name);
+    }
+
+    // 20. Let options be ObjectCreate(%ObjectPrototype%).
+    // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]).
+    // 22. Perform ! DefinePropertyOrThrow(result, "style", style).
+    // 23. Perform ! DefinePropertyOrThrow(result, "values", values).
+    const result = { locale: r.locale, style, values };
+
+    // 24. Return result.
+    return result;
+}
+
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -472,16 +472,18 @@ MSG_DEF(JSMSG_TRACELOGGER_ENABLE_FAIL, 1
 
 // Intl
 MSG_DEF(JSMSG_DATE_NOT_FINITE,         0, JSEXN_RANGEERR, "date value is not finite in DateTimeFormat.format()")
 MSG_DEF(JSMSG_INTERNAL_INTL_ERROR,     0, JSEXN_ERR, "internal error while computing Intl data")
 MSG_DEF(JSMSG_INTL_OBJECT_NOT_INITED,  3, JSEXN_TYPEERR, "Intl.{0}.prototype.{1} called on value that's not an object initialized as a {2}")
 MSG_DEF(JSMSG_INTL_OBJECT_REINITED,    0, JSEXN_TYPEERR, "can't initialize object twice as an object of an Intl constructor")
 MSG_DEF(JSMSG_INVALID_CURRENCY_CODE,   1, JSEXN_RANGEERR, "invalid currency code in NumberFormat(): {0}")
 MSG_DEF(JSMSG_INVALID_DIGITS_VALUE,    1, JSEXN_RANGEERR, "invalid digits value: {0}")
+MSG_DEF(JSMSG_INVALID_KEYS_TYPE,       0, JSEXN_TYPEERR, "calendar info keys must be an object or undefined")
+MSG_DEF(JSMSG_INVALID_KEY,             1, JSEXN_RANGEERR, "invalid key: {0}")
 MSG_DEF(JSMSG_INVALID_LANGUAGE_TAG,    1, JSEXN_RANGEERR, "invalid language tag: {0}")
 MSG_DEF(JSMSG_INVALID_LOCALES_ELEMENT, 0, JSEXN_TYPEERR, "invalid element in locales argument")
 MSG_DEF(JSMSG_INVALID_LOCALE_MATCHER,  1, JSEXN_RANGEERR, "invalid locale matcher in supportedLocalesOf(): {0}")
 MSG_DEF(JSMSG_INVALID_OPTION_VALUE,    2, JSEXN_RANGEERR, "invalid value {1} for option {0}")
 MSG_DEF(JSMSG_INVALID_TIME_ZONE,       1, JSEXN_RANGEERR, "invalid time zone in DateTimeFormat(): {0}")
 MSG_DEF(JSMSG_UNDEFINED_CURRENCY,      0, JSEXN_TYPEERR, "undefined currency in NumberFormat() with currency style")
 
 // RegExp
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -902,16 +902,17 @@ AddIntlExtras(JSContext* cx, unsigned ar
     if (!args.get(0).isObject()) {
         JS_ReportErrorASCII(cx, "addIntlExtras must be passed an object");
         return false;
     }
     JS::RootedObject intl(cx, &args[0].toObject());
 
     static const JSFunctionSpec funcs[] = {
         JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0),
+        JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0),
         JS_FS_END
     };
 
     if (!JS_DefineFunctions(cx, intl, funcs))
         return false;
 
     args.rval().setUndefined();
     return true;
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/getDisplayNames.js
@@ -0,0 +1,238 @@
+// |reftest| skip-if(!this.hasOwnProperty('Intl')||!this.hasOwnProperty('addIntlExtras'))
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the getCalendarInfo function with a diverse set of arguments.
+
+/*
+ * Test if getDisplayNames return value matches expected values.
+ */
+function checkDisplayNames(names, expected)
+{
+  assertEq(Object.getPrototypeOf(names), Object.prototype);
+
+  assertEq(names.locale, expected.locale);
+  assertEq(names.style, expected.style);
+
+  const nameValues = names.values;
+  const expectedValues = expected.values;
+
+  const nameValuesKeys = Object.getOwnPropertyNames(nameValues).sort();
+  const expectedValuesKeys = Object.getOwnPropertyNames(expectedValues).sort();
+
+  assertEqArray(nameValuesKeys, expectedValuesKeys);
+
+  for (let key of expectedValuesKeys)
+    assertEq(nameValues[key], expectedValues[key]);
+}
+
+addIntlExtras(Intl);
+
+let gDN = Intl.getDisplayNames;
+
+assertEq(gDN.length, 2);
+
+checkDisplayNames(gDN('en-US', {
+}), {
+  locale: 'en-US',
+  style: 'long',
+  values: {}
+});
+
+checkDisplayNames(gDN('en-US', {
+  keys: [
+    'dates/gregorian/weekdays/wednesday'
+  ],
+  style: 'narrow'
+}), {
+  locale: 'en-US',
+  style: 'narrow',
+  values: {
+    'dates/gregorian/weekdays/wednesday': 'W'
+  }
+});
+
+checkDisplayNames(gDN('en-US', {
+  keys: [
+    'dates/fields/year',
+    'dates/fields/month',
+    'dates/fields/week',
+    'dates/fields/day',
+    'dates/gregorian/months/january',
+    'dates/gregorian/months/february',
+    'dates/gregorian/months/march',
+    'dates/gregorian/weekdays/tuesday'
+  ]
+}), {
+  locale: 'en-US',
+  style: 'long',
+  values: {
+    'dates/fields/year': 'year',
+    'dates/fields/month': 'month',
+    'dates/fields/week': 'week',
+    'dates/fields/day': 'day',
+    'dates/gregorian/months/january': 'January',
+    'dates/gregorian/months/february': 'February',
+    'dates/gregorian/months/march': 'March',
+    'dates/gregorian/weekdays/tuesday': 'Tuesday',
+  }
+});
+
+checkDisplayNames(gDN('fr', {
+  keys: [
+    'dates/fields/year',
+    'dates/fields/day',
+    'dates/gregorian/months/october',
+    'dates/gregorian/weekdays/saturday',
+    'dates/gregorian/dayperiods/pm'
+  ]
+}), {
+  locale: 'fr',
+  style: 'long',
+  values: {
+    'dates/fields/year': 'année',
+    'dates/fields/day': 'jour',
+    'dates/gregorian/months/october': 'octobre',
+    'dates/gregorian/weekdays/saturday': 'samedi',
+    'dates/gregorian/dayperiods/pm': 'PM'
+  }
+});
+
+checkDisplayNames(gDN('it', {
+  style: 'short',
+  keys: [
+    'dates/gregorian/weekdays/thursday',
+    'dates/gregorian/months/august',
+    'dates/gregorian/dayperiods/am',
+    'dates/fields/month',
+  ]
+}), {
+  locale: 'it',
+  style: 'short',
+  values: {
+    'dates/gregorian/weekdays/thursday': 'gio',
+    'dates/gregorian/months/august': 'ago',
+    'dates/gregorian/dayperiods/am': 'AM',
+    'dates/fields/month': 'mese'
+  }
+});
+
+checkDisplayNames(gDN('ar', {
+  style: 'long',
+  keys: [
+    'dates/gregorian/weekdays/thursday',
+    'dates/gregorian/months/august',
+    'dates/gregorian/dayperiods/am',
+    'dates/fields/month',
+  ]
+}), {
+  locale: 'ar',
+  style: 'long',
+  values: {
+    'dates/gregorian/weekdays/thursday': 'الخميس',
+    'dates/gregorian/months/august': 'أغسطس',
+    'dates/gregorian/dayperiods/am': 'ص',
+    'dates/fields/month': 'الشهر'
+  }
+});
+
+/* Invalid input */
+
+assertThrowsInstanceOf(() => {
+  gDN('en-US', {
+    style: '',
+    keys: [
+      'dates/gregorian/weekdays/thursday',
+    ]
+  });
+}, RangeError);
+
+assertThrowsInstanceOf(() => {
+  gDN('en-US', {
+    style: 'bogus',
+    keys: [
+      'dates/gregorian/weekdays/thursday',
+    ]
+  });
+}, RangeError);
+
+assertThrowsInstanceOf(() => {
+  gDN('foo-X', {
+    keys: [
+      'dates/gregorian/weekdays/thursday',
+    ]
+  });
+}, RangeError);
+
+const typeErrorKeys = [
+  null,
+  'string',
+  Symbol.iterator,
+  15,
+  1,
+  3.7,
+  NaN,
+  Infinity
+];
+
+for (let keys of typeErrorKeys) {
+  assertThrowsInstanceOf(() => {
+    gDN('en-US', {
+      keys
+    });
+  }, TypeError);
+}
+
+const rangeErrorKeys = [
+  [''],
+  ['foo'],
+  ['dates/foo'],
+  ['/dates/foo'],
+  ['dates/foo/foo'],
+  ['dates/fields'],
+  ['dates/fields/'],
+  ['dates/fields/foo'],
+  ['dates/fields/foo/month'],
+  ['/dates/foo/faa/bar/baz'],
+  ['dates///bar/baz'],
+  ['dates/gregorian'],
+  ['dates/gregorian/'],
+  ['dates/gregorian/foo'],
+  ['dates/gregorian/months'],
+  ['dates/gregorian/months/foo'],
+  ['dates/gregorian/weekdays'],
+  ['dates/gregorian/weekdays/foo'],
+  ['dates/gregorian/dayperiods'],
+  ['dates/gregorian/dayperiods/foo'],
+  ['dates/gregorian/months/الشهر'],
+  [3],
+  [null],
+  ['d', 'a', 't', 'e', 's'],
+  ['datesEXTRA'],
+  ['dates/fieldsEXTRA'],
+  ['dates/gregorianEXTRA'],
+  ['dates/gregorian/monthsEXTRA'],
+  ['dates/gregorian/weekdaysEXTRA'],
+  ['dates/fields/dayperiods/amEXTRA'],
+  ['dates/gregori\u1161n/months/january'],
+  ["dates/fields/year/"],
+  ["dates/fields/month/"],
+  ["dates/fields/week/"],
+  ["dates/fields/day/"],
+  ["dates/gregorian/months/january/"],
+  ["dates/gregorian/weekdays/saturday/"],
+  ["dates/gregorian/dayperiods/am/"],
+  ["dates/fields/months/january/"],
+];
+
+for (let keys of rangeErrorKeys) {
+  assertThrowsInstanceOf(() => {
+    gDN('en-US', {
+      keys
+    });
+  }, RangeError);
+}
+
+if (typeof reportCompare === 'function')
+    reportCompare(0, 0);
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -2472,16 +2472,17 @@ static const JSFunctionSpec intrinsic_fu
     JS_FN("intl_CompareStrings", intl_CompareStrings, 3,0),
     JS_FN("intl_DateTimeFormat", intl_DateTimeFormat, 2,0),
     JS_FN("intl_DateTimeFormat_availableLocales", intl_DateTimeFormat_availableLocales, 0,0),
     JS_FN("intl_defaultTimeZone", intl_defaultTimeZone, 0,0),
     JS_FN("intl_defaultTimeZoneOffset", intl_defaultTimeZoneOffset, 0,0),
     JS_FN("intl_FormatDateTime", intl_FormatDateTime, 2,0),
     JS_FN("intl_FormatNumber", intl_FormatNumber, 2,0),
     JS_FN("intl_GetCalendarInfo", intl_GetCalendarInfo, 1,0),
+    JS_FN("intl_ComputeDisplayNames", intl_ComputeDisplayNames, 3,0),
     JS_FN("intl_IsValidTimeZoneName", intl_IsValidTimeZoneName, 1,0),
     JS_FN("intl_NumberFormat", intl_NumberFormat, 2,0),
     JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0),
     JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0),
     JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0),
 
     JS_INLINABLE_FN("IsRegExpObject",
                     intrinsic_IsInstanceOfBuiltin<RegExpObject>, 1,0,
--- a/toolkit/components/mozintl/MozIntl.cpp
+++ b/toolkit/components/mozintl/MozIntl.cpp
@@ -39,16 +39,42 @@ MozIntl::AddGetCalendarInfo(JS::Handle<J
 
   if (!JS_DefineFunctions(cx, realIntlObj, funcs)) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+MozIntl::AddGetDisplayNames(JS::Handle<JS::Value> val, JSContext* cx)
+{
+  if (!val.isObject()) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  JS::Rooted<JSObject*> realIntlObj(cx, js::CheckedUnwrap(&val.toObject()));
+  if (!realIntlObj) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  JSAutoCompartment ac(cx, realIntlObj);
+
+  static const JSFunctionSpec funcs[] = {
+    JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0),
+    JS_FS_END
+  };
+
+  if (!JS_DefineFunctions(cx, realIntlObj, funcs)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
 NS_GENERIC_FACTORY_CONSTRUCTOR(MozIntl)
 NS_DEFINE_NAMED_CID(MOZ_MOZINTL_CID);
 
 static const Module::CIDEntry kMozIntlCIDs[] = {
   { &kMOZ_MOZINTL_CID, false, nullptr, MozIntlConstructor },
   { nullptr }
 };
 
--- a/toolkit/components/mozintl/mozIMozIntl.idl
+++ b/toolkit/components/mozintl/mozIMozIntl.idl
@@ -4,9 +4,10 @@
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 [scriptable, uuid(9f9bc42e-54f4-11e6-9aed-4b1429ac0ba0)]
 interface mozIMozIntl : nsISupports
 {
   [implicit_jscontext] void addGetCalendarInfo(in jsval intlObject);
+  [implicit_jscontext] void addGetDisplayNames(in jsval intlObject);
 };
--- a/toolkit/components/mozintl/test/test_mozintl.js
+++ b/toolkit/components/mozintl/test/test_mozintl.js
@@ -2,16 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function run_test() {
   const mozIntl = Components.classes["@mozilla.org/mozintl;1"]
                             .getService(Components.interfaces.mozIMozIntl);
 
   test_this_global(mozIntl);
   test_cross_global(mozIntl);
+  test_methods_presence(mozIntl);
 
   ok(true);
 }
 
 function test_this_global(mozIntl) {
   let x = {};
 
   mozIntl.addGetCalendarInfo(x);
@@ -25,8 +26,21 @@ function test_cross_global(mozIntl) {
 
   mozIntl.addGetCalendarInfo(x);
   var waivedX = Components.utils.waiveXrays(x);
   equal(waivedX.getCalendarInfo instanceof Function, false);
   equal(waivedX.getCalendarInfo instanceof global.Function, true);
   equal(waivedX.getCalendarInfo() instanceof Object, false);
   equal(waivedX.getCalendarInfo() instanceof global.Object, true);
 }
+
+function test_methods_presence(mozIntl) {
+  equal(mozIntl.addGetCalendarInfo instanceof Function, true);
+  equal(mozIntl.addGetDisplayNames instanceof Function, true);
+
+  let x = {};
+
+  mozIntl.addGetCalendarInfo(x);
+  equal(x.getCalendarInfo instanceof Function, true);
+
+  mozIntl.addGetDisplayNames(x);
+  equal(x.getDisplayNames instanceof Function, true);
+}