Bug 730209 - Parse spellchecker dictionary names as BCP 47 language tags. r=gavin
authorGordon P. Hemsley <gphemsley@gmail.com>
Wed, 28 Mar 2012 18:56:02 -0400
changeset 93895 9949bb0923030bdaccc2300b71641368e232633c
parent 93894 095fd525afa782e7f4c23a7056b1810f512284ee
child 93896 0cb85ff0f764433d05954772f49f12be2c27f330
push idunknown
push userunknown
push dateunknown
reviewersgavin
bugs730209
milestone14.0a1
Bug 730209 - Parse spellchecker dictionary names as BCP 47 language tags. r=gavin
toolkit/content/InlineSpellChecker.jsm
toolkit/content/tests/browser/Makefile.in
toolkit/content/tests/browser/browser_InlineSpellChecker.js
--- a/toolkit/content/InlineSpellChecker.jsm
+++ b/toolkit/content/InlineSpellChecker.jsm
@@ -178,80 +178,108 @@ InlineSpellChecker.prototype = {
   // returns the number of dictionary languages. If insertBefore is NULL, this
   // does an append to the given menu
   addDictionaryListToMenu: function(menu, insertBefore)
   {
     this.mDictionaryMenu = menu;
     this.mDictionaryNames = [];
     this.mDictionaryItems = [];
 
-    if (! gLanguageBundle) {
-      // create the bundles for language and region
-      var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
-                                    .getService(Components.interfaces.nsIStringBundleService);
-      gLanguageBundle = bundleService.createBundle(
-          "chrome://global/locale/languageNames.properties");
-      gRegionBundle = bundleService.createBundle(
-          "chrome://global/locale/regionNames.properties");
-    }
-
     if (! this.mInlineSpellChecker || ! this.enabled)
       return 0;
     var spellchecker = this.mInlineSpellChecker.spellChecker;
     var o1 = {}, o2 = {};
     spellchecker.GetDictionaryList(o1, o2);
     var list = o1.value;
     var listcount = o2.value;
     var curlang = "";
     try {
         curlang = spellchecker.GetCurrentDictionary();
     } catch(e) {}
-    var isoStrArray;
 
     for (var i = 0; i < list.length; i ++) {
-      // get the display name for this dictionary
-      isoStrArray = list[i].split(/[-_]/);
-      var displayName = "";
-      if (gLanguageBundle && isoStrArray[0]) {
-        try {
-          displayName = gLanguageBundle.GetStringFromName(isoStrArray[0].toLowerCase());
-        } catch(e) {} // ignore language bundle errors
-        if (gRegionBundle && isoStrArray[1]) {
-          try {
-            displayName += " / " + gRegionBundle.GetStringFromName(isoStrArray[1].toLowerCase());
-          } catch(e) {} // ignore region bundle errors
-          if (isoStrArray[2])
-            displayName += " (" + isoStrArray[2] + ")";
-        }
-      }
-
-      // if we didn't get a name, just use the raw dictionary name
-      if (displayName.length == 0)
-        displayName = list[i];
-
       this.mDictionaryNames.push(list[i]);
       var item = menu.ownerDocument.createElement("menuitem");
       item.setAttribute("id", "spell-check-dictionary-" + list[i]);
-      item.setAttribute("label", displayName);
+      item.setAttribute("label", this.getDictionaryDisplayName(list[i]));
       item.setAttribute("type", "radio");
       this.mDictionaryItems.push(item);
       if (curlang == list[i]) {
         item.setAttribute("checked", "true");
       } else {
         var callback = function(me, val) { return function(evt) { me.selectDictionary(val); } };
         item.addEventListener("command", callback(this, i), true);
       }
       if (insertBefore)
         menu.insertBefore(item, insertBefore);
       else
         menu.appendChild(item);
     }
     return list.length;
   },
 
+  // Formats a valid BCP 47 language tag based on available localized names.
+  getDictionaryDisplayName: function(dictionaryName) {
+    try {
+      // Get the display name for this dictionary.
+      let 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}))*)$/i;
+      var [languageTag, languageSubtag, scriptSubtag, regionSubtag, variantSubtags] = dictionaryName.match(languageTagMatch);
+    } catch(e) {
+      // If we weren't given a valid language tag, just use the raw dictionary name.
+      return dictionaryName;
+    }
+
+    if (!gLanguageBundle) {
+      // Create the bundles for language and region names.
+      var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
+                                    .getService(Components.interfaces.nsIStringBundleService);
+      gLanguageBundle = bundleService.createBundle(
+          "chrome://global/locale/languageNames.properties");
+      gRegionBundle = bundleService.createBundle(
+          "chrome://global/locale/regionNames.properties");
+    }
+
+    var displayName = "";
+
+    // Language subtag will normally be 2 or 3 letters, but could be up to 8.
+    try {
+      displayName += gLanguageBundle.GetStringFromName(languageSubtag.toLowerCase());
+    } catch(e) {
+      displayName += languageSubtag.toLowerCase(); // Fall back to raw language subtag.
+    }
+
+    // Region subtag will be 2 letters or 3 digits.
+    if (regionSubtag) {
+      displayName += " (";
+
+      try {
+        displayName += gRegionBundle.GetStringFromName(regionSubtag.toLowerCase());
+      } catch(e) {
+        displayName += regionSubtag.toUpperCase(); // Fall back to raw region subtag.
+      }
+
+      displayName += ")";
+    }
+
+    // Script subtag will be 4 letters.
+    if (scriptSubtag) {
+      displayName += " / ";
+
+      // XXX: See bug 666662 and bug 666731 for full implementation.
+      displayName += scriptSubtag; // Fall back to raw script subtag.
+    }
+
+    // Each variant subtag will be 4 to 8 chars.
+    if (variantSubtags)
+      // XXX: See bug 666662 and bug 666731 for full implementation.
+      displayName += " (" + variantSubtags.substr(1).split(/[-_]/).join(" / ") + ")"; // Collapse multiple variants.
+
+    return displayName;
+  },
+
   // undoes the work of addDictionaryListToMenu for the menu
   // (call on popup hiding)
   clearDictionaryListFromMenu: function()
   {
     for (var i = 0; i < this.mDictionaryItems.length; i ++) {
       this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]);
     }
     this.mDictionaryItems = [];
