Bug 1270140 - Add mozIntl.RelativeTimeFormat. draft
authorZibi Braniecki <gandalf@mozilla.com>
Fri, 13 Jan 2017 23:07:02 -0800
changeset 462569 cd2d650bec5f16e698a74f55c655009a94dcaf1e
parent 460918 ac3275723df59db0f09198fdb61b51e7002c391a
child 542450 8c41f649776ef3e915c36134235c2ab3756b7835
push id41813
push userzbraniecki@mozilla.com
push dateTue, 17 Jan 2017 19:44:09 +0000
bugs1270140
milestone53.0a1
Bug 1270140 - Add mozIntl.RelativeTimeFormat. MozReview-Commit-ID: LUuBXEjh8zt
config/external/icu/defs.mozbuild
js/public/Class.h
js/src/builtin/Intl.cpp
js/src/builtin/Intl.h
js/src/builtin/Intl.js
js/src/jsfriendapi.h
js/src/shell/js.cpp
js/src/tests/Intl/RelativeTimeFormat/browser.js
js/src/tests/Intl/RelativeTimeFormat/construct-newtarget.js
js/src/tests/Intl/RelativeTimeFormat/format.js
js/src/tests/Intl/RelativeTimeFormat/relativetimeformat.js
js/src/tests/Intl/RelativeTimeFormat/shell.js
js/src/tests/Intl/RelativeTimeFormat/supportedLocalesOf.js
js/src/vm/CommonPropertyNames.h
js/src/vm/GlobalObject.h
js/src/vm/SelfHosting.cpp
toolkit/components/mozintl/MozIntl.cpp
toolkit/components/mozintl/mozIMozIntl.idl
--- a/config/external/icu/defs.mozbuild
+++ b/config/external/icu/defs.mozbuild
@@ -10,17 +10,16 @@ DEFINES.update(
 
     # Don't include obsolete header files.
     U_NO_DEFAULT_INCLUDE_UTF_HEADERS = 1,
 
     # Remove chunks of the library that we don't need (yet).
     UCONFIG_NO_LEGACY_CONVERSION = True,
     UCONFIG_NO_TRANSLITERATION = True,
     UCONFIG_NO_REGULAR_EXPRESSIONS = True,
-    UCONFIG_NO_BREAK_ITERATION = True,
 
     # We don't need to pass data to and from legacy char* APIs.
     U_CHARSET_IS_UTF8 = True,
 )
 
 if not CONFIG['HAVE_LANGINFO_CODESET']:
     DEFINES['U_HAVE_NL_LANGINFO_CODESET'] = 0
 
--- a/js/public/Class.h
+++ b/js/public/Class.h
@@ -736,17 +736,17 @@ struct JSClass {
 // with the following flags. Failure to use JSCLASS_GLOBAL_FLAGS was
 // previously allowed, but is now an ES5 violation and thus unsupported.
 //
 // JSCLASS_GLOBAL_APPLICATION_SLOTS is the number of slots reserved at
 // the beginning of every global object's slots for use by the
 // application.
 #define JSCLASS_GLOBAL_APPLICATION_SLOTS 5
 #define JSCLASS_GLOBAL_SLOT_COUNT                                             \
-    (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 39)
+    (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 40)
 #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n)                                    \
     (JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n)))
 #define JSCLASS_GLOBAL_FLAGS                                                  \
     JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(0)
 #define JSCLASS_HAS_GLOBAL_FLAG_AND_SLOTS(clasp)                              \
   (((clasp)->flags & JSCLASS_IS_GLOBAL)                                       \
    && JSCLASS_RESERVED_SLOTS(clasp) >= JSCLASS_GLOBAL_SLOT_COUNT)
 
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -27,20 +27,22 @@
 #include "builtin/IntlTimeZoneData.h"
 #include "ds/Sort.h"
 #if ENABLE_INTL_API
 #include "unicode/plurrule.h"
 #include "unicode/ucal.h"
 #include "unicode/ucol.h"
 #include "unicode/udat.h"
 #include "unicode/udatpg.h"
+#include "unicode/udisplaycontext.h"
 #include "unicode/uenum.h"
 #include "unicode/unum.h"
 #include "unicode/unumsys.h"
 #include "unicode/upluralrules.h"
+#include "unicode/ureldatefmt.h"
 #include "unicode/ustring.h"
 #endif
 #include "vm/DateTime.h"
 #include "vm/GlobalObject.h"
 #include "vm/Interpreter.h"
 #include "vm/Stack.h"
 #include "vm/StringBuffer.h"
 #include "vm/Unicode.h"
@@ -3951,16 +3953,426 @@ js::intl_GetPluralCategories(JSContext* 
         if (!DefineElement(cx, res, i++, element))
             return false;
     } while (true);
 
     args.rval().setObject(*res);
     return true;
 }
 
