Bug 730209 - Parse spellchecker dictionary names as BCP 47 language tags. r=gavin
--- 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)'");
+ },
+};