@@ -285,17 +313,17 @@ InlineSpellChecker.prototype = {
   },
 
   // callback for adding the current misspelling to the user-defined dictionary
   addToDictionary: function()
   {
     // Prevent the undo stack from growing over the max depth
     if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH)
       this.mAddedWordStack.shift();
-      
+
     this.mAddedWordStack.push(this.mMisspelling);
     this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
   },
   // callback for removing the last added word to the dictionary LIFO fashion
   undoAddToDictionary: function()
   {
     if (this.mAddedWordStack.length > 0)
     {
--- a/toolkit/content/tests/browser/Makefile.in
+++ b/toolkit/content/tests/browser/Makefile.in
@@ -50,15 +50,16 @@ DIRS = \
 
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   browser_keyevents_during_autoscrolling.js \
   browser_bug295977_autoscroll_overflow.js \
   browser_bug594509.js \
   browser_Geometry.js \
+  browser_InlineSpellChecker.js \
   browser_save_resend_postdata.js \
   browser_browserDrop.js \
   browser_Services.js \
   $(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_InlineSpellChecker.js
@@ -0,0 +1,59 @@
+function test() {
+  let tempScope = {};
+  Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm", tempScope);
+  let InlineSpellChecker = tempScope.InlineSpellChecker;
+
+  ok(InlineSpellChecker, "InlineSpellChecker class exists");
+  for (var fname in tests) {
+    tests[fname]();
+  }
+}
+
+let tests = {
+  // Test various possible dictionary name to ensure they display as expected.
+  // XXX: This only works for the 'en-US' locale, as the testing involves localized output.
+  testDictionaryDisplayNames: function() {
+    let isc = new InlineSpellChecker();
+
+    // Check for valid language tag.
+    is(isc.getDictionaryDisplayName("-invalid-"), "-invalid-", "'-invalid-' should display as '-invalid-'");
+
+    // Check if display name is available for language subtag.
+    is(isc.getDictionaryDisplayName("en"), "English", "'en' should display as 'English'");
+    is(isc.getDictionaryDisplayName("qaz"), "qaz", "'qaz' should display as 'qaz'"); // Private use subtag
+
+    // Check if display name is available for region subtag.
+    is(isc.getDictionaryDisplayName("en-US"), "English (United States)", "'en-US' should display as 'English (United States)'");
+    is(isc.getDictionaryDisplayName("en-QZ"), "English (QZ)", "'en-QZ' should display as 'English (QZ)'"); // Private use subtag
+    todo_is(isc.getDictionaryDisplayName("es-419"), "Spanish (Latin America and the Caribbean)", "'es-419' should display as 'Spanish (Latin America and the Caribbean)'");
+
+    // Check if display name is available for script subtag.
+    todo_is(isc.getDictionaryDisplayName("en-Cyrl"), "English / Cyrillic", "'en-Cyrl' should display as 'English / Cyrillic'");
+    todo_is(isc.getDictionaryDisplayName("en-Cyrl-US"), "English (United States) / Cyrillic", "'en-Cyrl-US' should display as 'English (United States) / Cyrillic'");
+    todo_is(isc.getDictionaryDisplayName("en-Cyrl-QZ"), "English (QZ) / Cyrillic", "'en-Cyrl-QZ' should display as 'English (QZ) / Cyrillic'"); // Private use subtag
+    todo_is(isc.getDictionaryDisplayName("qaz-Cyrl"), "qaz / Cyrillic", "'qaz-Cyrl' should display as 'qaz / Cyrillic'"); // Private use subtag
+    todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US"), "qaz (United States) / Cyrillic", "'qaz-Cyrl-US' should display as 'qaz (United States) / Cyrillic'"); // Private use subtag
+    todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-QZ"), "qaz (QZ) / Cyrillic", "'qaz-Cyrl-QZ' should display as 'qaz (QZ) / Cyrillic'"); // Private use subtags
+    is(isc.getDictionaryDisplayName("en-Qaaz"), "English / Qaaz", "'en-Qaaz' should display as 'English / Qaaz'"); // Private use subtag
+    is(isc.getDictionaryDisplayName("en-Qaaz-US"), "English (United States) / Qaaz", "'en-Qaaz-US' should display as 'English (United States) / Qaaz'"); // Private use subtag
+    is(isc.getDictionaryDisplayName("en-Qaaz-QZ"), "English (QZ) / Qaaz", "'en-Qaaz-QZ' should display as 'English (QZ) / Qaaz'"); // Private use subtags
+    is(isc.getDictionaryDisplayName("qaz-Qaaz"), "qaz / Qaaz", "'qaz-Qaaz' should display as 'qaz / Qaaz'"); // Private use subtags
+    is(isc.getDictionaryDisplayName("qaz-Qaaz-US"), "qaz (United States) / Qaaz", "'qaz-Qaaz-US' should display as 'qaz (United States) / Qaaz'"); // Private use subtags
+    is(isc.getDictionaryDisplayName("qaz-Qaaz-QZ"), "qaz (QZ) / Qaaz", "'qaz-Qaaz-QZ' should display as 'qaz (QZ) / Qaaz'"); // Private use subtags
+
+    // Check if display name is available for variant subtag.
+    // XXX: It isn't clear how we'd ideally want to display variant subtags.
+    is(isc.getDictionaryDisplayName("de-1996"), "German (1996)", "'de-1996' should display as 'German (1996)'");
+    is(isc.getDictionaryDisplayName("de-CH-1996"), "German (Switzerland) (1996)", "'de-CH-1996' should display as 'German (Switzerland) (1996)'");
+
+    // Complex cases.
+    // XXX: It isn't clear how we'd ideally want to display variant subtags.
+    todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa"), "English (United States) / Cyrillic (fonipa)", "'en-Cyrl-US-fonipa' should display as 'English (United States) / Cyrillic (fonipa)'");
+    todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-fonxsamp"), "English (United States) / Cyrillic (fonipa / fonxsamp)", "'en-Cyrl-US-fonipa-fonxsamp' should display as 'English (United States) / Cyrillic (fonipa / fonxsamp)'");
+    is(isc.getDictionaryDisplayName("qaz-Qaaz-QZ-fonipa"), "qaz (QZ) / Qaaz (fonipa)", "'qaz-Qaaz-QZ-fonipa' should display as 'qaz (QZ) / Qaaz (fonipa)'"); // Private use subtags
+    is(isc.getDictionaryDisplayName("qaz-Qaaz-QZ-fonipa-fonxsamp"), "qaz (QZ) / Qaaz (fonipa / fonxsamp)", "'qaz-Qaaz-QZ-fonipa-fonxsamp' should display as 'qaz (QZ) / Qaaz (fonipa / fonxsamp)'"); // Private use subtags
+
+    // Check if display name is available for grandfathered tags.
+    todo_is(isc.getDictionaryDisplayName("en-GB-oed"), "English (United Kingdom) (OED)", "'en-GB-oed' should display as 'English (United Kingdom) (OED)'");
+  },
+};