+/**************** RelativeTimeFormat *****************/
+
+static void relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj);
+
+static const uint32_t URELATIVE_TIME_FORMAT_SLOT = 0;
+static const uint32_t RELATIVE_TIME_FORMAT_SLOTS_COUNT = 1;
+
+static const ClassOps RelativeTimeFormatClassOps = {
+    nullptr, /* addProperty */
+    nullptr, /* delProperty */
+    nullptr, /* getProperty */
+    nullptr, /* setProperty */
+    nullptr, /* enumerate */
+    nullptr, /* resolve */
+    nullptr, /* mayResolve */
+    relativeTimeFormat_finalize
+};
+
+static const Class RelativeTimeFormatClass = {
+    js_Object_str,
+    JSCLASS_HAS_RESERVED_SLOTS(RELATIVE_TIME_FORMAT_SLOTS_COUNT) |
+    JSCLASS_FOREGROUND_FINALIZE,
+    &RelativeTimeFormatClassOps
+};
+
+#if JS_HAS_TOSOURCE
+static bool
+relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    args.rval().setString(cx->names().RelativeTimeFormat);
+    return true;
+}
+#endif
+
+static const JSFunctionSpec relativeTimeFormat_static_methods[] = {
+    JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0),
+    JS_FS_END
+};
+
+static const JSFunctionSpec relativeTimeFormat_methods[] = {
+    JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0),
+    JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 1, 0),
+#if JS_HAS_TOSOURCE
+    JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0),
+#endif
+    JS_FS_END
+};
+
+/**
+ * RelativeTimeFormat constructor.
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
+ */
+static bool
+RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
+
+    // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+    RootedObject proto(cx);
+    if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto))
+        return false;
+
+    if (!proto) {
+        proto = cx->global()->getOrCreateRelativeTimeFormatPrototype(cx);
+        if (!proto)
+            return false;
+    }
+
+    RootedObject obj(cx, NewObjectWithGivenProto(cx, &RelativeTimeFormatClass, proto));
+    if (!obj)
+        return false;
+
+    obj->as<NativeObject>().setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr));
+
+    RootedValue locales(cx, args.get(0));
+    RootedValue options(cx, args.get(1));
+
+    // Step 3.
+    if (!IntlInitialize(cx, obj, cx->names().InitializeRelativeTimeFormat, locales, options))
+        return false;
+
+    args.rval().setObject(*obj);
+    return true;
+}
+
+static void
+relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj)
+{
+    MOZ_ASSERT(fop->onMainThread());
+
+    // This is-undefined check shouldn't be necessary, but for internal
+    // brokenness in object allocation code.  For the moment, hack around it by
+    // explicitly guarding against the possibility of the reserved slot not
+    // containing a private.  See bug 949220.
+    const Value& slot = obj->as<NativeObject>().getReservedSlot(UPLURAL_RULES_SLOT);
+    if (!slot.isUndefined()) {
+        if (URelativeDateTimeFormatter* rtf = static_cast<URelativeDateTimeFormatter*>(slot.toPrivate()))
+            ureldatefmt_close(rtf);
+    }
+}
+
+static JSObject*
+CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global)
+{
+    RootedFunction ctor(cx);
+    ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0);
+    if (!ctor)
+        return nullptr;
+
+    RootedNativeObject proto(cx, global->createBlankPrototype(cx, &RelativeTimeFormatClass));
+    if (!proto)
+        return nullptr;
+    MOZ_ASSERT(proto->getReservedSlot(URELATIVE_TIME_FORMAT_SLOT).isUndefined(),
+               "improperly creating RelativeTimeFormat more than once for a single "
+               "global?");
+    proto->setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr));
+
+    if (!LinkConstructorAndPrototype(cx, ctor, proto))
+        return nullptr;
+
+    if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods))
+        return nullptr;
+
+    if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods))
+        return nullptr;
+
+    RootedValue options(cx);
+    if (!CreateDefaultOptions(cx, &options))
+        return nullptr;
+
+    if (!IntlInitialize(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue,
+                        options))
+    {
+        return nullptr;
+    }
+
+    RootedValue ctorValue(cx, ObjectValue(*ctor));
+    if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0))
+        return nullptr;
+
+    return proto;
+}
+
+/* static */ bool
+js::GlobalObject::addRelativeTimeFormatConstructor(JSContext* cx, HandleObject intl)
+{
+    Handle<GlobalObject*> global = cx->global();
+
+    {
+        const HeapSlot& slot = global->getReservedSlotRef(RELATIVE_TIME_FORMAT_PROTO);
+        if (!slot.isUndefined()) {
+            MOZ_ASSERT(slot.isObject());
+            MOZ_ASSERT(slot.toObject().hasClass(&RelativeTimeFormatClass));
+            JS_ReportErrorASCII(cx,
+                                "the RelativeTimeFormat constructor can't be added "
+                                "multiple times in the same global");
+            return false;
+        }
+    }
+
+    JSObject* relativeTimeFormatProto = CreateRelativeTimeFormatPrototype(cx, intl, global);
+    if (!relativeTimeFormatProto)
+        return false;
+
+    global->setReservedSlot(RELATIVE_TIME_FORMAT_PROTO, ObjectValue(*relativeTimeFormatProto));
+    return true;
+}
+
+bool
+js::AddRelativeTimeFormatConstructor(JSContext* cx, JS::Handle<JSObject*> intl)
+{
+    return GlobalObject::addRelativeTimeFormatConstructor(cx, intl);
+}
+
+
+bool
+js::intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() == 0);
+
+    RootedValue result(cx);
+    if (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result))
+        return false;
+    args.rval().set(result);
+    return true;
+}
+
+/**
+ * GetBestMatchUnit calculates the correct number and unit for given number
+ * of milliseconds and unit option.
+ *
+ * We also combine ComputeTimeUnits (1.1.7) into this function.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.6-7
+ */
+URelativeDateTimeUnit
+GetBestMatchUnit(JSContext* cx, double ms, HandleString unitStr, double& v)
+{
+    RootedLinearString unit(cx, unitStr->ensureLinear(cx));
+
+    double msPerSecond = 1000;
+    double msPerMinute = msPerSecond * 60;
+    double msPerHour = msPerMinute * 60;
+    double msPerDay = msPerHour * 24;
+    double msPerWeek = msPerDay * 7;
+    double msPer400Years = msPerDay * 146097;
+
+    double rawYear = ms * 400 / msPer400Years;
+
+    double seconds = round(ms / msPerSecond);
+    double minutes = round(ms / msPerMinute);
+    double hours = round(ms / msPerHour);
+    double days = round(ms / msPerDay);
+    double weeks = round(ms / msPerWeek);
+    double months = round(rawYear * 12);
+    double quarters = round(rawYear * 4);
+    double years = round(rawYear);
+
+    if (StringEqualsAscii(unit, "second")) {
+        v = seconds;
+        return UDAT_REL_UNIT_SECOND;
+    } else if (StringEqualsAscii(unit, "minute")) {
+        v = minutes;
+        return UDAT_REL_UNIT_MINUTE;
+    } else if (StringEqualsAscii(unit, "hour")) {
+        v = hours;
+        return UDAT_REL_UNIT_HOUR;
+    } else if (StringEqualsAscii(unit, "day")) {
+        v = days;
+        return UDAT_REL_UNIT_DAY;
+    } else if (StringEqualsAscii(unit, "week")) {
+        v = weeks;
+        return UDAT_REL_UNIT_WEEK;
+    } else if (StringEqualsAscii(unit, "month")) {
+        v = months;
+        return UDAT_REL_UNIT_MONTH;
+    } else if (StringEqualsAscii(unit, "quarter")) {
+        v = quarters;
+        return UDAT_REL_UNIT_QUARTER;
+    } else if (StringEqualsAscii(unit, "year")) {
+        v = years;
+        return UDAT_REL_UNIT_YEAR;
+    }
+
+    if (abs(seconds) < 60) {
+        v = seconds;
+        return UDAT_REL_UNIT_SECOND;
+    }
+    if (abs(minutes) < 60) {
+        v = minutes;
+        return UDAT_REL_UNIT_MINUTE;
+    }
+    if (abs(hours) < 24) {
+        v = hours;
+        return UDAT_REL_UNIT_HOUR;
+    }
+    if (abs(days) < 7) {
+        v = days;
+        return UDAT_REL_UNIT_DAY;
+    }
+    if (abs(weeks) < 4) {
+        v = weeks;
+        return UDAT_REL_UNIT_WEEK;
+    }
+    if (abs(months) < 11) {
+        v = months;
+        return UDAT_REL_UNIT_MONTH;
+    }
+    v = years;
+    return UDAT_REL_UNIT_YEAR;
+}
+
+bool
+js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() == 2);
+
+    RootedObject relativeTimeFormat(cx, &args[0].toObject());
+
+    RootedObject internals(cx, GetInternals(cx, relativeTimeFormat));
+    if (!internals)
+        return false;
+
+    RootedValue value(cx);
+
+    if (!GetProperty(cx, internals, internals, cx->names().locale, &value))
+        return false;
+    JSAutoByteString locale(cx, value.toString());
+    if (!locale)
+        return false;
+
+    if (!GetProperty(cx, internals, internals, cx->names().unit, &value))
+        return false;
+    RootedString unit(cx, value.toString());
+    if (!unit)
+        return false;
+
+    if (!GetProperty(cx, internals, internals, cx->names().style, &value))
+        return false;
+    RootedLinearString style(cx, value.toString()->ensureLinear(cx));
+    if (!style)
+        return false;
+
+    if (!GetProperty(cx, internals, internals, cx->names().type, &value))
+        return false;
+    RootedLinearString type(cx, value.toString()->ensureLinear(cx));
+    if (!type)
+        return false;
+
+    double x = args[1].toNumber();
+
+    UDateRelativeDateTimeFormatterStyle relDateTimeStyle = UDAT_STYLE_LONG;
+    if (StringEqualsAscii(style, "short")) {
+        relDateTimeStyle = UDAT_STYLE_SHORT;
+    } else if (StringEqualsAscii(style, "narrow")) {
+        relDateTimeStyle = UDAT_STYLE_NARROW;
+    }
+
+    double v;
+    URelativeDateTimeUnit relDateTimeUnit = GetBestMatchUnit(cx, x, unit, v);
+
+    // ICU doesn't handle -0 well. This worksaround the bug by turning every
+    // `-0.0` into `0.0`.
+    // See: http://bugs.icu-project.org/trac/ticket/12936
+    if (v == 0.0 && signbit(v) == 1) {
+        v = 0.0;
+    }
+
+    UErrorCode status = U_ZERO_ERROR;
+    URelativeDateTimeFormatter* rtf = ureldatefmt_open(
+            icuLocale(locale.ptr()),
+            nullptr,
+            relDateTimeStyle,
+            UDISPCTX_CAPITALIZATION_NONE,
+            &status);
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+
+    ScopedICUObject<URelativeDateTimeFormatter, ureldatefmt_close> closeRelativeTimeFormat(rtf);
+
+    Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
+    if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE))
+        return false;
+
+    int32_t size;
+
+    if (StringEqualsAscii(type, "numeric")) {
+        size = ureldatefmt_formatNumeric(
+            rtf,
+            v,
+            relDateTimeUnit,
+            Char16ToUChar(chars.begin()),
+            INITIAL_CHAR_BUFFER_SIZE,
+            &status);
+        if (status == U_BUFFER_OVERFLOW_ERROR) {
+            MOZ_ASSERT(size >= 0);
+            if (!chars.resize(size))
+                return false;
+            status = U_ZERO_ERROR;
+            ureldatefmt_formatNumeric(
+                    rtf,
+                    v,
+                    relDateTimeUnit,
+                    Char16ToUChar(chars.begin()),
+                    size,
+                    &status);
+        }
+    } else {
+        size = ureldatefmt_format(
+            rtf,
+            v,
+            relDateTimeUnit,
+            Char16ToUChar(chars.begin()),
+            INITIAL_CHAR_BUFFER_SIZE,
+            &status);
+        if (status == U_BUFFER_OVERFLOW_ERROR) {
+            MOZ_ASSERT(size >= 0);
+            if (!chars.resize(size))
+                return false;
+            status = U_ZERO_ERROR;
+            ureldatefmt_format(
+                    rtf,
+                    v,
+                    relDateTimeUnit,
+                    Char16ToUChar(chars.begin()),
+                    size,
+                    &status);
+        }
+    }
+
+    if (U_FAILURE(status)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR);
+        return false;
+    }
+
+    JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), size);
+    if (!str)
+        return false;
+
+    args.rval().setString(str);
+    return true;
+}
+
 bool
 js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 1);
 
     JSAutoByteString locale(cx, args[0].toString());
     if (!locale)
