Bug 1449505 - Add mozIntl.getLocaleDisplayNames. r?jfkthame draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Fri, 30 Mar 2018 20:50:58 +0200
changeset 780080 f3f9c9c46e2ca5749f575533907a21c5691763b2
parent 779350 15678b283f0f62b7a27afba6e572ef8961d46c69
child 780081 a6997da1d6c4cb3bb899ac5086761578fddafb35
push id105961
push userbmo:gandalf@aviary.pl
push dateWed, 11 Apr 2018 07:08:45 +0000
reviewersjfkthame
bugs1449505
milestone61.0a1
Bug 1449505 - Add mozIntl.getLocaleDisplayNames. r?jfkthame MozReview-Commit-ID: GJroYvSODgh
toolkit/components/mozintl/mozIMozIntl.idl
toolkit/components/mozintl/mozIntl.js
toolkit/components/mozintl/test/test_mozintl.js
toolkit/components/mozintl/test/test_mozintl_getLocaleDisplayNames.js
toolkit/components/mozintl/test/xpcshell.ini
--- 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]