Bug 1449505 - Add mozIntl.getLocaleDisplayNames. r?jfkthame
MozReview-Commit-ID: GJroYvSODgh
--- a/toolkit/components/mozintl/mozIMozIntl.idl
+++ b/toolkit/components/mozintl/mozIMozIntl.idl
@@ -36,14 +36,41 @@
*/
[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 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 = getLanguageDisplayNames(["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 = getLanguageDisplayNames(["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 DateTimeFormat;
readonly attribute jsval NumberFormat;
readonly attribute jsval Collator;
readonly attribute jsval PluralRules;
readonly attribute jsval RelativeTimeFormat;
};
--- a/toolkit/components/mozintl/mozIntl.js
+++ b/toolkit/components/mozintl/mozIntl.js
@@ -6,16 +6,21 @@ ChromeUtils.import("resource://gre/modul
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 a BCP47 language tag (ex: en-US, sr-Cyrl-RU etc.)
+ */
+const languageTagMatch = /^([a-z]{2,3}|[a-z]{4}|[a-z]{5,8})(?:[-_]([a-z]{4}))?(?:[-_]([A-Z]{2}|[0-9]{3}))?((?:[-_](?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)(?:[-_][a-wy-z0-9](?:[-_][a-z0-9]{2,8})+)*(?:[-_]x(?:[-_][a-z0-9]{1,8})+)?$/i;
+
+/**
* This helper function retrives currently used app locales, allowing
* all mozIntl APIs to use the current regional prefs locales unless
* called with explicitly listed locales.
*/
function getLocales(locales) {
if (!locales) {
return Services.locale.getRegionalPrefsLocales();
}
@@ -170,16 +175,111 @@ class MozIntl {
getLocaleInfo(locales, ...args) {
if (!this._cache.hasOwnProperty("getLocaleInfo")) {
mozIntlHelper.addGetLocaleInfo(this._cache);
}
return this._cache.getLocaleInfo(getLocales(locales), ...args);
}
+ getLanguageDisplayNames(locales, langCodes) {
+ if (locales !== undefined) {
+ throw new Error("First argument support not implemented yet");
+ }
+ const languageBundle = Services.strings.createBundle(
+ "chrome://global/locale/languageNames.properties");
+
+ return langCodes.map(langCode => {
+ if (typeof langCode !== "string") {
+ throw new TypeError("All language codes must be strings.");
+ }
+ try {
+ return languageBundle.GetStringFromName(langCode.toLowerCase());
+ } catch (e) {
+ return langCode.toLowerCase(); // Fall back to raw language subtag.
+ }
+ });
+ }
+
+ getRegionDisplayNames(locales, regionCodes) {
+ if (locales !== undefined) {
+ throw new Error("First argument support not implemented yet");
+ }
+ const regionBundle = Services.strings.createBundle(
+ "chrome://global/locale/regionNames.properties");
+
+ return regionCodes.map(regionCode => {
+ if (typeof regionCode !== "string") {
+ throw new TypeError("All region codes must be strings.");
+ }
+ try {
+ return regionBundle.GetStringFromName(regionCode.toLowerCase());
+ } catch (e) {
+ return regionCode.toUpperCase(); // Fall back to raw region subtag.
+ }
+ });
+ }
+
+ 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.");
+ }
+ // Get the display name for this dictionary.
+ // XXX: To be replaced with Intl.Locale once it lands - bug 1433303.
+ const match = localeCode.match(languageTagMatch);
+
+ if (match === null) {
+ return localeCode;
+ }
+
+ const [
+ /* languageTag */,
+ languageSubtag,
+ scriptSubtag,
+ regionSubtag,
+ variantSubtags
+ ] = match;
+
+ 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.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);
}
let DateTimeFormat = this._cache.DateTimeFormat;
class MozDateTimeFormat extends DateTimeFormat {
--- a/toolkit/components/mozintl/test/test_mozintl.js
+++ b/toolkit/components/mozintl/test/test_mozintl.js
@@ -11,22 +11,24 @@ function run_test() {
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");
+ Services.intl.getLocaleDisplayNames(undefined, ["en-US", "sr-Cyrl-RU"]);
new Services.intl.DateTimeFormat("fr");
new Services.intl.RelativeTimeFormat("fr");
ok(true);
}
function test_constructors() {
let constructors = [
"DateTimeFormat", "NumberFormat", "PluralRules", "Collator"];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+
+const gLangDN = Services.intl.getLanguageDisplayNames.bind(Services.intl, undefined);
+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]));
+ Assert.throws(() => gLocDN([{}]));
+ Assert.throws(() => gLocDN([true]));
+ 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]));
+ Assert.throws(() => gLangDN([{}]));
+ Assert.throws(() => gLangDN([true]));
+ 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]));
+ Assert.throws(() => gRegDN([{}]));
+ Assert.throws(() => gRegDN([true]));
+ run_next_test();
+});
--- a/toolkit/components/mozintl/test/xpcshell.ini
+++ b/toolkit/components/mozintl/test/xpcshell.ini
@@ -1,5 +1,6 @@
[DEFAULT]
head =
[test_mozintl.js]
+[test_mozintl_getLocaleDisplayNames.js]
[test_mozintlhelper.js]