--- a/js/src/builtin/Intl.h
+++ b/js/src/builtin/Intl.h
@@ -396,16 +396,41 @@ intl_SelectPluralRule(JSContext* cx, uns
  *
  * Example:
  *
  * intl_getPluralCategories('pl', 'cardinal'); // ['one', 'few', 'many', 'other']
  */
 extern MOZ_MUST_USE bool
 intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp);
 
+/******************** RelativeTimeFormat ********************/
+
+/**
+ * Returns an object indicating the supported locales for relative time format
+ * by having a true-valued property for each such locale with the
+ * canonicalized language tag as the property name. The object has no
+ * prototype.
+ *
+ * Usage: availableLocales = intl_RelativeTimeFormat_availableLocales()
+ */
+extern MOZ_MUST_USE bool
+intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp);
+
+/**
+ * Returns a relative time as a string formatted according to the effective
+ * locale and the formatting options of the given RelativeTimeFormat.
+ *
+ * x should be a number representing number of milliseconds relative to
+ * the current point in time.
+ *
+ * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, x)
+ */
+extern MOZ_MUST_USE bool
+intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp);
+
 /**
  * Returns a plain object with calendar information for a single valid locale
  * (callers must perform this validation).  The object will have these
  * properties:
  *
  *   firstDayOfWeek
  *     an integer in the range 1=Sunday to 7=Saturday indicating the day
  *     considered the first day of the week in calendars, e.g. 1 for en-US,
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -1256,17 +1256,18 @@ function initializeIntlObject(obj) {
 
 /**
  * Mark |internals| as having the given type and lazy data.
  */
 function setLazyData(internals, type, lazyData)
 {
     assert(internals.type === "partial", "can't set lazy data for anything but a newborn");
     assert(type === "Collator" || type === "DateTimeFormat" ||
-           type == "NumberFormat" || type === "PluralRules",
+           type === "NumberFormat" || type === "PluralRules" ||
+           type === "RelativeTimeFormat",
            "bad type");
     assert(IsObject(lazyData), "non-object lazy data");
 
     // Set in reverse order so that the .type change is a barrier.
     internals.lazyData = lazyData;
     internals.type = type;
 }
 
