summaryrefslogtreecommitdiffstats
path: root/toolkit/components/mozintl
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/mozintl')
-rw-r--r--toolkit/components/mozintl/MozIntlHelper.cpp115
-rw-r--r--toolkit/components/mozintl/MozIntlHelper.h21
-rw-r--r--toolkit/components/mozintl/components.conf23
-rw-r--r--toolkit/components/mozintl/moz.build31
-rw-r--r--toolkit/components/mozintl/mozIMozIntl.idl105
-rw-r--r--toolkit/components/mozintl/mozIMozIntlHelper.idl65
-rw-r--r--toolkit/components/mozintl/mozIntl.sys.mjs1108
-rw-r--r--toolkit/components/mozintl/test/test_mozintl.js204
-rw-r--r--toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js142
-rw-r--r--toolkit/components/mozintl/test/test_mozintlhelper.js57
-rw-r--r--toolkit/components/mozintl/test/xpcshell.toml8
11 files changed, 1879 insertions, 0 deletions
diff --git a/toolkit/components/mozintl/MozIntlHelper.cpp b/toolkit/components/mozintl/MozIntlHelper.cpp
new file mode 100644
index 0000000000..84e192f3dd
--- /dev/null
+++ b/toolkit/components/mozintl/MozIntlHelper.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "MozIntlHelper.h"
+#include "nsBidiUtils.h"
+#include "nsJSUtils.h"
+#include "jsapi.h"
+#include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor
+#include "js/PropertyAndElement.h" // JS_DefineFunctions
+#include "js/PropertySpec.h"
+#include "js/Wrapper.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(MozIntlHelper, mozIMozIntlHelper)
+
+MozIntlHelper::MozIntlHelper() = default;
+
+MozIntlHelper::~MozIntlHelper() = default;
+
+static nsresult AddFunctions(JSContext* cx, JS::Handle<JS::Value> val,
+ const JSFunctionSpec* funcs) {
+ if (!val.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // We might be adding functions to a Window.
+ JS::Rooted<JSObject*> realIntlObj(
+ cx, js::CheckedUnwrapDynamic(&val.toObject(), cx));
+ if (!realIntlObj) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JSAutoRealm ar(cx, realIntlObj);
+
+ if (!JS_DefineFunctions(cx, realIntlObj, funcs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MozIntlHelper::AddGetCalendarInfo(JS::Handle<JS::Value> val, JSContext* cx) {
+ static const JSFunctionSpec funcs[] = {
+ JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0),
+ JS_FS_END};
+
+ return AddFunctions(cx, val, funcs);
+}
+
+NS_IMETHODIMP
+MozIntlHelper::AddDateTimeFormatConstructor(JS::Handle<JS::Value> val,
+ JSContext* cx) {
+ if (!val.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // We might be adding this constructor to a Window
+ JS::Rooted<JSObject*> realIntlObj(
+ cx, js::CheckedUnwrapDynamic(&val.toObject(), cx));
+ if (!realIntlObj) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JSAutoRealm ar(cx, realIntlObj);
+
+ if (!JS::AddMozDateTimeFormatConstructor(cx, realIntlObj)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MozIntlHelper::AddDisplayNamesConstructor(JS::Handle<JS::Value> val,
+ JSContext* cx) {
+ if (!val.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // We might be adding this constructor to a Window
+ JS::Rooted<JSObject*> realIntlObj(
+ cx, js::CheckedUnwrapDynamic(&val.toObject(), cx));
+ if (!realIntlObj) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JSAutoRealm ar(cx, realIntlObj);
+
+ if (!JS::AddMozDisplayNamesConstructor(cx, realIntlObj)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MozIntlHelper::StringHasRTLChars(JS::Handle<JS::Value> str, JSContext* cx,
+ bool* res) {
+ if (!str.isString()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoJSString string;
+ if (!string.init(cx, str)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *res = HasRTLChars(
+ Span(static_cast<const char16_t*>(string.get()), string.Length()));
+ return NS_OK;
+}
diff --git a/toolkit/components/mozintl/MozIntlHelper.h b/toolkit/components/mozintl/MozIntlHelper.h
new file mode 100644
index 0000000000..f37ac37899
--- /dev/null
+++ b/toolkit/components/mozintl/MozIntlHelper.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozIMozIntlHelper.h"
+
+namespace mozilla {
+
+class MozIntlHelper final : public mozIMozIntlHelper {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIMOZINTLHELPER
+
+ MozIntlHelper();
+
+ private:
+ ~MozIntlHelper();
+};
+
+} // namespace mozilla
diff --git a/toolkit/components/mozintl/components.conf b/toolkit/components/mozintl/components.conf
new file mode 100644
index 0000000000..d189f2a2b6
--- /dev/null
+++ b/toolkit/components/mozintl/components.conf
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{b43c96be-2b3a-4dc4-90e9-b06d34219b68}',
+ 'contract_ids': ['@mozilla.org/mozintlhelper;1'],
+ 'type': 'mozilla::MozIntlHelper',
+ 'headers': ['/toolkit/components/mozintl/MozIntlHelper.h'],
+ },
+
+ {
+ 'js_name': 'intl',
+ 'cid': '{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}',
+ 'contract_ids': ['@mozilla.org/mozintl;1'],
+ 'interfaces': ['mozIMozIntl'],
+ 'esModule': 'resource://gre/modules/mozIntl.sys.mjs',
+ 'constructor': 'MozIntl',
+ },
+]
diff --git a/toolkit/components/mozintl/moz.build b/toolkit/components/mozintl/moz.build
new file mode 100644
index 0000000000..9bfc79ad10
--- /dev/null
+++ b/toolkit/components/mozintl/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Internationalization")
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.toml"]
+
+XPIDL_SOURCES += [
+ "mozIMozIntl.idl",
+ "mozIMozIntlHelper.idl",
+]
+
+XPIDL_MODULE = "mozintl"
+
+SOURCES += [
+ "MozIntlHelper.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "mozIntl.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/toolkit/components/mozintl/mozIMozIntl.idl b/toolkit/components/mozintl/mozIMozIntl.idl
new file mode 100644
index 0000000000..aa042a3fe1
--- /dev/null
+++ b/toolkit/components/mozintl/mozIMozIntl.idl
@@ -0,0 +1,105 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This is a set of APIs that are of general usefulness for user interface
+ * internationalization.
+ *
+ * They're all in various stages of the standardization process through
+ * ECMA402, so they are exposed to privileged content only but are written
+ * in the way to allow for easy migration to standard Intl object once
+ * the appropriate stage of the ECMA402 is achieved.
+ *
+ * The exact structure of the code is a little bit complex because of that:
+ *
+ * 1) The core is in SpiderMonkey together with other Intl APIs
+ *
+ * This allows us to write the code once, stick to the spec language
+ * of the proposal, reuse our ICU bindings in Spidermonkey and use
+ * the code to inform us on refining the spec proposal for the given API itself.
+ *
+ * 2) The MozIntlHelper API exposes the SpiderMonkey APIs
+ *
+ * This helper API allows attaching the new APIs on any regular object.
+ *
+ * 3) The MozIntl API provides the access to those APIs
+ *
+ * This API exposes the actual functionality and wraps around the MozIntlHelper
+ * lazily retrieving and setting the accessors.
+ * On top of that, the API also binds additional functionality like using
+ * current application locale by default, and fetching OS regional preferences
+ * for date time format.
+ */
+[scriptable, uuid(7f63279a-1a29-4ae6-9e7a-dc9684a23530)]
+interface mozIMozIntl : nsISupports
+{
+ jsval getCalendarInfo([optional] in jsval locales);
+
+ /**
+ * Helper for IntlUtils.webidl, will be removed once Intl.DisplayNames
+ * supports date-time types in non-privileged code.
+ */
+ jsval getDisplayNamesDeprecated([optional] in jsval locales, [optional] in jsval options);
+
+
+ /**
+ * Returns a list of locale codes for a given type.
+ * At the moment only type="region" is supported.
+ *
+ * Example:
+ * let codes = getAvailableLocaleDisplayNames("region");
+ * codes === ["ar", "ae", "af", ...]
+ */
+ jsval getAvailableLocaleDisplayNames(in jsval type);
+
+ /**
+ * Returns a list of language names formatted for display.
+ *
+ * Example:
+ * let langs = getLanguageDisplayNames(["pl"], ["fr", "de", "en"]);
+ * langs === ["Francuski", "Niemiecki", "Angielski"]
+ */
+ jsval getLanguageDisplayNames(in jsval locales, in jsval langCodes);
+
+ /**
+ * Returns a list of region names formatted for display.
+ *
+ * Example:
+ * let regs = getRegionDisplayNames(["pl"], ["US", "CA", "MX"]);
+ * regs === ["Stany Zjednoczone", "Kanada", "Meksyk"]
+ */
+ jsval getRegionDisplayNames(in jsval locales, in jsval regionCodes);
+
+ /**
+ * Returns a list of locale names formatted for display.
+ *
+ * Example:
+ * let locs = getLocaleDisplayNames(["pl"], ["sr-RU", "es-MX", "fr-CA"]);
+ * locs === ["Serbski (Rosja)", "Hiszpański (Meksyk)", "Francuski (Kanada)"]
+ */
+ jsval getLocaleDisplayNames(in jsval locales, in jsval localeCodes, [optional] in jsval options);
+
+ /**
+ * Returns the assumed script directionality for known Firefox locales. This is
+ * somewhat crude, but should work until Bug 1750781 lands.
+ *
+ * TODO (Bug 1750781) - Callers should use Intl.LocaleInfo once it is standardized (see
+ * Bug 1693576), rather than maintaining a hardcoded list of RTL locales.
+ */
+ jsval getScriptDirection(in jsval locale);
+
+ boolean stringHasRTLChars(in jsval str);
+
+ readonly attribute jsval Collator;
+ readonly attribute jsval DateTimeFormat;
+ readonly attribute jsval DisplayNames;
+ readonly attribute jsval ListFormat;
+ readonly attribute jsval Locale;
+ readonly attribute jsval NumberFormat;
+ readonly attribute jsval PluralRules;
+ readonly attribute jsval RelativeTimeFormat;
+};
diff --git a/toolkit/components/mozintl/mozIMozIntlHelper.idl b/toolkit/components/mozintl/mozIMozIntlHelper.idl
new file mode 100644
index 0000000000..a2834c11e7
--- /dev/null
+++ b/toolkit/components/mozintl/mozIMozIntlHelper.idl
@@ -0,0 +1,65 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This is an internal helper for mozIMozIntl API. There should be virtually
+ * no reason for you to call this API except from mozIMozIntl implementation.
+ *
+ * This API helps accessing the SpiderMonkey Intl APIs, but it is mozIMozIntl
+ * that exposes the thin wrapper around them that binds the functionality
+ * to Gecko.
+ */
+[scriptable, uuid(189eaa7d-b29a-43a9-b1fb-7658990df940)]
+interface mozIMozIntlHelper : nsISupports
+{
+ [implicit_jscontext] void addGetCalendarInfo(in jsval intlObject);
+
+ /**
+ * Adds a MozDateTimeFormat contructor to the given object.
+ *
+ * The difference between regular Intl.DateTimeFormat and the method created here
+ * is that we support two more options:
+ *
+ * timeStyle: full | long | medium | short
+ * dateStyle: full | long | medium | short
+ *
+ * which allow user to create normalized date/time style formats.
+ * Additionally, when those options are used instead of the regular atomic
+ * options (hour, minute, month, etc.) this code will look into host
+ * Operating System regional preferences and adjust for that.
+ *
+ * That means that if user will manually select time format (hour12/24) or
+ * adjust how the date should be displayed, MozDateTimeFormat will use that.
+ *
+ * This API should be used everywhere in the UI instead of regular Intl API.
+ */
+ [implicit_jscontext] void addDateTimeFormatConstructor(in jsval intlObject);
+
+ /**
+ * Adds a MozDisplayNames contructor to the given object.
+ *
+ * The difference between regular Intl.DisplayNames and the method created here
+ * is that we additionally support the following values for the "type" option:
+ *
+ * weekday
+ * month
+ * quarter
+ * dayPeriod
+ *
+ * And we additionally support "abbreviated" for the "style" option.
+ *
+ * MozDisplayNames.prototype.of accepts the following inputs for these options:
+ *
+ * weekday: an integer in the range 1 = Monday to 7 = Sunday.
+ * month: an integer in the range 1 = January to 13 = Undecimber.
+ * quarter: an integer in the range 1 to 4.
+ * dayPeriod: a string from the set {"am", "pm"}.
+ */
+ [implicit_jscontext] void addDisplayNamesConstructor(in jsval intlObject);
+
+ [implicit_jscontext] boolean stringHasRTLChars(in jsval str);
+};
diff --git a/toolkit/components/mozintl/mozIntl.sys.mjs b/toolkit/components/mozintl/mozIntl.sys.mjs
new file mode 100644
index 0000000000..50633e365c
--- /dev/null
+++ b/toolkit/components/mozintl/mozIntl.sys.mjs
@@ -0,0 +1,1108 @@
+/* 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/. */
+
+const mozIntlHelper = Cc["@mozilla.org/mozintlhelper;1"].getService(
+ Ci.mozIMozIntlHelper
+);
+const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+);
+
+/**
+ * RegExp used to parse variant subtags from a BCP47 language tag.
+ * For example: ca-valencia
+ */
+const variantSubtagsMatch = /(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))+$/;
+
+function getDateTimePatternStyle(option) {
+ switch (option) {
+ case "full":
+ return osPrefs.dateTimeFormatStyleFull;
+ case "long":
+ return osPrefs.dateTimeFormatStyleLong;
+ case "medium":
+ return osPrefs.dateTimeFormatStyleMedium;
+ case "short":
+ return osPrefs.dateTimeFormatStyleShort;
+ default:
+ return osPrefs.dateTimeFormatStyleNone;
+ }
+}
+
+/**
+ * Number of milliseconds in other time units.
+ *
+ * This is used by relative time format best unit
+ * calculations.
+ */
+const second = 1e3;
+const minute = 6e4;
+const hour = 36e5;
+const day = 864e5;
+
+/**
+ * Use by RelativeTimeFormat.
+ *
+ * Allows for defining a cached getter to perform
+ * calculations only once.
+ *
+ * @param {Object} obj - Object to place the getter on.
+ * @param {String} prop - Name of the property.
+ * @param {Function} get - Function that will be used as a getter.
+ */
+function defineCachedGetter(obj, prop, get) {
+ defineGetter(obj, prop, function () {
+ if (!this._[prop]) {
+ this._[prop] = get.call(this);
+ }
+ return this._[prop];
+ });
+}
+
+/**
+ * Used by RelativeTimeFormat.
+ *
+ * Defines a getter on an object
+ *
+ * @param {Object} obj - Object to place the getter on.
+ * @param {String} prop - Name of the property.
+ * @param {Function} get - Function that will be used as a getter.
+ */
+function defineGetter(obj, prop, get) {
+ Object.defineProperty(obj, prop, { get, enumerable: true });
+}
+
+/**
+ * Used by RelativeTimeFormat.
+ *
+ * Allows for calculation of the beginning of
+ * a period for discrete distances.
+ *
+ * @param {Date} date - Date of which we're looking to find a start of.
+ * @param {String} unit - Period to calculate the start of.
+ *
+ * @returns {Date}
+ */
+function startOf(date, unit) {
+ date = new Date(date.getTime());
+ switch (unit) {
+ case "year":
+ date.setMonth(0);
+ // falls through
+ case "month":
+ date.setDate(1);
+ // falls through
+ case "day":
+ date.setHours(0);
+ // falls through
+ case "hour":
+ date.setMinutes(0);
+ // falls through
+ case "minute":
+ date.setSeconds(0);
+ // falls through
+ case "second":
+ date.setMilliseconds(0);
+ }
+ return date;
+}
+
+/**
+ * Used by RelativeTimeFormat.
+ *
+ * Calculates the best fit unit to use for an absolute diff distance based
+ * on thresholds.
+ *
+ * @param {Object} absDiff - Object with absolute diff per unit calculated.
+ *
+ * @returns {String}
+ */
+function bestFit(absDiff) {
+ switch (true) {
+ case absDiff.years > 0 && absDiff.months > threshold.month:
+ return "year";
+ case absDiff.months > 0 && absDiff.weeks > threshold.week:
+ return "month";
+ case absDiff.weeks > 0 && absDiff.days > threshold.day:
+ return "week";
+ case absDiff.days > 0 && absDiff.hours > threshold.hour:
+ return "day";
+ case absDiff.hours > 0 && absDiff.minutes > threshold.minute:
+ return "hour";
+ case absDiff.minutes > 0 && absDiff.seconds > threshold.second:
+ return "minute";
+ default:
+ return "second";
+ }
+}
+
+/**
+ * Used by RelativeTimeFormat.
+ *
+ * Thresholds to use for calculating the best unit for relative time fromatting.
+ */
+const threshold = {
+ month: 2, // at least 2 months before using year.
+ week: 3, // at least 3 weeks before using month.
+ day: 6, // at least 6 days before using week.
+ hour: 6, // at least 6 hours before using day.
+ minute: 59, // at least 59 minutes before using hour.
+ second: 59, // at least 59 seconds before using minute.
+};
+
+/**
+ * Notice: If you're updating this list, you should also
+ * update the list in
+ * languageNames.ftl and regionNames.ftl.
+ */
+const availableLocaleDisplayNames = {
+ region: new Set([
+ "ad",
+ "ae",
+ "af",
+ "ag",
+ "ai",
+ "al",
+ "am",
+ "ao",
+ "aq",
+ "ar",
+ "as",
+ "at",
+ "au",
+ "aw",
+ "az",
+ "ba",
+ "bb",
+ "bd",
+ "be",
+ "bf",
+ "bg",
+ "bh",
+ "bi",
+ "bj",
+ "bl",
+ "bm",
+ "bn",
+ "bo",
+ "bq",
+ "br",
+ "bs",
+ "bt",
+ "bv",
+ "bw",
+ "by",
+ "bz",
+ "ca",
+ "cc",
+ "cd",
+ "cf",
+ "cg",
+ "ch",
+ "ci",
+ "ck",
+ "cl",
+ "cm",
+ "cn",
+ "co",
+ "cp",
+ "cr",
+ "cu",
+ "cv",
+ "cw",
+ "cx",
+ "cy",
+ "cz",
+ "de",
+ "dg",
+ "dj",
+ "dk",
+ "dm",
+ "do",
+ "dz",
+ "ec",
+ "ee",
+ "eg",
+ "eh",
+ "er",
+ "es",
+ "et",
+ "fi",
+ "fj",
+ "fk",
+ "fm",
+ "fo",
+ "fr",
+ "ga",
+ "gb",
+ "gd",
+ "ge",
+ "gf",
+ "gg",
+ "gh",
+ "gi",
+ "gl",
+ "gm",
+ "gn",
+ "gp",
+ "gq",
+ "gr",
+ "gs",
+ "gt",
+ "gu",
+ "gw",
+ "gy",
+ "hk",
+ "hm",
+ "hn",
+ "hr",
+ "ht",
+ "hu",
+ "id",
+ "ie",
+ "il",
+ "im",
+ "in",
+ "io",
+ "iq",
+ "ir",
+ "is",
+ "it",
+ "je",
+ "jm",
+ "jo",
+ "jp",
+ "ke",
+ "kg",
+ "kh",
+ "ki",
+ "km",
+ "kn",
+ "kp",
+ "kr",
+ "kw",
+ "ky",
+ "kz",
+ "la",
+ "lb",
+ "lc",
+ "li",
+ "lk",
+ "lr",
+ "ls",
+ "lt",
+ "lu",
+ "lv",
+ "ly",
+ "ma",
+ "mc",
+ "md",
+ "me",
+ "mf",
+ "mg",
+ "mh",
+ "mk",
+ "ml",
+ "mm",
+ "mn",
+ "mo",
+ "mp",
+ "mq",
+ "mr",
+ "ms",
+ "mt",
+ "mu",
+ "mv",
+ "mw",
+ "mx",
+ "my",
+ "mz",
+ "na",
+ "nc",
+ "ne",
+ "nf",
+ "ng",
+ "ni",
+ "nl",
+ "no",
+ "np",
+ "nr",
+ "nu",
+ "nz",
+ "om",
+ "pa",
+ "pe",
+ "pf",
+ "pg",
+ "ph",
+ "pk",
+ "pl",
+ "pm",
+ "pn",
+ "pr",
+ "pt",
+ "pw",
+ "py",
+ "qa",
+ "qm",
+ "qs",
+ "qu",
+ "qw",
+ "qx",
+ "qz",
+ "re",
+ "ro",
+ "rs",
+ "ru",
+ "rw",
+ "sa",
+ "sb",
+ "sc",
+ "sd",
+ "se",
+ "sg",
+ "sh",
+ "si",
+ "sk",
+ "sl",
+ "sm",
+ "sn",
+ "so",
+ "sr",
+ "ss",
+ "st",
+ "sv",
+ "sx",
+ "sy",
+ "sz",
+ "tc",
+ "td",
+ "tf",
+ "tg",
+ "th",
+ "tj",
+ "tk",
+ "tl",
+ "tm",
+ "tn",
+ "to",
+ "tr",
+ "tt",
+ "tv",
+ "tw",
+ "tz",
+ "ua",
+ "ug",
+ "us",
+ "uy",
+ "uz",
+ "va",
+ "vc",
+ "ve",
+ "vg",
+ "vi",
+ "vn",
+ "vu",
+ "wf",
+ "ws",
+ "xa",
+ "xb",
+ "xc",
+ "xd",
+ "xe",
+ "xg",
+ "xh",
+ "xj",
+ "xk",
+ "xl",
+ "xm",
+ "xp",
+ "xq",
+ "xr",
+ "xs",
+ "xt",
+ "xu",
+ "xv",
+ "xw",
+ "ye",
+ "yt",
+ "za",
+ "zm",
+ "zw",
+ ]),
+ language: new Set([
+ "aa",
+ "ab",
+ "ach",
+ "ae",
+ "af",
+ "ak",
+ "am",
+ "an",
+ "ar",
+ "as",
+ "ast",
+ "av",
+ "ay",
+ "az",
+ "ba",
+ "be",
+ "bg",
+ "bh",
+ "bi",
+ "bm",
+ "bn",
+ "bo",
+ "br",
+ "bs",
+ "ca",
+ "cak",
+ "ce",
+ "ch",
+ "co",
+ "cr",
+ "crh",
+ "cs",
+ "csb",
+ "cu",
+ "cv",
+ "cy",
+ "da",
+ "de",
+ "dsb",
+ "dv",
+ "dz",
+ "ee",
+ "el",
+ "en",
+ "eo",
+ "es",
+ "et",
+ "eu",
+ "fa",
+ "ff",
+ "fi",
+ "fj",
+ "fo",
+ "fr",
+ "fur",
+ "fy",
+ "ga",
+ "gd",
+ "gl",
+ "gn",
+ "gu",
+ "gv",
+ "ha",
+ "haw",
+ "he",
+ "hi",
+ "hil",
+ "ho",
+ "hr",
+ "hsb",
+ "ht",
+ "hu",
+ "hy",
+ "hz",
+ "ia",
+ "id",
+ "ie",
+ "ig",
+ "ii",
+ "ik",
+ "io",
+ "is",
+ "it",
+ "iu",
+ "ja",
+ "jv",
+ "ka",
+ "kab",
+ "kg",
+ "ki",
+ "kj",
+ "kk",
+ "kl",
+ "km",
+ "kn",
+ "ko",
+ "kok",
+ "kr",
+ "ks",
+ "ku",
+ "kv",
+ "kw",
+ "ky",
+ "la",
+ "lb",
+ "lg",
+ "li",
+ "lij",
+ "ln",
+ "lo",
+ "lt",
+ "ltg",
+ "lu",
+ "lv",
+ "mai",
+ "meh",
+ "mg",
+ "mh",
+ "mi",
+ "mix",
+ "mk",
+ "ml",
+ "mn",
+ "mr",
+ "ms",
+ "mt",
+ "my",
+ "na",
+ "nb",
+ "nd",
+ "ne",
+ "ng",
+ "nl",
+ "nn",
+ "no",
+ "nr",
+ "nso",
+ "nv",
+ "ny",
+ "oc",
+ "oj",
+ "om",
+ "or",
+ "os",
+ "pa",
+ "pi",
+ "pl",
+ "ps",
+ "pt",
+ "qu",
+ "rm",
+ "rn",
+ "ro",
+ "ru",
+ "rw",
+ "sa",
+ "sat",
+ "sc",
+ "sco",
+ "sd",
+ "se",
+ "sg",
+ "si",
+ "sk",
+ "sl",
+ "sm",
+ "sn",
+ "so",
+ "son",
+ "sq",
+ "sr",
+ "ss",
+ "st",
+ "su",
+ "sv",
+ "sw",
+ "szl",
+ "ta",
+ "te",
+ "tg",
+ "th",
+ "ti",
+ "tig",
+ "tk",
+ "tl",
+ "tlh",
+ "tn",
+ "to",
+ "tr",
+ "trs",
+ "ts",
+ "tt",
+ "tw",
+ "ty",
+ "ug",
+ "uk",
+ "ur",
+ "uz",
+ "ve",
+ "vi",
+ "vo",
+ "wa",
+ "wen",
+ "wo",
+ "xh",
+ "yi",
+ "yo",
+ "za",
+ "zam",
+ "zh",
+ "zu",
+ ]),
+};
+
+/**
+ * Notice: If you're updating these names, you should also update the data
+ * in python/mozbuild/mozbuild/action/langpack_localeNames.json.
+ */
+const nativeLocaleNames = new Map(
+ Object.entries({
+ ach: "Acholi",
+ af: "Afrikaans",
+ an: "Aragonés",
+ ar: "العربية",
+ ast: "Asturianu",
+ az: "Azərbaycanca",
+ be: "Беларуская",
+ bg: "Български",
+ bn: "বাংলা",
+ bo: "བོད་སྐད",
+ br: "Brezhoneg",
+ brx: "बड़ो",
+ bs: "Bosanski",
+ ca: "Català",
+ "ca-valencia": "Català (Valencià)",
+ cak: "Kaqchikel",
+ cs: "Čeština",
+ cy: "Cymraeg",
+ da: "Dansk",
+ de: "Deutsch",
+ dsb: "Dolnoserbšćina",
+ el: "Ελληνικά",
+ "en-CA": "English (CA)",
+ "en-GB": "English (GB)",
+ "en-US": "English (US)",
+ eo: "Esperanto",
+ "es-AR": "Español (AR)",
+ "es-CL": "Español (CL)",
+ "es-ES": "Español (ES)",
+ "es-MX": "Español (MX)",
+ et: "Eesti",
+ eu: "Euskara",
+ fa: "فارسی",
+ ff: "Pulaar",
+ fi: "Suomi",
+ fr: "Français",
+ fur: "Furlan",
+ "fy-NL": "Frysk",
+ "ga-IE": "Gaeilge",
+ gd: "Gàidhlig",
+ gl: "Galego",
+ gn: "Guarani",
+ "gu-IN": "ગુજરાતી",
+ he: "עברית",
+ "hi-IN": "हिन्दी",
+ hr: "Hrvatski",
+ hsb: "Hornjoserbšćina",
+ hu: "Magyar",
+ "hy-AM": "հայերեն",
+ ia: "Interlingua",
+ id: "Indonesia",
+ is: "Islenska",
+ it: "Italiano",
+ ja: "日本語",
+ "ja-JP-mac": "日本語",
+ ka: "ქართული",
+ kab: "Taqbaylit",
+ kk: "қазақ тілі",
+ km: "ខ្មែរ",
+ kn: "ಕನ್ನಡ",
+ ko: "한국어",
+ lij: "Ligure",
+ lo: "ລາວ",
+ lt: "Lietuvių",
+ ltg: "Latgalīšu",
+ lv: "Latviešu",
+ mk: "македонски",
+ ml: "മലയാളം",
+ mr: "मराठी",
+ ms: "Melayu",
+ my: "မြန်မာ",
+ "nb-NO": "Norsk Bokmål",
+ "ne-NP": "नेपाली",
+ nl: "Nederlands",
+ "nn-NO": "Nynorsk",
+ oc: "Occitan",
+ or: "ଓଡ଼ିଆ",
+ "pa-IN": "ਪੰਜਾਬੀ",
+ pl: "Polski",
+ "pt-BR": "Português (BR)",
+ "pt-PT": "Português (PT)",
+ rm: "Rumantsch",
+ ro: "Română",
+ ru: "Русский",
+ sat: "ᱥᱟᱱᱛᱟᱲᱤ",
+ sc: "Sardu",
+ sco: "Scots",
+ si: "සිංහල",
+ sk: "Slovenčina",
+ sl: "Slovenščina",
+ son: "Soŋay",
+ sq: "Shqip",
+ sr: "Cрпски",
+ "sv-SE": "Svenska",
+ szl: "Ślōnsko",
+ ta: "தமிழ்",
+ te: "తెలుగు",
+ tg: "Тоҷикӣ",
+ th: "ไทย",
+ tl: "Tagalog",
+ tr: "Türkçe",
+ trs: "Triqui",
+ uk: "Українська",
+ ur: "اردو",
+ uz: "O‘zbek",
+ vi: "Tiếng Việt",
+ wo: "Wolof",
+ xh: "IsiXhosa",
+ "zh-CN": "简体中文",
+ "zh-TW": "正體中文",
+ })
+);
+
+class MozRelativeTimeFormat extends Intl.RelativeTimeFormat {
+ constructor(locales, options = {}, ...args) {
+ // If someone is asking for MozRelativeTimeFormat, it's likely they'll want
+ // to use `formatBestUnit` which works better with `auto`
+ if (options.numeric === undefined) {
+ options.numeric = "auto";
+ }
+ super(locales, options, ...args);
+ }
+
+ formatBestUnit(date, { now = new Date() } = {}) {
+ const diff = {
+ _: {},
+ ms: date.getTime() - now.getTime(),
+ years: date.getFullYear() - now.getFullYear(),
+ };
+
+ defineCachedGetter(diff, "months", function () {
+ return this.years * 12 + date.getMonth() - now.getMonth();
+ });
+ defineCachedGetter(diff, "weeks", function () {
+ return Math.trunc(this.days / 7);
+ });
+ defineCachedGetter(diff, "days", function () {
+ return Math.trunc((startOf(date, "day") - startOf(now, "day")) / day);
+ });
+ defineCachedGetter(diff, "hours", function () {
+ return Math.trunc((startOf(date, "hour") - startOf(now, "hour")) / hour);
+ });
+ defineCachedGetter(diff, "minutes", function () {
+ return Math.trunc(
+ (startOf(date, "minute") - startOf(now, "minute")) / minute
+ );
+ });
+ defineCachedGetter(diff, "seconds", function () {
+ return Math.trunc(
+ (startOf(date, "second") - startOf(now, "second")) / second
+ );
+ });
+
+ const absDiff = {
+ _: {},
+ };
+
+ for (let prop of Object.keys(diff)) {
+ defineGetter(absDiff, prop, function () {
+ return Math.abs(diff[prop]);
+ });
+ }
+ const unit = bestFit(absDiff);
+
+ switch (unit) {
+ case "year":
+ return this.format(diff.years, unit);
+ case "month":
+ return this.format(diff.months, unit);
+ case "week":
+ return this.format(diff.weeks, unit);
+ case "day":
+ return this.format(diff.days, unit);
+ case "hour":
+ return this.format(diff.hours, unit);
+ case "minute":
+ return this.format(diff.minutes, unit);
+ default:
+ if (unit !== "second") {
+ throw new TypeError(`Unsupported unit "${unit}"`);
+ }
+ return this.format(diff.seconds, unit);
+ }
+ }
+}
+
+export class MozIntl {
+ Collator = Intl.Collator;
+ ListFormat = Intl.ListFormat;
+ Locale = Intl.Locale;
+ NumberFormat = Intl.NumberFormat;
+ PluralRules = Intl.PluralRules;
+ RelativeTimeFormat = MozRelativeTimeFormat;
+
+ constructor() {
+ this._cache = {};
+ Services.obs.addObserver(this, "intl:app-locales-changed", true);
+ }
+
+ observe() {
+ // Clear cache when things change.
+ this._cache = {};
+ }
+
+ getCalendarInfo(locales, ...args) {
+ if (!this._cache.hasOwnProperty("getCalendarInfo")) {
+ mozIntlHelper.addGetCalendarInfo(this._cache);
+ }
+
+ return this._cache.getCalendarInfo(locales, ...args);
+ }
+
+ getDisplayNamesDeprecated(locales, options = {}) {
+ // Helper for IntlUtils.webidl, will be removed once Intl.DisplayNames is
+ // available in non-privileged code.
+
+ let { type, style, calendar, keys = [] } = options;
+
+ let dn = new this.DisplayNames(locales, { type, style, calendar });
+ let {
+ locale: resolvedLocale,
+ type: resolvedType,
+ style: resolvedStyle,
+ calendar: resolvedCalendar,
+ } = dn.resolvedOptions();
+ let values = keys.map(key => dn.of(key));
+
+ return {
+ locale: resolvedLocale,
+ type: resolvedType,
+ style: resolvedStyle,
+ calendar: resolvedCalendar,
+ values,
+ };
+ }
+
+ getAvailableLocaleDisplayNames(type) {
+ if (availableLocaleDisplayNames.hasOwnProperty(type)) {
+ return Array.from(availableLocaleDisplayNames[type]);
+ }
+
+ return new Error("Unimplemented!");
+ }
+
+ getLanguageDisplayNames(locales, langCodes) {
+ if (locales !== undefined) {
+ throw new Error("First argument support not implemented yet");
+ }
+
+ if (!this._cache.hasOwnProperty("languageLocalization")) {
+ const loc = new Localization(["toolkit/intl/languageNames.ftl"], true);
+ this._cache.languageLocalization = loc;
+ }
+
+ const loc = this._cache.languageLocalization;
+
+ return langCodes.map(langCode => {
+ if (typeof langCode !== "string") {
+ throw new TypeError("All language codes must be strings.");
+ }
+ let lcLangCode = langCode.toLowerCase();
+ if (availableLocaleDisplayNames.language.has(lcLangCode)) {
+ const value = loc.formatValueSync(`language-name-${lcLangCode}`);
+ if (value !== null) {
+ return value;
+ }
+ }
+ return lcLangCode;
+ });
+ }
+
+ getRegionDisplayNames(locales, regionCodes) {
+ if (locales !== undefined) {
+ throw new Error("First argument support not implemented yet");
+ }
+
+ if (!this._cache.hasOwnProperty("regionLocalization")) {
+ const loc = new Localization(["toolkit/intl/regionNames.ftl"], true);
+ this._cache.regionLocalization = loc;
+ }
+
+ const loc = this._cache.regionLocalization;
+
+ return regionCodes.map(regionCode => {
+ if (typeof regionCode !== "string") {
+ throw new TypeError("All region codes must be strings.");
+ }
+ let lcRegionCode = regionCode.toLowerCase();
+ if (availableLocaleDisplayNames.region.has(lcRegionCode)) {
+ let regionID;
+ // Allow changing names over time for specific regions
+ switch (lcRegionCode) {
+ case "bq":
+ regionID = "region-name-bq-2018";
+ break;
+ case "cv":
+ regionID = "region-name-cv-2020";
+ break;
+ case "cz":
+ regionID = "region-name-cz-2019";
+ break;
+ case "mk":
+ regionID = "region-name-mk-2019";
+ break;
+ case "sz":
+ regionID = "region-name-sz-2019";
+ break;
+ default:
+ regionID = `region-name-${lcRegionCode}`;
+ }
+
+ const value = loc.formatValueSync(regionID);
+ if (value !== null) {
+ return value;
+ }
+ }
+ return regionCode.toUpperCase();
+ });
+ }
+
+ getLocaleDisplayNames(locales, localeCodes, options = {}) {
+ const { preferNative = false } = options;
+
+ if (locales !== undefined) {
+ throw new Error("First argument support not implemented yet");
+ }
+ // Patterns hardcoded from CLDR 33 english.
+ // We can later look into fetching them from CLDR directly.
+ const localePattern = "{0} ({1})";
+ const localeSeparator = ", ";
+
+ return localeCodes.map(localeCode => {
+ if (typeof localeCode !== "string") {
+ throw new TypeError("All locale codes must be strings.");
+ }
+
+ if (preferNative && nativeLocaleNames.has(localeCode)) {
+ return nativeLocaleNames.get(localeCode);
+ }
+
+ let locale;
+ try {
+ locale = new Intl.Locale(localeCode.replaceAll("_", "-"));
+ } catch {
+ return localeCode;
+ }
+
+ const {
+ language: languageSubtag,
+ script: scriptSubtag,
+ region: regionSubtag,
+ } = locale;
+
+ const variantSubtags = locale.baseName.match(variantSubtagsMatch);
+
+ const displayName = [
+ this.getLanguageDisplayNames(locales, [languageSubtag])[0],
+ ];
+
+ if (scriptSubtag) {
+ displayName.push(scriptSubtag);
+ }
+
+ if (regionSubtag) {
+ displayName.push(
+ this.getRegionDisplayNames(locales, [regionSubtag])[0]
+ );
+ }
+
+ if (variantSubtags) {
+ displayName.push(...variantSubtags[0].substr(1).split("-")); // Collapse multiple variants.
+ }
+
+ let modifiers;
+ if (displayName.length === 1) {
+ return displayName[0];
+ } else if (displayName.length > 2) {
+ modifiers = displayName.slice(1).join(localeSeparator);
+ } else {
+ modifiers = displayName[1];
+ }
+ return localePattern
+ .replace("{0}", displayName[0])
+ .replace("{1}", modifiers);
+ });
+ }
+
+ getScriptDirection(locale) {
+ // This is a crude implementation until Bug 1693576 lands.
+ // See justification in toolkit/components/mozintl/mozIMozIntl.idl
+ const { language } = new Intl.Locale(locale);
+ if (
+ language == "ar" ||
+ language == "ckb" ||
+ language == "fa" ||
+ language == "he" ||
+ language == "ur"
+ ) {
+ return "rtl";
+ }
+ return "ltr";
+ }
+
+ stringHasRTLChars(str) {
+ return mozIntlHelper.stringHasRTLChars(str);
+ }
+
+ get DateTimeFormat() {
+ if (!this._cache.hasOwnProperty("DateTimeFormat")) {
+ mozIntlHelper.addDateTimeFormatConstructor(this._cache);
+
+ const DateTimeFormat = this._cache.DateTimeFormat;
+
+ class MozDateTimeFormat extends DateTimeFormat {
+ constructor(locales, options, ...args) {
+ let resolvedLocales = DateTimeFormat.supportedLocalesOf(locales);
+ if (options) {
+ if (options.dateStyle || options.timeStyle) {
+ options.pattern = osPrefs.getDateTimePattern(
+ getDateTimePatternStyle(options.dateStyle),
+ getDateTimePatternStyle(options.timeStyle),
+ resolvedLocales[0]
+ );
+ } else {
+ // make sure that user doesn't pass a pattern explicitly
+ options.pattern = undefined;
+ }
+ }
+ super(resolvedLocales, options, ...args);
+ }
+ }
+ this._cache.MozDateTimeFormat = MozDateTimeFormat;
+ }
+
+ return this._cache.MozDateTimeFormat;
+ }
+
+ get DisplayNames() {
+ if (!this._cache.hasOwnProperty("DisplayNames")) {
+ mozIntlHelper.addDisplayNamesConstructor(this._cache);
+ }
+
+ return this._cache.DisplayNames;
+ }
+}
+
+MozIntl.prototype.classID = Components.ID(
+ "{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}"
+);
+MozIntl.prototype.QueryInterface = ChromeUtils.generateQI([
+ "mozIMozIntl",
+ "nsIObserver",
+ "nsISupportsWeakReference",
+]);
diff --git a/toolkit/components/mozintl/test/test_mozintl.js b/toolkit/components/mozintl/test/test_mozintl.js
new file mode 100644
index 0000000000..dc7b8a7afd
--- /dev/null
+++ b/toolkit/components/mozintl/test/test_mozintl.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ test_methods_presence();
+ test_methods_calling();
+ test_constructors();
+ test_rtf_formatBestUnit();
+ test_datetimeformat();
+ test_getLanguageDirection();
+ test_stringHasRTLChars();
+
+ ok(true);
+}
+
+function test_methods_presence() {
+ equal(Services.intl.getCalendarInfo instanceof Function, true);
+ equal(Services.intl.getDisplayNamesDeprecated instanceof Function, true);
+ equal(Services.intl.getLocaleDisplayNames instanceof Function, true);
+}
+
+function test_methods_calling() {
+ Services.intl.getCalendarInfo("pl");
+ Services.intl.getDisplayNamesDeprecated("ar", { type: "language" });
+ new Services.intl.DateTimeFormat("fr");
+ new Services.intl.DisplayNames("fr", { type: "language" });
+ new Services.intl.ListFormat("fr");
+ new Services.intl.Locale("fr");
+ new Services.intl.RelativeTimeFormat("fr");
+ ok(true);
+}
+
+function test_constructors() {
+ let constructors = [
+ "Collator",
+ "DateTimeFormat",
+ "ListFormat",
+ "NumberFormat",
+ "PluralRules",
+ ];
+
+ constructors.forEach(constructor => {
+ let obj = new Intl[constructor]();
+ let obj2 = new Services.intl[constructor]();
+
+ equal(typeof obj, typeof obj2);
+ });
+}
+
+function testRTFBestUnit(anchor, value, expected) {
+ let rtf = new Services.intl.RelativeTimeFormat("en-US");
+ deepEqual(rtf.formatBestUnit(new Date(value), { now: anchor }), expected);
+}
+
+function test_rtf_formatBestUnit() {
+ {
+ // format seconds-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2016-04-10 11:59:01", "59 seconds ago");
+ testRTFBestUnit(anchor, "2016-04-10 12:00:00", "now");
+ testRTFBestUnit(anchor, "2016-04-10 12:00:59", "in 59 seconds");
+ }
+
+ {
+ // format minutes-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2016-04-10 11:01:00", "59 minutes ago");
+ testRTFBestUnit(anchor, "2016-04-10 11:59", "1 minute ago");
+ testRTFBestUnit(anchor, "2016-04-10 12:01", "in 1 minute");
+ testRTFBestUnit(anchor, "2016-04-10 12:01:59", "in 1 minute");
+ testRTFBestUnit(anchor, "2016-04-10 12:59:59", "in 59 minutes");
+ }
+
+ {
+ // format hours-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2016-04-10 00:00", "12 hours ago");
+ testRTFBestUnit(anchor, "2016-04-10 13:00", "in 1 hour");
+ testRTFBestUnit(anchor, "2016-04-10 13:59:59", "in 1 hour");
+ testRTFBestUnit(anchor, "2016-04-10 23:59:59", "in 11 hours");
+
+ anchor = new Date("2016-04-10 01:00");
+ testRTFBestUnit(anchor, "2016-04-09 19:00", "6 hours ago");
+ testRTFBestUnit(anchor, "2016-04-09 18:00", "yesterday");
+
+ anchor = new Date("2016-04-10 23:00");
+ testRTFBestUnit(anchor, "2016-04-11 05:00", "in 6 hours");
+ testRTFBestUnit(anchor, "2016-04-11 06:00", "tomorrow");
+
+ anchor = new Date("2016-01-31 23:00");
+ testRTFBestUnit(anchor, "2016-02-01 05:00", "in 6 hours");
+ testRTFBestUnit(anchor, "2016-02-01 07:00", "tomorrow");
+
+ anchor = new Date("2016-12-31 23:00");
+ testRTFBestUnit(anchor, "2017-01-01 05:00", "in 6 hours");
+ testRTFBestUnit(anchor, "2017-01-01 07:00", "tomorrow");
+ }
+
+ {
+ // format days-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2016-04-01 00:00", "last week");
+ testRTFBestUnit(anchor, "2016-04-05 00:00", "5 days ago");
+ testRTFBestUnit(anchor, "2016-04-09 18:00", "yesterday");
+ testRTFBestUnit(anchor, "2016-04-11 09:00", "tomorrow");
+ testRTFBestUnit(anchor, "2016-04-30 23:59", "in 2 weeks");
+ testRTFBestUnit(anchor, "2016-03-31 23:59", "last week");
+ testRTFBestUnit(anchor, "2016-04-18 23:59", "next week");
+ testRTFBestUnit(anchor, "2016-03-03 23:59", "last month");
+ testRTFBestUnit(anchor, "2016-05-12 00:00", "next month");
+
+ anchor = new Date("2016-04-06 12:00");
+ testRTFBestUnit(anchor, "2016-03-31 23:59", "6 days ago");
+
+ anchor = new Date("2016-04-25 23:00");
+ testRTFBestUnit(anchor, "2016-05-01 00:00", "in 6 days");
+ }
+
+ {
+ // format months-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2016-01-01 00:00", "3 months ago");
+ testRTFBestUnit(anchor, "2016-03-01 00:00", "last month");
+ testRTFBestUnit(anchor, "2016-05-11 00:00", "next month");
+ testRTFBestUnit(anchor, "2016-12-01 23:59", "in 8 months");
+
+ anchor = new Date("2017-01-12 18:30");
+ testRTFBestUnit(anchor, "2016-12-14 18:30", "last month");
+
+ anchor = new Date("2016-12-14 18:30");
+ testRTFBestUnit(anchor, "2017-01-12 18:30", "next month");
+
+ anchor = new Date("2016-02-28 12:00");
+ testRTFBestUnit(anchor, "2015-12-31 23:59", "2 months ago");
+ }
+
+ {
+ // format year-distant dates
+ let anchor = new Date("2016-04-10 12:00:00");
+ testRTFBestUnit(anchor, "2014-04-01 00:00", "2 years ago");
+ testRTFBestUnit(anchor, "2015-03-01 00:00", "last year");
+ testRTFBestUnit(anchor, "2017-05-01 00:00", "next year");
+ testRTFBestUnit(anchor, "2024-12-01 23:59", "in 8 years");
+
+ anchor = new Date("2017-01-12 18:30");
+ testRTFBestUnit(anchor, "2016-01-01 18:30", "last year");
+ testRTFBestUnit(anchor, "2015-12-29 18:30", "2 years ago");
+
+ anchor = new Date("2016-12-29 18:30");
+ testRTFBestUnit(anchor, "2017-07-12 18:30", "next year");
+ testRTFBestUnit(anchor, "2017-02-12 18:30", "in 2 months");
+ testRTFBestUnit(anchor, "2018-01-02 18:30", "in 2 years");
+
+ testRTFBestUnit(anchor, "2098-01-02 18:30", "in 82 years");
+ }
+}
+
+function test_datetimeformat() {
+ Services.prefs.setStringPref(
+ "intl.date_time.pattern_override.date_long",
+ "yyyy年M月d日"
+ );
+
+ let formatted = new Services.intl.DateTimeFormat("ja", {
+ dateStyle: "long",
+ }).format(new Date("2020-12-08 21:00:05"));
+
+ equal(formatted, "2020年12月8日");
+
+ Services.prefs.clearUserPref("intl.date_time.pattern_override.date_long");
+}
+
+function test_getLanguageDirection() {
+ equal(Services.intl.getScriptDirection("ar"), "rtl");
+ equal(Services.intl.getScriptDirection("ar-EG"), "rtl");
+ equal(Services.intl.getScriptDirection("ckb"), "rtl");
+ equal(Services.intl.getScriptDirection("fa"), "rtl");
+ equal(Services.intl.getScriptDirection("he"), "rtl");
+ equal(Services.intl.getScriptDirection("ur"), "rtl");
+
+ equal(Services.intl.getScriptDirection("en"), "ltr");
+ equal(Services.intl.getScriptDirection("en-US"), "ltr");
+ equal(Services.intl.getScriptDirection("fr"), "ltr");
+}
+
+function test_stringHasRTLChars() {
+ equal(Services.intl.stringHasRTLChars(""), false);
+ equal(Services.intl.stringHasRTLChars("a"), false);
+ equal(Services.intl.stringHasRTLChars("أهلا"), true);
+ equal(Services.intl.stringHasRTLChars(">\u202e<"), true);
+
+ const invalidArgs = [undefined, null, false, 42, {}];
+ for (const invalidArg of invalidArgs) {
+ try {
+ Services.intl.stringHasRTLChars(invalidArg);
+ ok(
+ false,
+ `stringHasRTLChars should throw when called with ${invalidArg}`
+ );
+ } catch (e) {
+ ok(true, `stringHasRTLChars throws when called with ${invalidArg}`);
+ }
+ }
+}
diff --git a/toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js b/toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js
new file mode 100644
index 0000000000..e275a46e61
--- /dev/null
+++ b/toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const fs = [
+ {
+ path: "resource://mock_source/toolkit/intl/languageNames.ftl",
+ source: `
+language-name-en = English
+ `,
+ },
+ {
+ path: "resource://mock_source/toolkit/intl/regionNames.ftl",
+ source: `
+region-name-us = United States
+region-name-ru = Russia
+ `,
+ },
+];
+
+let locales = Services.locale.packagedLocales;
+const mockSource = L10nFileSource.createMock(
+ "mock",
+ "app",
+ locales,
+ "resource://mock_source",
+ fs
+);
+L10nRegistry.getInstance().registerSources([mockSource]);
+
+const gLangDN = Services.intl.getLanguageDisplayNames.bind(
+ Services.intl,
+ undefined
+);
+const gAvLocDN = Services.intl.getAvailableLocaleDisplayNames.bind(
+ Services.intl
+);
+const gRegDN = Services.intl.getRegionDisplayNames.bind(
+ Services.intl,
+ undefined
+);
+const gLocDN = Services.intl.getLocaleDisplayNames.bind(
+ Services.intl,
+ undefined
+);
+
+add_test(function test_native_tag() {
+ const options = { preferNative: true };
+ deepEqual(gLocDN([], options), []);
+ deepEqual(gLocDN(["ca-valencia"], options), ["Català (Valencià)"]);
+ deepEqual(gLocDN(["en-US"], options), ["English (US)"]);
+ deepEqual(gLocDN(["en-RU"], options), ["English (Russia)"]);
+ deepEqual(gLocDN(["ja-JP-mac"], options), ["日本語"]);
+ run_next_test();
+});
+
+add_test(function test_valid_language_tag() {
+ deepEqual(gLocDN([]), []);
+ deepEqual(gLocDN(["en"]), ["English"]);
+ deepEqual(gLocDN(["und"]), ["und"]);
+ run_next_test();
+});
+
+add_test(function test_valid_region_tag() {
+ deepEqual(gLocDN(["en-US"]), ["English (United States)"]);
+ deepEqual(gLocDN(["en-XY"]), ["English (XY)"]);
+ run_next_test();
+});
+
+add_test(function test_valid_script_tag() {
+ deepEqual(gLocDN(["en-Cyrl"]), ["English (Cyrl)"]);
+ deepEqual(gLocDN(["en-Cyrl-RU"]), ["English (Cyrl, Russia)"]);
+ run_next_test();
+});
+
+add_test(function test_valid_variants_tag() {
+ deepEqual(gLocDN(["en-Cyrl-macos"]), ["English (Cyrl, macos)"]);
+ deepEqual(gLocDN(["en-Cyrl-RU-macos"]), ["English (Cyrl, Russia, macos)"]);
+ deepEqual(gLocDN(["en-Cyrl-RU-macos-modern"]), [
+ "English (Cyrl, Russia, macos, modern)",
+ ]);
+ run_next_test();
+});
+
+add_test(function test_other_subtags_ignored() {
+ deepEqual(gLocDN(["en-x-ignore"]), ["English"]);
+ deepEqual(gLocDN(["en-t-en-latn"]), ["English"]);
+ deepEqual(gLocDN(["en-u-hc-h24"]), ["English"]);
+ run_next_test();
+});
+
+add_test(function test_invalid_locales() {
+ deepEqual(gLocDN(["2"]), ["2"]);
+ deepEqual(gLocDN([""]), [""]);
+ Assert.throws(() => gLocDN([2]), /All locale codes must be strings/);
+ Assert.throws(() => gLocDN([{}]), /All locale codes must be strings/);
+ Assert.throws(() => gLocDN([true]), /All locale codes must be strings/);
+ run_next_test();
+});
+
+add_test(function test_language_only() {
+ deepEqual(gLangDN([]), []);
+ deepEqual(gLangDN(["en"]), ["English"]);
+ deepEqual(gLangDN(["und"]), ["und"]);
+ run_next_test();
+});
+
+add_test(function test_invalid_languages() {
+ deepEqual(gLangDN(["2"]), ["2"]);
+ deepEqual(gLangDN([""]), [""]);
+ Assert.throws(() => gLangDN([2]), /All language codes must be strings/);
+ Assert.throws(() => gLangDN([{}]), /All language codes must be strings/);
+ Assert.throws(() => gLangDN([true]), /All language codes must be strings/);
+ run_next_test();
+});
+
+add_test(function test_region_only() {
+ deepEqual(gRegDN([]), []);
+ deepEqual(gRegDN(["US"]), ["United States"]);
+ deepEqual(gRegDN(["und"]), ["UND"]);
+ run_next_test();
+});
+
+add_test(function test_invalid_regions() {
+ deepEqual(gRegDN(["2"]), ["2"]);
+ deepEqual(gRegDN([""]), [""]);
+ Assert.throws(() => gRegDN([2]), /All region codes must be strings/);
+ Assert.throws(() => gRegDN([{}]), /All region codes must be strings/);
+ Assert.throws(() => gRegDN([true]), /All region codes must be strings/);
+ run_next_test();
+});
+
+add_test(function test_availableLocaleDisplayNames() {
+ let langCodes = gAvLocDN("language");
+ equal(
+ !!langCodes.length,
+ true,
+ "There should be some language codes available"
+ );
+ let regCodes = gAvLocDN("region");
+ equal(!!regCodes.length, true, "There should be some region codes available");
+ run_next_test();
+});
diff --git a/toolkit/components/mozintl/test/test_mozintlhelper.js b/toolkit/components/mozintl/test/test_mozintlhelper.js
new file mode 100644
index 0000000000..b233ab611d
--- /dev/null
+++ b/toolkit/components/mozintl/test/test_mozintlhelper.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ const miHelper = Cc["@mozilla.org/mozintlhelper;1"].getService(
+ Ci.mozIMozIntlHelper
+ );
+
+ test_this_global(miHelper);
+ test_cross_global(miHelper);
+ test_methods_presence(miHelper);
+
+ ok(true);
+}
+
+function test_this_global(miHelper) {
+ let x = {};
+
+ miHelper.addGetCalendarInfo(x);
+ equal(x.getCalendarInfo instanceof Function, true);
+ equal(x.getCalendarInfo() instanceof Object, true);
+}
+
+function test_cross_global(miHelper) {
+ var global = new Cu.Sandbox("https://example.com/");
+ var x = global.Object();
+
+ miHelper.addGetCalendarInfo(x);
+ var waivedX = Cu.waiveXrays(x);
+ equal(waivedX.getCalendarInfo instanceof Function, false);
+ equal(
+ waivedX.getCalendarInfo instanceof Cu.waiveXrays(global.Function),
+ true
+ );
+ equal(waivedX.getCalendarInfo() instanceof Object, false);
+ equal(
+ waivedX.getCalendarInfo() instanceof Cu.waiveXrays(global.Object),
+ true
+ );
+}
+
+function test_methods_presence(miHelper) {
+ equal(miHelper.addGetCalendarInfo instanceof Function, true);
+ equal(miHelper.addDateTimeFormatConstructor instanceof Function, true);
+ equal(miHelper.addDisplayNamesConstructor instanceof Function, true);
+
+ let x = {};
+
+ miHelper.addGetCalendarInfo(x);
+ equal(x.getCalendarInfo instanceof Function, true);
+
+ miHelper.addDateTimeFormatConstructor(x);
+ equal(x.DateTimeFormat instanceof Function, true);
+
+ miHelper.addDisplayNamesConstructor(x);
+ equal(x.DisplayNames instanceof Function, true);
+}
diff --git a/toolkit/components/mozintl/test/xpcshell.toml b/toolkit/components/mozintl/test/xpcshell.toml
new file mode 100644
index 0000000000..b84f63e022
--- /dev/null
+++ b/toolkit/components/mozintl/test/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = ""
+
+["test_mozintl.js"]
+
+["test_mozintl_getLocaleDisplayNames.js"]
+
+["test_mozintlhelper.js"]