diff options
Diffstat (limited to 'toolkit/components/mozintl')
-rw-r--r-- | toolkit/components/mozintl/MozIntlHelper.cpp | 115 | ||||
-rw-r--r-- | toolkit/components/mozintl/MozIntlHelper.h | 21 | ||||
-rw-r--r-- | toolkit/components/mozintl/components.conf | 23 | ||||
-rw-r--r-- | toolkit/components/mozintl/moz.build | 31 | ||||
-rw-r--r-- | toolkit/components/mozintl/mozIMozIntl.idl | 105 | ||||
-rw-r--r-- | toolkit/components/mozintl/mozIMozIntlHelper.idl | 65 | ||||
-rw-r--r-- | toolkit/components/mozintl/mozIntl.sys.mjs | 1108 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintl.js | 204 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js | 142 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintlhelper.js | 57 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/xpcshell.toml | 8 |
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"] |