@@ -1308,17 +1309,18 @@ function maybeInternalProperties(interna
  */
 function isInitializedIntlObject(obj) {
 #ifdef DEBUG
     var internals = callFunction(std_WeakMap_get, internalsMap, obj);
     if (IsObject(internals)) {
         assert(callFunction(std_Object_hasOwnProperty, internals, "type"), "missing type");
         var type = internals.type;
         assert(type === "partial" || type === "Collator" ||
-               type === "DateTimeFormat" || type === "NumberFormat" || type === "PluralRules",
+               type === "DateTimeFormat" || type === "NumberFormat" ||
+               type === "PluralRules" || type === "RelativeTimeFormat",
                "unexpected type");
         assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData");
         assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps");
     } else {
         assert(internals === undefined, "bad mapping for |obj|");
     }
 #endif
     return callFunction(std_WeakMap_has, internalsMap, obj);
@@ -3188,16 +3190,217 @@ function Intl_PluralRules_resolvedOption
     for (var i = 0; i < optionalProperties.length; i++) {
         var p = optionalProperties[i];
         if (callFunction(std_Object_hasOwnProperty, internals, p))
             _DefineDataProperty(result, p, internals[p]);
     }
     return result;
 }
 
+/********** Intl.RelativeTimeFormat **********/
+
+/**
+ * RelativeTimeFormat internal properties.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3.
+ */
+var relativeTimeFormatInternalProperties = {
+    _availableLocales: null,
+    availableLocales: function()
+    {
+        var locales = this._availableLocales;
+        if (locales)
+            return locales;
+
+        locales = intl_RelativeTimeFormat_availableLocales();
+        addSpecialMissingLanguageTags(locales);
+        return (this._availableLocales = locales);
+    }
+};
+
+/**
+ * Compute an internal properties object from |lazyRelativeTimeFormatData|.
+ */
+function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) {
+    assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?");
+
+    var internalProps = std_Object_create(null);
+
+    var requestedLocales = lazyRelativeTimeFormatData.requestedLocales;
+
+    var RelativeTimeFormat = relativeTimeFormatInternalProperties;
+
+    // Step 16.
+    const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat),
+                          lazyRelativeTimeFormatData.requestedLocales,
+                          lazyRelativeTimeFormatData.opt,
+                          noRelevantExtensionKeys, undefined);
+
+    // Step 17.
+    internalProps.locale = r.locale;
+    internalProps.unit = lazyRelativeTimeFormatData.unit;
+    internalProps.style = lazyRelativeTimeFormatData.style;
+    internalProps.type = lazyRelativeTimeFormatData.type;
+
+    return internalProps;
+}
+
+/**
+ * Returns an object containing the RelativeTimeFormat internal properties of |obj|,
+ * or throws a TypeError if |obj| isn't RelativeTimeFormat-initialized.
+ */
+function getRelativeTimeFormatInternals(obj, methodName) {
+    var internals = getIntlObjectInternals(obj, "RelativeTimeFormat", methodName);
+    assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals");
+
+    var internalProps = maybeInternalProperties(internals);
+    if (internalProps)
+        return internalProps;
+
+    internalProps = resolveRelativeTimeFormatInternals(internals.lazyData);
+    setInternalProperties(internals, internalProps);
+    return internalProps;
+}
+
+/**
+ * Initializes an object as a RelativeTimeFormat.
+ *
+ * This method is complicated a moderate bit by its implementing initialization
+ * as a *lazy* concept.  Everything that must happen now, does -- but we defer
+ * all the work we can until the object is actually used as a PluralRules.
+ * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted
+ * here occur there.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1.
+ */
+function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) {
+    assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat");
+
+    // Step 1.
+    if (isInitializedIntlObject(relativeTimeFormat))
+        ThrowTypeError(JSMSG_INTL_OBJECT_REINITED);
+
+    let internals = initializeIntlObject(relativeTimeFormat);
+
+    // Lazy RelativeTimeFormat data has the following structure:
+    //
+    //   {
+    //     requestedLocales: List of locales,
+    //     type: "text" / "numeric",
+    //     style: "long" / "short" / "narrow",
+    //     unit: "best fit" / "second" / "minute" / "hour" / "day" / "week" / "month" / "year",
+    //
+    //     opt: // opt object computer in InitializeRelativeTimeFormat
+    //       {
+    //         localeMatcher: "lookup" / "best fit",
+    //       }
+    //   }
+    //
+    // Note that lazy data is only installed as a final step of initialization,
+    // so every RelativeTimeFormat lazy data object has *all* these properties, never a
+    // subset of them.
+    const lazyRelativeTimeFormatData = std_Object_create(null);
+
+    // Step 3.
+    let requestedLocales = CanonicalizeLocaleList(locales);
+    lazyRelativeTimeFormatData.requestedLocales = requestedLocales;
+
+    // Steps 4-5.
+    if (options === undefined)
+        options = {};
+    else
+        options = ToObject(options);
+
+    // Steps 6-7.
+    const type = GetOption(options, "type", "string", ["numeric", "text"], "text");
+    lazyRelativeTimeFormatData.type = type;
+
+    // Steps 8-9.
+    const unit = GetOption(options, "unit", "string", ["best fit", "second", "minute", "hour", "day", "week", "month", "quarter", "year"], "best fit");
+    lazyRelativeTimeFormatData.unit = unit;
+
+    // Steps 10-11.
+    const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long");
+    lazyRelativeTimeFormatData.style = style;
+
+
+    // Step 12.
+    let opt = new Record();
+    lazyRelativeTimeFormatData.opt = opt;
+
+    // Steps 13-14.
+    let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit");
+    opt.localeMatcher = matcher;
+
+    setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData)
+}
+
+/**
+ * Returns the subset of the given locale list for which this locale list has a
+ * matching (possibly fallback) locale. Locales appear in the same order in the
+ * returned list as in the input list.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.2.
+ */
+function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) {
+    var options = arguments.length > 1 ? arguments[1] : undefined;
+
+    // Step 1.
+    var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales,
+                                        relativeTimeFormatInternalProperties);
+    // Step 2.
+    let requestedLocales = CanonicalizeLocaleList(locales);
+
+    // Step 3.
+    return SupportedLocales(availableLocales, requestedLocales, options);
+}
+
+/**
+ * Returns a String value representing the written form of a relative date
+ * formatted according to the effective locale and the formatting options
+ * of this RelativeTimeFormat object.
+ *
+ * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3.
+ */
+function Intl_RelativeTimeFormat_format(value) {
+    // Step 1.
+    let relativeTimeFormat = this;
+
+    // Step 2.
+    let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format");
+
+    // Step 4.
+    let x = ToNumber(value);
+
+    let now = std_Date_now();
+
+    let ms = x - now;
+
+    // Step 5.
+    return intl_FormatRelativeTime(relativeTimeFormat, ms);
+}
+
+/**
+ * Returns the resolved options for a PluralRules object.
+ *
+ * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4.
+ */
+function Intl_RelativeTimeFormat_resolvedOptions() {
+    var internals = getRelativeTimeFormatInternals(this, "resolvedOptions");
+
+    var result = {
+        locale: internals.locale,
+        unit: internals.unit,
+        style: internals.style,
+        type: internals.type
+    };
+
+    return result;
+}
+
 
 function Intl_getCanonicalLocales(locales) {
   let codes = CanonicalizeLocaleList(locales);
   let result = [];
 
   let len = codes.length;
   let k = 0;
 
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -2860,16 +2860,22 @@ extern JS_FRIEND_API(JSObject*)
 ToWindowIfWindowProxy(JSObject* obj);
 
 // Create and add the Intl.PluralRules constructor function to the provided
 // object.  This function throws if called more than once per realm/global
 // object.
 extern bool
 AddPluralRulesConstructor(JSContext* cx, JS::Handle<JSObject*> intl);
 
+// Create and add the Intl.RelativeTimeFormat constructor function to the provided
+// object.  This function throws if called more than once per realm/global
+// object.
+extern bool
+AddRelativeTimeFormatConstructor(JSContext* cx, JS::Handle<JSObject*> intl);
+
 } /* namespace js */
 
 class NativeProfiler
 {
   public:
     virtual ~NativeProfiler() {};
     virtual void sampleNative(void* addr, uint32_t size) = 0;
     virtual void removeNative(void* addr) = 0;
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -911,16 +911,19 @@ AddIntlExtras(JSContext* cx, unsigned ar
     };
 
     if (!JS_DefineFunctions(cx, intl, funcs))
         return false;
 
     if (!js::AddPluralRulesConstructor(cx, intl))
         return false;
 
+    if (!js::AddRelativeTimeFormatConstructor(cx, intl))
+        return false;
+
     args.rval().setUndefined();
     return true;
 }
 #endif // ENABLE_INTL_API
 
 static bool
 EvalAndPrint(JSContext* cx, const char* bytes, size_t length,
              int lineno, bool compileOnly)
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/RelativeTimeFormat/construct-newtarget.js
@@ -0,0 +1,82 @@
+// |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/. */
+
+addIntlExtras(Intl);
+
+// Test subclassing %Intl.RelativeTimeFormat% works correctly.
+class MyRelativeTimeFormat extends Intl.RelativeTimeFormat {}
+
+var obj = new MyRelativeTimeFormat();
+assertEq(obj instanceof MyRelativeTimeFormat, true);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), MyRelativeTimeFormat.prototype);
+
+obj = Reflect.construct(MyRelativeTimeFormat, []);
+assertEq(obj instanceof MyRelativeTimeFormat, true);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), MyRelativeTimeFormat.prototype);
+
+obj = Reflect.construct(MyRelativeTimeFormat, [], MyRelativeTimeFormat);
+assertEq(obj instanceof MyRelativeTimeFormat, true);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), MyRelativeTimeFormat.prototype);
+
+obj = Reflect.construct(MyRelativeTimeFormat, [], Intl.RelativeTimeFormat);
+assertEq(obj instanceof MyRelativeTimeFormat, false);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), Intl.RelativeTimeFormat.prototype);
+
+
+// Set a different constructor as NewTarget.
+obj = Reflect.construct(MyRelativeTimeFormat, [], Array);
+assertEq(obj instanceof MyRelativeTimeFormat, false);
+assertEq(obj instanceof Intl.RelativeTimeFormat, false);
+assertEq(obj instanceof Array, true);
+assertEq(Object.getPrototypeOf(obj), Array.prototype);
+
+obj = Reflect.construct(Intl.RelativeTimeFormat, [], Array);
+assertEq(obj instanceof Intl.RelativeTimeFormat, false);
+assertEq(obj instanceof Array, true);
+assertEq(Object.getPrototypeOf(obj), Array.prototype);
+
+
+// The prototype defaults to %RelativeTimeFormatPrototype% if null.
+function NewTargetNullPrototype() {}
+NewTargetNullPrototype.prototype = null;
+
+obj = Reflect.construct(Intl.RelativeTimeFormat, [], NewTargetNullPrototype);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), Intl.RelativeTimeFormat.prototype);
+
+obj = Reflect.construct(MyRelativeTimeFormat, [], NewTargetNullPrototype);
+assertEq(obj instanceof MyRelativeTimeFormat, false);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), Intl.RelativeTimeFormat.prototype);
+
+
+// "prototype" property is retrieved exactly once.
+var trapLog = [], getLog = [];
+var ProxiedConstructor = new Proxy(Intl.RelativeTimeFormat, new Proxy({
+    get(target, propertyKey, receiver) {
+        getLog.push(propertyKey);
+        return Reflect.get(target, propertyKey, receiver);
+    }
+}, {
+    get(target, propertyKey, receiver) {
+        trapLog.push(propertyKey);
+        return Reflect.get(target, propertyKey, receiver);
+    }
+}));
+
+obj = Reflect.construct(Intl.RelativeTimeFormat, [], ProxiedConstructor);
+assertEqArray(trapLog, ["get"]);
+assertEqArray(getLog, ["prototype"]);
+assertEq(obj instanceof Intl.RelativeTimeFormat, true);
+assertEq(Object.getPrototypeOf(obj), Intl.RelativeTimeFormat.prototype);
+
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/RelativeTimeFormat/format.js
@@ -0,0 +1,76 @@
+// |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 format function with a diverse set of locales and options.
+
+var rtf;
+
+addIntlExtras(Intl);
+
+
+rtf = new Intl.RelativeTimeFormat("en-US");
+assertEq(rtf.format(Date.now()), "now");
+assertEq(rtf.format(Date.now() - 1000), "1 second ago");
+assertEq(rtf.format(Date.now() + 1000), "in 1 second");
+
+assertEq(rtf.format(Date.now() - 1000 * 60), "1 minute ago");
+assertEq(rtf.format(Date.now() + 1000 * 60), "in 1 minute");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60), "1 hour ago");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60), "in 1 hour");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24), "yesterday");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24), "tomorrow");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 7), "last week");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 7), "next week");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 32), "last month");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 32), "next month");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 366), "last year");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 366), "next year");
+
+
+rtf = new Intl.RelativeTimeFormat("en-US", { type: "numeric" });
+assertEq(rtf.format(Date.now()), "in 0 seconds");
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24), "1 day ago");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24), "in 1 day");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 7), "1 week ago");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 7), "in 1 week");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 32), "1 month ago");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 32), "in 1 month");
+
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24 * 366), "1 year ago");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24 * 366), "in 1 year");
+
+
+rtf = new Intl.RelativeTimeFormat("de");
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24), "gestern");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24), "morgen");
+
+rtf = new Intl.RelativeTimeFormat("ar");
+assertEq(rtf.format(Date.now() - 1000 * 60 * 60 * 24), "أمس");
+assertEq(rtf.format(Date.now() + 1000 * 60 * 60 * 24), "غدًا");
+
+
+rtf = new Intl.RelativeTimeFormat("en-US");
+assertEq(rtf.format(Infinity), "in ∞ years");
+assertEq(rtf.format(-Infinity), "∞ years ago");
+
+var weirdCases = [
+  NaN,
+  "word",
+  [0,2],
+  {},
+];
+
+for (let c of weirdCases) {
+  assertEq(rtf.format(c), "in NaN years");
+};
+
+reportCompare(0, 0, 'ok');
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/RelativeTimeFormat/relativetimeformat.js
@@ -0,0 +1,18 @@
+// |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 format function with a diverse set of locales and options.
+
+var rtf;
+
+addIntlExtras(Intl);
+
+rtf = new Intl.RelativeTimeFormat("en-us");
+assertEq(rtf.resolvedOptions().locale, "en-US");
+assertEq(rtf.resolvedOptions().unit, "best fit");
+assertEq(rtf.resolvedOptions().style, "long");
+assertEq(rtf.resolvedOptions().type, "text");
+
+reportCompare(0, 0, 'ok');
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/RelativeTimeFormat/supportedLocalesOf.js
@@ -0,0 +1,375 @@
+// |reftest| skip-if(!this.hasOwnProperty('Intl')||!this.hasOwnProperty('addIntlExtras')||xulRuntime.shell)
+// -- test in browser only that ICU has locale data for all Mozilla languages
+
+/* 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/. */
+
+// This array contains the locales that ICU supports in
+// number formatting whose languages Mozilla localizes Firefox into.
+// Current as of ICU 50.1.2 and Firefox March 2013.
+var locales = [
+    "af",
+    "af-NA",
+    "af-ZA",
+    "ar",
+    "ar-001",
+    "ar-AE",
+    "ar-BH",
+    "ar-DJ",
+    "ar-DZ",
+    "ar-EG",
+    "ar-EH",
+    "ar-ER",
+    "ar-IL",
+    "ar-IQ",
+    "ar-JO",
+    "ar-KM",
+    "ar-KW",
+    "ar-LB",
+    "ar-LY",
+    "ar-MA",
+    "ar-MR",
+    "ar-OM",
+    "ar-PS",
+    "ar-QA",
+    "ar-SA",
+    "ar-SD",
+    "ar-SO",
+    "ar-SY",
+    "ar-TD",
+    "ar-TN",
+    "ar-YE",
+    "as",
+    "as-IN",
+    "be",
+    "be-BY",
+    "bg",
+    "bg-BG",
+    "bn",
+    "bn-BD",
+    "bn-IN",
+    "br",
+    "br-FR",
+    "bs",
+    "bs-Cyrl",
+    "bs-Cyrl-BA",
+    "bs-Latn",
+    "bs-Latn-BA",
+    "ca",
+    "ca-AD",
+    "ca-ES",
+    "cs",
+    "cs-CZ",
+    "cy",
+    "cy-GB",
+    "da",
+    "da-DK",
+    "de",
+    "de-AT",
+    "de-BE",
+    "de-CH",
+    "de-DE",
+    "de-LI",
+    "de-LU",
+    "el",
+    "el-CY",
+    "el-GR",
+    "en",
+    "en-150",
+    "en-AG",
+    "en-AS",
+    "en-AU",
+    "en-BB",
+    "en-BE",
+    "en-BM",
+    "en-BS",
+    "en-BW",
+    "en-BZ",
+    "en-CA",
+    "en-CM",
+    "en-DM",
+    "en-FJ",
+    "en-FM",
+    "en-GB",
+    "en-GD",
+    "en-GG",
+    "en-GH",
+    "en-GI",
+    "en-GM",
+    "en-GU",
+    "en-GY",
+    "en-HK",
+    "en-IE",
+    "en-IM",
+    "en-IN",
+    "en-JE",
+    "en-JM",
+    "en-KE",
+    "en-KI",
+    "en-KN",
+    "en-KY",
+    "en-LC",
+    "en-LR",
+    "en-LS",
+    "en-MG",
+    "en-MH",
+    "en-MP",
+    "en-MT",
+    "en-MU",
+    "en-MW",
+    "en-NA",
+    "en-NG",
+    "en-NZ",
+    "en-PG",
+    "en-PH",
+    "en-PK",
+    "en-PR",
+    "en-PW",
+    "en-SB",
+    "en-SC",
+    "en-SG",
+    "en-SL",
+    "en-SS",
+    "en-SZ",
+    "en-TC",
+    "en-TO",
+    "en-TT",
+    "en-TZ",
+    "en-UG",
+    "en-UM",
+    "en-US",
+    "en-US-posix",
+    "en-VC",
+    "en-VG",
+    "en-VI",
+    "en-VU",
+    "en-WS",
+    "en-ZA",
+    "en-ZM",
+    "en-ZW",
+    "eo",
+    "es",
+    "es-419",
+    "es-AR",
+    "es-BO",
+    "es-CL",
+    "es-CO",
+    "es-CR",
+    "es-CU",
+    "es-DO",
+    "es-EA",
+    "es-EC",
+    "es-ES",
+    "es-GQ",
+    "es-GT",
+    "es-HN",
+    "es-IC",
+    "es-MX",
+    "es-NI",
+    "es-PA",
+    "es-PE",
+    "es-PH",
+    "es-PR",
+    "es-PY",
+    "es-SV",
+    "es-US",
+    "es-UY",
+    "es-VE",
+    "et",
+    "et-EE",
+    "eu",
+    "eu-ES",
+    "fa",
+    "fa-AF",
+    "fa-IR",
+    "ff",
+    "ff-SN",
+    "fi",
+    "fi-FI",
+    "fr",
+    "fr-BE",
+    "fr-BF",
+    "fr-BI",
+    "fr-BJ",
+    "fr-BL",
+    "fr-CA",
+    "fr-CD",
+    "fr-CF",
+    "fr-CG",
+    "fr-CH",
+    "fr-CI",
+    "fr-CM",
+    "fr-DJ",
+    "fr-DZ",
+    "fr-FR",
+    "fr-GA",
+    "fr-GF",
+    "fr-GN",
+    "fr-GP",
+    "fr-GQ",
+    "fr-HT",
+    "fr-KM",
+    "fr-LU",
+    "fr-MA",
+    "fr-MC",
+    "fr-MF",
+    "fr-MG",
+    "fr-ML",
+    "fr-MQ",
+    "fr-MR",
+    "fr-MU",
+    "fr-NC",
+    "fr-NE",
+    "fr-PF",
+    "fr-RE",
+    "fr-RW",
+    "fr-SC",
+    "fr-SN",
+    "fr-SY",
+    "fr-TD",
+    "fr-TG",
+    "fr-TN",
+    "fr-VU",
+    "fr-YT",
+    "ga",
+    "ga-IE",
+    "gl",
+    "gl-ES",
+    "gu",
+    "gu-IN",
+    "he",
+    "he-IL",
+    "hi",
+    "hi-IN",
+    "hr",
+    "hr-BA",
+    "hr-HR",
+    "hu",
+    "hu-HU",
+    "hy",
+    "hy-AM",
+    "id",
+    "id-ID",
+    "is",
+    "is-IS",
+    "it",
+    "it-CH",
+    "it-IT",
+    "it-SM",
+    "ja",
+    "ja-JP",
+    "kk",
+    "kk-Cyrl",
+    "kk-Cyrl-KZ",
+    "km",
+    "km-KH",
+    "kn",
+    "kn-IN",
+    "ko",
+    "ko-KP",
+    "ko-KR",
+    "lt",
+    "lt-LT",
+    "lv",
+    "lv-LV",
+    "mk",
+    "mk-MK",
+    "ml",
+    "ml-IN",
+    "mr",
+    "mr-IN",
+    "nb",
+    "nb-NO",
+    "nl",
+    "nl-AW",
+    "nl-BE",
+    "nl-CW",
+    "nl-NL",
+    "nl-SR",
+    "nl-SX",
+    "nn",
+    "nn-NO",
+    "or",
+    "or-IN",
+    "pa",
+    "pa-Arab",
+    "pa-Arab-PK",
+    "pa-Guru",
+    "pa-Guru-IN",
+    "pl",
+    "pl-PL",
+    "pt",
+    "pt-AO",
+    "pt-BR",
+    "pt-CV",
+    "pt-GW",
+    "pt-MO",
+    "pt-MZ",
+    "pt-PT",
+    "pt-ST",
+    "pt-TL",
+    "rm",
+    "rm-CH",
+    "ro",
+    "ro-MD",
+    "ro-RO",
+    "ru",
+    "ru-BY",
+    "ru-KG",
+    "ru-KZ",
+    "ru-MD",
+    "ru-RU",
+    "ru-UA",
+    "si",
+    "si-LK",
+    "sk",
+    "sk-SK",
+    "sl",
+    "sl-SI",
+    "sq",
+    "sq-AL",
+    "sq-MK",
+    "sr",
+    "sr-Cyrl",
+    "sr-Cyrl-BA",
+    "sr-Cyrl-ME",
+    "sr-Cyrl-RS",
+    "sr-Latn",
+    "sr-Latn-BA",
+    "sr-Latn-ME",
+    "sr-Latn-RS",
+    "sv",
+    "sv-AX",
+    "sv-FI",
+    "sv-SE",
+    "te",
+    "te-IN",
+    "th",
+    "th-TH",
+    "tr",
+    "tr-CY",
+    "tr-TR",
+    "uk",
+    "uk-UA",
+    "vi",
+    "vi-VN",
+    "zh",
+    "zh-Hans",
+    "zh-Hans-CN",
+    "zh-Hans-HK",
+    "zh-Hans-MO",
+    "zh-Hans-SG",
+    "zh-Hant",
+    "zh-Hant-HK",
+    "zh-Hant-MO",
+    "zh-Hant-TW",
+];
+
+addIntlExtras(Intl);
+
+const result = Intl.RelativeTimeFormat.supportedLocalesOf(locales);
+
+assertEqArray(locales, result);
+
+reportCompare(0, 0, 'ok');
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -151,16 +151,17 @@
     macro(incumbentGlobal, incumbentGlobal, "incumbentGlobal") \
     macro(index, index, "index") \
     macro(infinity, infinity, "infinity") \
     macro(Infinity, Infinity, "Infinity") \
     macro(InitializeCollator, InitializeCollator, "InitializeCollator") \
     macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \
     macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \
     macro(InitializePluralRules, InitializePluralRules, "InitializePluralRules") \
