diff options
Diffstat (limited to 'toolkit/components/mozintl')
-rw-r--r-- | toolkit/components/mozintl/MozIntlHelper.cpp | 90 | ||||
-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 | 88 | ||||
-rw-r--r-- | toolkit/components/mozintl/mozIMozIntlHelper.idl | 45 | ||||
-rw-r--r-- | toolkit/components/mozintl/mozIntl.jsm | 935 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/.eslintrc.js | 5 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintl.js | 169 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js | 132 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/test_mozintlhelper.js | 61 | ||||
-rw-r--r-- | toolkit/components/mozintl/test/xpcshell.ini | 6 |
12 files changed, 1606 insertions, 0 deletions
diff --git a/toolkit/components/mozintl/MozIntlHelper.cpp b/toolkit/components/mozintl/MozIntlHelper.cpp new file mode 100644 index 0000000000..f76cbe2924 --- /dev/null +++ b/toolkit/components/mozintl/MozIntlHelper.cpp @@ -0,0 +1,90 @@ +/* -*- 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 "jsapi.h" +#include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor +#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::AddGetDisplayNames(JS::Handle<JS::Value> val, JSContext* cx) { + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 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::AddGetLocaleInfo(JS::Handle<JS::Value> val, JSContext* cx) { + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getLocaleInfo", "Intl_getLocaleInfo", 1, 0), + JS_FS_END}; + + return AddFunctions(cx, val, funcs); +} 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..ca0f42de3a --- /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'], + 'jsm': 'resource://gre/modules/mozIntl.jsm', + 'constructor': 'MozIntl', + }, +] diff --git a/toolkit/components/mozintl/moz.build b/toolkit/components/mozintl/moz.build new file mode 100644 index 0000000000..de8ed47978 --- /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.ini"] + +XPIDL_SOURCES += [ + "mozIMozIntl.idl", + "mozIMozIntlHelper.idl", +] + +XPIDL_MODULE = "mozintl" + +SOURCES += [ + "MozIntlHelper.cpp", +] + +EXTRA_JS_MODULES += [ + "mozIntl.jsm", +] + +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..3192d246cb --- /dev/null +++ b/toolkit/components/mozintl/mozIMozIntl.idl @@ -0,0 +1,88 @@ +/* -*- 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); + jsval getDisplayNames([optional] in jsval locales, [optional] in jsval options); + jsval getLocaleInfo([optional] in jsval locales); + + /** + * 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); + + readonly attribute jsval Collator; + readonly attribute jsval DateTimeFormat; + 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..78546c8f8b --- /dev/null +++ b/toolkit/components/mozintl/mozIMozIntlHelper.idl @@ -0,0 +1,45 @@ +/* -*- 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); + [implicit_jscontext] void addGetDisplayNames(in jsval intlObject); + [implicit_jscontext] void addGetLocaleInfo(in jsval intlObject); + + /** + * Adds a MozDateTimeFormat contructor to the given object. This function may only + * be called once within a realm/global object: calling it multiple times will + * throw. + * + * 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); +}; diff --git a/toolkit/components/mozintl/mozIntl.jsm b/toolkit/components/mozintl/mozIntl.jsm new file mode 100644 index 0000000000..8775d9bafe --- /dev/null +++ b/toolkit/components/mozintl/mozIntl.jsm @@ -0,0 +1,935 @@ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 }); +} + +/** + * 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.days > threshold.day: + return "month"; + // 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: 4, // at least 4 weeks before using month. + day: 6, // at least 6 days before using month. + 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", + "sc", + "sd", + "se", + "sg", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "son", + "sq", + "sr", + "ss", + "st", + "su", + "sv", + "sw", + "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", + ]), +}; + +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, "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 = { + _: {}, + }; + + defineGetter(absDiff, "years", function() { + return Math.abs(diff.years); + }); + defineGetter(absDiff, "months", function() { + return Math.abs(diff.months); + }); + defineGetter(absDiff, "days", function() { + return Math.abs(diff.days); + }); + defineGetter(absDiff, "hours", function() { + return Math.abs(diff.hours); + }); + defineGetter(absDiff, "minutes", function() { + return Math.abs(diff.minutes); + }); + defineGetter(absDiff, "seconds", function() { + return Math.abs(diff.seconds); + }); + + const unit = bestFit(absDiff); + + switch (unit) { + case "year": + return this.format(diff.years, unit); + case "month": + return this.format(diff.months, 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); + } + } +} + +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); + } + + getDisplayNames(locales, ...args) { + if (!this._cache.hasOwnProperty("getDisplayNames")) { + mozIntlHelper.addGetDisplayNames(this._cache); + } + + return this._cache.getDisplayNames(locales, ...args); + } + + getLocaleInfo(locales, ...args) { + if (!this._cache.hasOwnProperty("getLocaleInfo")) { + mozIntlHelper.addGetLocaleInfo(this._cache); + } + + return this._cache.getLocaleInfo(locales, ...args); + } + + 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)) { + const value = loc.formatValueSync(`region-name-${lcRegionCode}`); + if (value !== null) { + return value; + } + } + return regionCode.toUpperCase(); + }); + } + + getLocaleDisplayNames(locales, localeCodes) { + 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."); + } + + 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); + }); + } + + 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; + } +} + +MozIntl.prototype.classID = Components.ID( + "{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}" +); +MozIntl.prototype.QueryInterface = ChromeUtils.generateQI([ + "mozIMozIntl", + "nsIObserver", + "nsISupportsWeakReference", +]); + +var EXPORTED_SYMBOLS = ["MozIntl"]; diff --git a/toolkit/components/mozintl/test/.eslintrc.js b/toolkit/components/mozintl/test/.eslintrc.js new file mode 100644 index 0000000000..69e89d0054 --- /dev/null +++ b/toolkit/components/mozintl/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/xpcshell-test"], +}; diff --git a/toolkit/components/mozintl/test/test_mozintl.js b/toolkit/components/mozintl/test/test_mozintl.js new file mode 100644 index 0000000000..9a118ff4b6 --- /dev/null +++ b/toolkit/components/mozintl/test/test_mozintl.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function run_test() { + test_methods_presence(); + test_methods_calling(); + test_constructors(); + test_rtf_formatBestUnit(); + test_datetimeformat(); + + ok(true); +} + +function test_methods_presence() { + equal(Services.intl.getCalendarInfo instanceof Function, true); + equal(Services.intl.getDisplayNames instanceof Function, true); + equal(Services.intl.getLocaleInfo instanceof Function, true); + equal(Services.intl.getLocaleDisplayNames instanceof Function, true); +} + +function test_methods_calling() { + Services.intl.getCalendarInfo("pl"); + Services.intl.getDisplayNames("ar"); + Services.intl.getLocaleInfo("de"); + new Services.intl.DateTimeFormat("fr"); + 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", "9 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 20 days"); + testRTFBestUnit(anchor, "2016-03-31 23:59", "last month"); + testRTFBestUnit(anchor, "2016-05-01 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-01 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-29 18:30", "last month"); + + anchor = new Date("2016-12-29 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"); +} 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..4b6e4e76e0 --- /dev/null +++ b/toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { L10nRegistry, FileSource } = ChromeUtils.import( + "resource://gre/modules/L10nRegistry.jsm" +); + +const fs = { + "toolkit/intl/languageNames.ftl": ` +language-name-en = English + `, + "toolkit/intl/regionNames.ftl": ` +region-name-us = United States +region-name-ru = Russia + `, +}; + +L10nRegistry.loadSync = function(url) { + if (!fs.hasOwnProperty(url)) { + return false; + } + return fs[url]; +}; + +let locales = Services.locale.packagedLocales; +const mockSource = new FileSource("mock", locales, ""); +L10nRegistry.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_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..3023a15cee --- /dev/null +++ b/toolkit/components/mozintl/test/test_mozintlhelper.js @@ -0,0 +1,61 @@ +/* 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.addGetDisplayNames instanceof Function, true); + equal(miHelper.addGetLocaleInfo instanceof Function, true); + equal(miHelper.addDateTimeFormatConstructor instanceof Function, true); + + let x = {}; + + miHelper.addGetCalendarInfo(x); + equal(x.getCalendarInfo instanceof Function, true); + + miHelper.addGetDisplayNames(x); + equal(x.getDisplayNames instanceof Function, true); + + miHelper.addGetLocaleInfo(x); + equal(x.getLocaleInfo instanceof Function, true); + + miHelper.addDateTimeFormatConstructor(x); + equal(x.DateTimeFormat instanceof Function, true); +} diff --git a/toolkit/components/mozintl/test/xpcshell.ini b/toolkit/components/mozintl/test/xpcshell.ini new file mode 100644 index 0000000000..b524252bba --- /dev/null +++ b/toolkit/components/mozintl/test/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +head = + +[test_mozintl.js] +[test_mozintl_getLocaleDisplayNames.js] +[test_mozintlhelper.js] |