+    macro(InitializeRelativeTimeFormat, InitializeRelativeTimeFormat, "InitializeRelativeTimeFormat") \
     macro(innermost, innermost, "innermost") \
     macro(inNursery, inNursery, "inNursery") \
     macro(input, input, "input") \
     macro(int8, int8, "int8") \
     macro(int16, int16, "int16") \
     macro(int32, int32, "int32") \
     macro(Int8x16, Int8x16, "Int8x16") \
     macro(Int16x8, Int16x8, "Int16x8") \
@@ -262,16 +263,18 @@
     macro(RegExpFlagsGetter, RegExpFlagsGetter, "RegExpFlagsGetter") \
     macro(RegExpMatcher, RegExpMatcher, "RegExpMatcher") \
     macro(RegExpSearcher, RegExpSearcher, "RegExpSearcher") \
     macro(RegExpTester, RegExpTester, "RegExpTester") \
     macro(RegExp_prototype_Exec, RegExp_prototype_Exec, "RegExp_prototype_Exec") \
     macro(Reify, Reify, "Reify") \
     macro(reject, reject, "reject") \
     macro(rejected, rejected, "rejected") \
+    macro(RelativeTimeFormat, RelativeTimeFormat, "RelativeTimeFormat") \
+    macro(RelativeTimeFormatFormat, RelativeTimeFormatFormat, "Intl_RelativeTimeFormat_Format") \
     macro(RequireObjectCoercible, RequireObjectCoercible, "RequireObjectCoercible") \
     macro(resolve, resolve, "resolve") \
     macro(resumeGenerator, resumeGenerator, "resumeGenerator") \
     macro(return, return_, "return") \
     macro(revoke, revoke, "revoke") \
     macro(script, script, "script") \
     macro(scripts, scripts, "scripts") \
     macro(second, second, "second") \
@@ -323,16 +326,17 @@
     macro(uint16, uint16, "uint16") \
     macro(uint32, uint32, "uint32") \
     macro(Uint8x16, Uint8x16, "Uint8x16") \
     macro(Uint16x8, Uint16x8, "Uint16x8") \
     macro(Uint32x4, Uint32x4, "Uint32x4") \
     macro(unescape, unescape, "unescape") \
     macro(uneval, uneval, "uneval") \
     macro(unicode, unicode, "unicode") \
+    macro(unit, unit, "unit") \
     macro(uninitialized, uninitialized, "uninitialized") \
     macro(unsized, unsized, "unsized") \
     macro(unwatch, unwatch, "unwatch") \
     macro(UnwrapAndCallRegExpBuiltinExec, UnwrapAndCallRegExpBuiltinExec, "UnwrapAndCallRegExpBuiltinExec") \
     macro(url, url, "url") \
     macro(usage, usage, "usage") \
     macro(useAsm, useAsm, "use asm") \
     macro(useGrouping, useGrouping, "useGrouping") \
--- a/js/src/vm/GlobalObject.h
+++ b/js/src/vm/GlobalObject.h
@@ -99,16 +99,17 @@ class GlobalObject : public NativeObject
         ASYNC_FUNCTION_PROTO,
         ASYNC_FUNCTION,
         MAP_ITERATOR_PROTO,
         SET_ITERATOR_PROTO,
         COLLATOR_PROTO,
         NUMBER_FORMAT_PROTO,
         DATE_TIME_FORMAT_PROTO,
         PLURAL_RULES_PROTO,
+        RELATIVE_TIME_FORMAT_PROTO,
         MODULE_PROTO,
         IMPORT_ENTRY_PROTO,
         EXPORT_ENTRY_PROTO,
         REGEXP_STATICS,
         WARNED_ONCE_FLAGS,
         RUNTIME_CODEGEN_ENABLED,
         DEBUGGERS,
         INTRINSICS,
@@ -479,16 +480,20 @@ class GlobalObject : public NativeObject
     JSObject* getOrCreateDateTimeFormatPrototype(JSContext* cx) {
         return getOrCreateObject(cx, DATE_TIME_FORMAT_PROTO, initIntlObject);
     }
 
     JSObject* getOrCreatePluralRulesPrototype(JSContext* cx) {
         return getOrCreateObject(cx, PLURAL_RULES_PROTO, initIntlObject);
     }
 
+    JSObject* getOrCreateRelativeTimeFormatPrototype(JSContext* cx) {
+        return getOrCreateObject(cx, RELATIVE_TIME_FORMAT_PROTO, initIntlObject);
+    }
+
     static bool ensureModulePrototypesCreated(JSContext *cx, Handle<GlobalObject*> global);
 
     JSObject* maybeGetModulePrototype() {
         Value value = getSlot(MODULE_PROTO);
         return value.isUndefined() ? nullptr : &value.toObject();
     }
 
     JSObject* maybeGetImportEntryPrototype() {
@@ -749,16 +754,17 @@ class GlobalObject : public NativeObject
 
     // Implemented in builtin/MapObject.cpp.
     static bool initMapIteratorProto(JSContext* cx, Handle<GlobalObject*> global);
     static bool initSetIteratorProto(JSContext* cx, Handle<GlobalObject*> global);
 
     // Implemented in Intl.cpp.
     static bool initIntlObject(JSContext* cx, Handle<GlobalObject*> global);
     static bool addPluralRulesConstructor(JSContext* cx, HandleObject intl);
+    static bool addRelativeTimeFormatConstructor(JSContext* cx, HandleObject intl);
 
     // Implemented in builtin/ModuleObject.cpp
     static bool initModuleProto(JSContext* cx, Handle<GlobalObject*> global);
     static bool initImportEntryProto(JSContext* cx, Handle<GlobalObject*> global);
     static bool initExportEntryProto(JSContext* cx, Handle<GlobalObject*> global);
 
     // Implemented in builtin/TypedObject.cpp
     static bool initTypedObjectModule(JSContext* cx, Handle<GlobalObject*> global);
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -2474,17 +2474,19 @@ static const JSFunctionSpec intrinsic_fu
     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_FN("intl_PluralRules_availableLocales", intl_PluralRules_availableLocales, 0,0),
-    JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2, 0),
+    JS_FN("intl_RelativeTimeFormat_availableLocales", intl_RelativeTimeFormat_availableLocales, 0,0),
+    JS_FN("intl_FormatRelativeTime", intl_FormatRelativeTime, 2,0),
+    JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2,0),
     JS_FN("intl_SelectPluralRule", intl_SelectPluralRule, 2,0),
 
     JS_INLINABLE_FN("IsRegExpObject",
                     intrinsic_IsInstanceOfBuiltin<RegExpObject>, 1,0,
                     IsRegExpObject),
     JS_FN("CallRegExpMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<RegExpObject>>, 2,0),
     JS_INLINABLE_FN("RegExpMatcher", RegExpMatcher, 4,0,
--- a/toolkit/components/mozintl/MozIntl.cpp
+++ b/toolkit/components/mozintl/MozIntl.cpp
@@ -77,16 +77,37 @@ MozIntl::AddPluralRulesConstructor(JS::H
 
   if (!js::AddPluralRulesConstructor(cx, realIntlObj)) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+MozIntl::AddRelativeTimeFormatConstructor(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);
+
+  if (!js::AddRelativeTimeFormatConstructor(cx, realIntlObj)) {
+    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
@@ -12,9 +12,16 @@ interface mozIMozIntl : nsISupports
   [implicit_jscontext] void addGetDisplayNames(in jsval intlObject);
 
   /**
    * Adds a PluralRules constructor to the given object.  This function may only
    * be called once within a realm/global object: calling it multiple times will
    * throw.
    */
   [implicit_jscontext] void addPluralRulesConstructor(in jsval intlObject);
+
+  /**
+   * Adds a RelativeTimeFormat constructor to the given object.  This function may only
+   * be called once within a realm/global object: calling it multiple times will
+   * throw.
+   */
+  [implicit_jscontext] void addRelativeTimeFormatConstructor(in jsval intlObject);
 };