Bug 338427 - Spellchecker should respect the langi attribute; r=ehsan
authorarno renevier <arno@renevier.net>
Fri, 12 Aug 2011 15:12:45 -0400
changeset 74740 f3f7872db0aef93fcd565ee96a25fb8c8f02179f
parent 74739 5ca0d6677b2c94da15a7e65d8824f586898d485b
child 74741 37605826f6fb7b21edbf8665c799de8521029035
push id313
push usereakhgari@mozilla.com
push dateTue, 16 Aug 2011 19:58:41 +0000
treeherdermozilla-aurora@ef9d1c90dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs338427
milestone8.0a1
Bug 338427 - Spellchecker should respect the langi attribute; r=ehsan
browser/base/content/test/test_contextmenu.html
content/base/public/nsIContent.h
editor/composer/src/nsEditorSpellCheck.cpp
editor/idl/nsIEditorSpellCheck.idl
editor/idl/nsIHTMLEditor.idl
editor/libeditor/base/nsEditor.cpp
editor/libeditor/base/nsEditor.h
editor/libeditor/base/nsEditorEventListener.cpp
editor/libeditor/html/nsHTMLEditor.h
editor/libeditor/html/tests/test_bug484181.html
editor/txtsvc/public/nsIInlineSpellChecker.idl
editor/txtsvc/public/nsISpellChecker.h
extensions/spellcheck/src/mozInlineSpellChecker.cpp
extensions/spellcheck/src/mozSpellChecker.cpp
layout/reftests/editor/338427-1-ref.html
layout/reftests/editor/338427-1.html
layout/reftests/editor/338427-2-ref.html
layout/reftests/editor/338427-2.html
layout/reftests/editor/338427-3-ref.html
layout/reftests/editor/338427-3.html
layout/reftests/editor/reftest.list
layout/reftests/unicode/langattribute-ref.html
layout/reftests/unicode/langattribute.html
layout/reftests/unicode/reftest.list
layout/style/nsCSSRuleProcessor.cpp
toolkit/content/InlineSpellChecker.jsm
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -584,16 +584,17 @@ function startTest() {
     img    = subwindow.document.getElementById("test-image");
     canvas = subwindow.document.getElementById("test-canvas");
     video_ok   = subwindow.document.getElementById("test-video-ok");
     video_bad  = subwindow.document.getElementById("test-video-bad");
     video_bad2 = subwindow.document.getElementById("test-video-bad2");
     iframe = subwindow.document.getElementById("test-iframe");
     textarea = subwindow.document.getElementById("test-textarea");
     contenteditable = subwindow.document.getElementById("test-contenteditable");
+    contenteditable.focus(); // content editable needs to be focused to enable spellcheck
     inputspell = subwindow.document.getElementById("test-input-spellcheck");
     pagemenu = subwindow.document.getElementById("test-pagemenu");
 
     contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false);
     runTest(1);
 }
 
 // We open this in a separate window, because the Mochitests run inside a frame.
--- a/content/base/public/nsIContent.h
+++ b/content/base/public/nsIContent.h
@@ -939,16 +939,41 @@ public:
 
   /**
    * If the content is a part of HTML editor, this returns editing
    * host content.  When the content is in designMode, this returns its body
    * element.  Also, when the content isn't editable, this returns null.
    */
   nsIContent* GetEditingHost();
 
+  /**
+   * Determing language. Look at the nearest ancestor element that has a lang
+   * attribute in the XML namespace or is an HTML element and has a lang in
+   * no namespace attribute.
+   */
+  void GetLang(nsAString& aResult) const {
+    for (const nsIContent* content = this; content; content = content->GetParent()) {
+      if (content->GetAttrCount() > 0) {
+        // xml:lang has precedence over lang on HTML elements (see
+        // XHTML1 section C.7).
+        PRBool hasAttr = content->GetAttr(kNameSpaceID_XML, nsGkAtoms::lang,
+                                          aResult);
+        if (!hasAttr && content->IsHTML()) {
+          hasAttr = content->GetAttr(kNameSpaceID_None, nsGkAtoms::lang,
+                                     aResult);
+        }
+        NS_ASSERTION(hasAttr || aResult.IsEmpty(),
+                     "GetAttr that returns false should not make string non-empty");
+        if (hasAttr) {
+          return;
+        }
+      }
+    }
+  }
+
   // Overloaded from nsINode
   virtual already_AddRefed<nsIURI> GetBaseURI() const;
 
   virtual nsresult PreHandleEvent(nsEventChainPreVisitor& aVisitor);
 
 protected:
   /**
    * Hook for implementing GetID.  This is guaranteed to only be
--- a/editor/composer/src/nsEditorSpellCheck.cpp
+++ b/editor/composer/src/nsEditorSpellCheck.cpp
@@ -37,28 +37,33 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsEditorSpellCheck.h"
 
+#include "nsStyleUtil.h"
+#include "nsIContent.h"
+#include "nsIDOMElement.h"
 #include "nsITextServicesDocument.h"
 #include "nsISpellChecker.h"
 #include "nsISelection.h"
 #include "nsIDOMRange.h"
 #include "nsIEditor.h"
+#include "nsIHTMLEditor.h"
 
 #include "nsIComponentManager.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIChromeRegistry.h"
 #include "nsString.h"
 #include "nsReadableUtils.h"
 #include "nsITextServicesFilter.h"
+#include "nsUnicharUtils.h"
 #include "mozilla/Services.h"
 #include "mozilla/Preferences.h"
 
 using namespace mozilla;
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsEditorSpellCheck)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsEditorSpellCheck)
 
@@ -178,71 +183,19 @@ nsEditorSpellCheck::InitSpellChecker(nsI
   mSpellChecker = do_CreateInstance(NS_SPELLCHECKER_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NULL_POINTER);
 
   rv = mSpellChecker->SetDocument(tsDoc, PR_TRUE);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Tell the spellchecker what dictionary to use:
-
-  nsAdoptingString dictName =
-    Preferences::GetLocalizedString("spellchecker.dictionary");
-
-  if (dictName.IsEmpty())
-  {
-    // Prefs didn't give us a dictionary name, so just get the current
-    // locale and use that as the default dictionary name!
-
-    nsCOMPtr<nsIXULChromeRegistry> packageRegistry =
-      mozilla::services::GetXULChromeRegistryService();
-
-    if (packageRegistry) {
-      nsCAutoString utf8DictName;
-      rv = packageRegistry->GetSelectedLocale(NS_LITERAL_CSTRING("global"),
-                                              utf8DictName);
-      AppendUTF8toUTF16(utf8DictName, dictName);
-    }
-  }
-
-  PRBool setDictionary = PR_FALSE;
-  if (NS_SUCCEEDED(rv) && !dictName.IsEmpty()) {
-    rv = SetCurrentDictionary(dictName.get());
-
-    // fall back to "en-US" if the current locale doesn't have a dictionary.
-    if (NS_FAILED(rv)) {
-      rv = SetCurrentDictionary(NS_LITERAL_STRING("en-US").get());
-    }
-
-    if (NS_SUCCEEDED(rv))
-      setDictionary = PR_TRUE;
-  }
-
-  // If there was no dictionary specified by spellchecker.dictionary and setting it to the 
-  // locale dictionary didn't work, try to use the first dictionary we find. This helps when 
-  // the first dictionary is installed
-  if (! setDictionary) {
-    nsTArray<nsString> dictList;
-    rv = mSpellChecker->GetDictionaryList(&dictList);
-    NS_ENSURE_SUCCESS(rv, rv);
-    if (dictList.Length() > 0) {
-      rv = SetCurrentDictionary(dictList[0].get());
-      if (NS_SUCCEEDED(rv))
-        SaveDefaultDictionary();
-    }
-  }
-
-  // If an error was thrown while checking the dictionary pref, just
-  // fail silently so that the spellchecker dialog is allowed to come
-  // up. The user can manually reset the language to their choice on
-  // the dialog if it is wrong.
-
-  DeleteSuggestedWordList();
-
+  // do not fail if UpdateCurrentDictionary fails because this method may
+  // succeed later.
+  UpdateCurrentDictionary(aEditor);
   return NS_OK;
 }
 
 NS_IMETHODIMP    
 nsEditorSpellCheck::GetNextMisspelledWord(PRUnichar **aNextMisspelledWord)
 {
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
 
@@ -434,24 +387,16 @@ nsEditorSpellCheck::SetCurrentDictionary
   return mSpellChecker->SetCurrentDictionary(nsDependentString(aDictionary));
 }
 
 NS_IMETHODIMP    
 nsEditorSpellCheck::UninitSpellChecker()
 {
   NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
 
-  // we preserve the last selected language, but ignore errors so we continue
-  // to uninitialize
-#ifdef DEBUG
-  nsresult rv =
-#endif
-  SaveDefaultDictionary();
-  NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "failed to set default dictionary");
-
   // Cleanup - kill the spell checker
   DeleteSuggestedWordList();
   mDictionaryList.Clear();
   mDictionaryIndex = 0;
   mSpellChecker = 0;
   return NS_OK;
 }
 
@@ -484,8 +429,129 @@ nsEditorSpellCheck::SetFilter(nsITextSer
 
 nsresult    
 nsEditorSpellCheck::DeleteSuggestedWordList()
 {
   mSuggestedWordList.Clear();
   mSuggestedWordIndex = 0;
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsEditorSpellCheck::UpdateCurrentDictionary(nsIEditor* aEditor)
+{
+  nsresult rv;
+
+  // Tell the spellchecker what dictionary to use:
+  nsAutoString dictName;
+
+  // First, try to get language with html5 algorithm
+  nsAutoString editorLang;
+
+  nsCOMPtr<nsIContent> rootContent;
+
+  nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(aEditor);
+  if (htmlEditor) {
+    rootContent = htmlEditor->GetActiveEditingHost();
+  } else {
+    nsCOMPtr<nsIDOMElement> rootElement;
+    rv = aEditor->GetRootElement(getter_AddRefs(rootElement));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rootContent = do_QueryInterface(rootElement);
+  }
+  NS_ENSURE_TRUE(rootContent, NS_ERROR_FAILURE);
+
+  rootContent->GetLang(editorLang);
+
+  if (editorLang.IsEmpty()) {
+    nsCOMPtr<nsIDocument> doc = rootContent->GetCurrentDoc();
+    NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+    doc->GetContentLanguage(editorLang);
+  }
+
+  if (!editorLang.IsEmpty()) {
+    dictName.Assign(editorLang);
+  }
+
+  // otherwise, get language from preferences
+  if (dictName.IsEmpty()) {
+    dictName.Assign(Preferences::GetLocalizedString("spellchecker.dictionary"));
+  }
+
+  if (dictName.IsEmpty())
+  {
+    // Prefs didn't give us a dictionary name, so just get the current
+    // locale and use that as the default dictionary name!
+
+    nsCOMPtr<nsIXULChromeRegistry> packageRegistry =
+      mozilla::services::GetXULChromeRegistryService();
+
+    if (packageRegistry) {
+      nsCAutoString utf8DictName;
+      rv = packageRegistry->GetSelectedLocale(NS_LITERAL_CSTRING("global"),
+                                              utf8DictName);
+      AppendUTF8toUTF16(utf8DictName, dictName);
+    }
+  }
+
+  SetCurrentDictionary(NS_LITERAL_STRING("").get());
+
+  if (NS_SUCCEEDED(rv) && !dictName.IsEmpty()) {
+    rv = SetCurrentDictionary(dictName.get());
+    if (NS_FAILED(rv)) {
+      // required dictionary was not available. Try to get a dictionary
+      // matching at least language part of dictName: If required dictionary is
+      // "aa-bb", we try "aa", then we try any available dictionary aa-XX
+      nsAutoString langCode;
+      PRInt32 dashIdx = dictName.FindChar('-');
+      if (dashIdx != -1) {
+        langCode.Assign(Substring(dictName, 0, dashIdx));
+        // try to use langCode
+        rv = SetCurrentDictionary(langCode.get());
+      } else {
+        langCode.Assign(dictName);
+      }
+      if (NS_FAILED(rv)) {
+        // loop over avaible dictionaries; if we find one with required
+        // language, use it
+        nsTArray<nsString> dictList;
+        rv = mSpellChecker->GetDictionaryList(&dictList);
+        NS_ENSURE_SUCCESS(rv, rv);
+        nsDefaultStringComparator comparator;
+        PRInt32 i, count = dictList.Length();
+        for (i = 0; i < count; i++) {
+          nsAutoString dictStr(dictList.ElementAt(i));
+          if (nsStyleUtil::DashMatchCompare(dictStr, langCode, comparator) &&
+              NS_SUCCEEDED(SetCurrentDictionary(dictStr.get()))) {
+              break;
+          }
+        }
+      }
+    }
+  }
+
+  // If we have not set dictionary, and the editable element doesn't have a
+  // lang attribute, we try to get a dictionary. First try, en-US. If it does
+  // not work, pick the first one.
+  if (editorLang.IsEmpty()) {
+    nsAutoString currentDictonary;
+    rv = mSpellChecker->GetCurrentDictionary(currentDictonary);
+    if (NS_FAILED(rv) || currentDictonary.IsEmpty()) {
+      rv = SetCurrentDictionary(NS_LITERAL_STRING("en-US").get());
+      if (NS_FAILED(rv)) {
+        nsTArray<nsString> dictList;
+        rv = mSpellChecker->GetDictionaryList(&dictList);
+        if (NS_SUCCEEDED(rv) && dictList.Length() > 0) {
+          SetCurrentDictionary(dictList[0].get());
+        }
+      }
+    }
+  }
+
+  // If an error was thrown while setting the dictionary, just
+  // fail silently so that the spellchecker dialog is allowed to come
+  // up. The user can manually reset the language to their choice on
+  // the dialog if it is wrong.
+
+  DeleteSuggestedWordList();
+
+  return NS_OK;
+}
--- a/editor/idl/nsIEditorSpellCheck.idl
+++ b/editor/idl/nsIEditorSpellCheck.idl
@@ -36,17 +36,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
  
 #include "nsISupports.idl"
 
 interface nsIEditor;
 interface nsITextServicesFilter;
 
-[scriptable, uuid(90c93610-c116-44ab-9793-62dccb9f43ce)]
+[scriptable, uuid(803ff0dd-07f2-4438-b3a6-ab9c2fe4e1dd)]
 interface nsIEditorSpellCheck : nsISupports
 {
 
  /**
    * Returns true if we can enable spellchecking. If there are no available
    * dictionaries, this will return false.
    */
   boolean       canSpellCheck();
@@ -183,9 +183,15 @@ interface nsIEditorSpellCheck : nsISuppo
    * compute any suggestions.
    *
    * Watch out: this does not clear any suggestions left over from previous
    * calls to CheckCurrentWord, so there may be suggestions, but they will be
    * invalid.
    */
   boolean       CheckCurrentWordNoSuggest(in wstring suggestedWord);
 
+  /**
+   * Update the dictionary in use to be sure it corresponds to what the editor
+   * needs.
+   */
+  void          UpdateCurrentDictionary(in nsIEditor editor);
+
 };
--- a/editor/idl/nsIHTMLEditor.idl
+++ b/editor/idl/nsIHTMLEditor.idl
@@ -37,26 +37,27 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 #include "domstubs.idl"
 
 interface nsIAtom;
+interface nsIContent;
 interface nsISupportsArray;
 interface nsISelection;
 interface nsIContentFilter;
 
 %{C++
 #define NS_EDITOR_ELEMENT_NOT_FOUND \
   NS_ERROR_GENERATE_SUCCESS(NS_ERROR_MODULE_EDITOR, 1)
 
 %}
-[scriptable, uuid(c964b8b0-e9e8-11df-9492-0800200c9a66)]
+[scriptable, uuid(d58f35a7-c269-4292-b9aa-a79e200a7c99)]
 
 interface nsIHTMLEditor : nsISupports
 {
 %{C++
   typedef short EAlignment;
 %}
 
   // used by GetAlignment()
@@ -608,10 +609,16 @@ interface nsIHTMLEditor : nsISupports
    * @return    true if CR in a paragraph creates a new paragraph
    */
   attribute boolean returnInParagraphCreatesNewParagraph;
 
   /**
    * Checks whether a BR node is visible to the user.
    */
   boolean breakIsVisible(in nsIDOMNode aNode);
+
+  /**
+   * Get an active editor's editing host in DOM window.  If this editor isn't
+   * active in the DOM window, this returns NULL.
+   */
+  [noscript, notxpcom] nsIContent GetActiveEditingHost();
 };
 
--- a/editor/libeditor/base/nsEditor.cpp
+++ b/editor/libeditor/base/nsEditor.cpp
@@ -5306,8 +5306,17 @@ nsEditor::BeginKeypressHandling(nsIDOMNS
   NS_ASSERTION(mLastKeypressEventWasTrusted == eTriUnset, "How come our status is not clear?");
 
   if (aEvent) {
     PRBool isTrusted = PR_FALSE;
     aEvent->GetIsTrusted(&isTrusted);
     mLastKeypressEventWasTrusted = isTrusted ? eTriTrue : eTriFalse;
   }
 }
+
+void
+nsEditor::OnFocus(nsIDOMEventTarget* aFocusEventTarget)
+{
+  InitializeSelection(aFocusEventTarget);
+  if (mInlineSpellChecker) {
+    mInlineSpellChecker->UpdateCurrentDictionary();
+  }
+}
--- a/editor/libeditor/base/nsEditor.h
+++ b/editor/libeditor/base/nsEditor.h
@@ -714,16 +714,21 @@ public:
   // element in the document has focus.
   virtual already_AddRefed<nsIContent> FindSelectionRoot(nsINode* aNode);
 
   // Initializes selection and caret for the editor.  If aEventTarget isn't
   // a host of the editor, i.e., the editor doesn't get focus, this does
   // nothing.
   nsresult InitializeSelection(nsIDOMEventTarget* aFocusEventTarget);
 
+  // This method has to be called by nsEditorEventListener::Focus.
+  // All actions that have to be done when the editor is focused needs to be
+  // added here.
+  void OnFocus(nsIDOMEventTarget* aFocusEventTarget);
+
 protected:
 
   PRUint32        mModCount;		// number of modifications (for undo/redo stack)
   PRUint32        mFlags;		// behavior flags. See nsIPlaintextEditor.idl for the flags we use.
 
   nsWeakPtr       mSelConWeak;   // weak reference to the nsISelectionController
   PRInt32         mUpdateCount;
   nsIViewManager::UpdateViewBatch mBatch;
--- a/editor/libeditor/base/nsEditorEventListener.cpp
+++ b/editor/libeditor/base/nsEditorEventListener.cpp
@@ -813,17 +813,17 @@ nsEditorEventListener::Focus(nsIDOMEvent
 
       nsCOMPtr<nsIDOMElement> element;
       fm->GetFocusedElement(getter_AddRefs(element));
       if (!SameCOMIdentity(element, target))
         return NS_OK;
     }
   }
 
-  mEditor->InitializeSelection(target);
+  mEditor->OnFocus(target);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsEditorEventListener::Blur(nsIDOMEvent* aEvent)
 {
   NS_ENSURE_TRUE(mEditor, NS_ERROR_NOT_AVAILABLE);
   NS_ENSURE_ARG(aEvent);
--- a/editor/libeditor/html/nsHTMLEditor.h
+++ b/editor/libeditor/html/nsHTMLEditor.h
@@ -453,19 +453,16 @@ protected:
 
   PRBool ShouldReplaceRootElement();
   void ResetRootElementAndEventTarget();
   nsresult GetBodyElement(nsIDOMHTMLElement** aBody);
   // Get the focused node of this editor.
   // @return    If the editor has focus, this returns the focused node.
   //            Otherwise, returns null.
   already_AddRefed<nsINode> GetFocusedNode();
-  // Get an active editor's editing host in DOM window.  If this editor isn't
-  // active in the DOM window, this returns NULL.
-  nsIContent* GetActiveEditingHost();
 
   // Return TRUE if aElement is a table-related elemet and caret was set
   PRBool SetCaretInTableCell(nsIDOMElement* aElement);
   PRBool IsNodeInActiveEditor(nsIDOMNode* aNode);
 
   // key event helpers
   NS_IMETHOD TabInTable(PRBool inIsShift, PRBool *outHandled);
   NS_IMETHOD CreateBR(nsIDOMNode *aNode, PRInt32 aOffset, 
--- a/editor/libeditor/html/tests/test_bug484181.html
+++ b/editor/libeditor/html/tests/test_bug484181.html
@@ -41,39 +41,41 @@ function getSpellCheckSelection() {
   var selcon = editor.selectionController;
   return selcon.getSelection(selcon.SELECTION_SPELLCHECK);  
 }
 
 function append(str) {
   netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
 
   var edit = document.getElementById("edit");
-  edit.focus();
   var editor = getEditor();
   var sel = editor.selection;
   sel.selectAllChildren(edit);
   sel.collapseToEnd();
 
   for (var i = 0; i < str.length; ++i) {
     synthesizeKey(str[i], {});
   }
 }
 
 function runTest() {
   gMisspeltWords = ["haz", "cheezburger"];
-  is(isSpellingCheckOk(), true, "All misspellings before editing are accounted for.");
-
   var edit = document.getElementById("edit");
-  append(" becaz I'm a lolcat!");
+  edit.focus();
   SimpleTest.executeSoon(function() {
-  gMisspeltWords.push("becaz");
-  gMisspeltWords.push("lolcat");
-  is(isSpellingCheckOk(), true, "All misspellings after typing are accounted for.");
+      is(isSpellingCheckOk(), true, "All misspellings before editing are accounted for.");
 
-  SimpleTest.finish();
+      append(" becaz I'm a lolcat!");
+      SimpleTest.executeSoon(function() {
+      gMisspeltWords.push("becaz");
+      gMisspeltWords.push("lolcat");
+      is(isSpellingCheckOk(), true, "All misspellings after typing are accounted for.");
+
+      SimpleTest.finish();
+      });
   });
 }
 
 function isSpellingCheckOk() {
   netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
 
   var sel = getSpellCheckSelection();
   var numWords = sel.rangeCount;
--- a/editor/txtsvc/public/nsIInlineSpellChecker.idl
+++ b/editor/txtsvc/public/nsIInlineSpellChecker.idl
@@ -38,17 +38,17 @@
 
 #include "nsISupports.idl"
 #include "domstubs.idl"
 
 interface nsISelection;
 interface nsIEditor;
 interface nsIEditorSpellCheck;
 
-[scriptable, uuid(07be036a-2355-4a92-b150-5c9b7e9fdf2f)]
+[scriptable, uuid(f456dda1-965d-470c-8c55-e51b38e45212)]
 
 interface nsIInlineSpellChecker : nsISupports
 {
   readonly attribute nsIEditorSpellCheck spellChecker;
 
   [noscript] void init(in nsIEditor aEditor);
   [noscript] void cleanup(in boolean aDestroyingFrames);
 
@@ -66,15 +66,16 @@ interface nsIInlineSpellChecker : nsISup
   void spellCheckRange(in nsIDOMRange aSelection);
 
   nsIDOMRange getMisspelledWord(in nsIDOMNode aNode, in long aOffset);
   void replaceWord(in nsIDOMNode aNode, in long aOffset, in AString aNewword);
   void addWordToDictionary(in AString aWord);
   
   void ignoreWord(in AString aWord);
   void ignoreWords([array, size_is(aCount)] in wstring aWordsToIgnore, in unsigned long aCount);
+  void updateCurrentDictionary();
 };
 
 %{C++
 
 #define MOZ_INLINESPELLCHECKER_CONTRACTID "@mozilla.org/spellchecker-inline;1"
 
 %}
--- a/editor/txtsvc/public/nsISpellChecker.h
+++ b/editor/txtsvc/public/nsISpellChecker.h
@@ -137,17 +137,18 @@ public:
    * This name is the same string that is in the list returned
    * by GetDictionaryList().
    */
   NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary) = 0;
 
   /**
    * Tells the spellchecker to use a specific dictionary.
    * @param aDictionary a string that is in the list returned
-   * by GetDictionaryList().
+   * by GetDictionaryList() or an empty string. If aDictionary is 
+   * empty string, spellchecker will be disabled.
    */
   NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsISpellChecker, NS_ISPELLCHECKER_IID)
 
 #endif // nsISpellChecker_h__
 
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -1379,16 +1379,19 @@ nsresult mozInlineSpellChecker::DoSpellC
       if (inExclusion)
         continue;
     }
 
     // check spelling and add to selection if misspelled
     PRBool isMisspelled;
     aWordUtil.NormalizeWord(wordText);
     rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
+    if (NS_FAILED(rv))
+      continue;
+
     if (isMisspelled) {
       // misspelled words count extra toward the max
       wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
       AddRange(aSpellCheckSelection, wordRange);
 
       aStatus->mWordCount ++;
       if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
           SpellCheckSelectionIsFull())
@@ -1436,16 +1439,33 @@ mozInlineSpellChecker::ResumeCheck(mozIn
   mozInlineSpellWordUtil wordUtil;
   nsresult rv = wordUtil.Init(mEditor);
   if (NS_FAILED(rv))
     return NS_OK; // editor doesn't like us, don't assert
 
   nsCOMPtr<nsISelection> spellCheckSelection;
   rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
   NS_ENSURE_SUCCESS(rv, rv);
+
+  PRUnichar *currentDictionary = nsnull;
+  rv = mSpellCheck->GetCurrentDictionary(&currentDictionary);
+  if (NS_FAILED(rv)) {
+    // no active dictionary
+    PRInt32 count;
+    spellCheckSelection->GetRangeCount(&count);
+    for (PRInt32 index = count - 1; index >= 0; index--) {
+      nsCOMPtr<nsIDOMRange> checkRange;
+      spellCheckSelection->GetRangeAt(index, getter_AddRefs(checkRange));
+      if (checkRange) {
+        RemoveRange(spellCheckSelection, checkRange);
+      }
+    }
+    return NS_OK; 
+  }
+ 
   CleanupRangesInSelection(spellCheckSelection);
 
   rv = aStatus->FinishInitOnEvent(wordUtil);
   NS_ENSURE_SUCCESS(rv, rv);
   if (! aStatus->mRange)
     return NS_OK; // empty range, nothing to do
 
   PRBool doneChecking = PR_TRUE;
@@ -1728,8 +1748,43 @@ nsresult mozInlineSpellChecker::KeyPress
     case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
     case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
       HandleNavigationEvent(PR_TRUE /* force a spelling correction */);
       break;
   }
 
   return NS_OK;
 }
+
+NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
+{
+  if (!mSpellCheck) {
+    return NS_OK;
+  }
+
+  PRUnichar *previousDictionary = nsnull;
+  nsDependentString previousDictionaryStr;
+  if (NS_SUCCEEDED(mSpellCheck->GetCurrentDictionary(&previousDictionary))) {
+    previousDictionaryStr.Assign(previousDictionary);
+  }
+
+  nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+  nsresult rv = mSpellCheck->UpdateCurrentDictionary(editor);
+
+  PRUnichar *currentDictionary = nsnull;
+  nsDependentString currentDictionaryStr;
+  if (NS_SUCCEEDED(mSpellCheck->GetCurrentDictionary(&currentDictionary))) {
+    currentDictionaryStr.Assign(currentDictionary);
+  }
+
+  if (!previousDictionary || !currentDictionary || !previousDictionaryStr.Equals(currentDictionaryStr)) {
+      rv = SpellCheckRange(nsnull);
+  }
+
+  if (previousDictionary) {
+     nsMemory::Free(previousDictionary);
+  }
+  if (currentDictionary) {
+     nsMemory::Free(currentDictionary);
+  }
+
+  return rv;
+}
--- a/extensions/spellcheck/src/mozSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozSpellChecker.cpp
@@ -364,16 +364,22 @@ mozSpellChecker::GetCurrentDictionary(ns
 }
 
 NS_IMETHODIMP 
 mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary)
 {
   nsresult rv;
   nsCString *contractId;
 
+  if (aDictionary.IsEmpty()) {
+    mCurrentEngineContractId = nsnull;
+    mSpellCheckingEngine = nsnull;
+    return NS_OK;
+  }
+
   if (!mDictionariesMap.Get(aDictionary, &contractId)){
     NS_WARNING("Dictionary not found");
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (!mCurrentEngineContractId || !mCurrentEngineContractId->Equals(*contractId)){
     mSpellCheckingEngine = do_GetService(contractId->get(), &rv);
     if (NS_FAILED(rv))
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-1-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body>
+    <textarea spellcheck="false" lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+    <textarea lang="testing-XX">strangeimpossibleword</textarea>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-2-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+    var editor = document.getElementById('editor');
+    editor.addEventListener("focus", function() {
+        window.setTimeout(function() {
+            document.documentElement.className = '';
+        }, 0);
+    }, false);
+    editor.focus();
+}
+</script>
+<body onload="init()">
+    <div id="editor" lang="testing-XX" contenteditable="true" spellcheck="false">strangeimpossibleword</div>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+    var editor = document.getElementById('editor');
+    editor.addEventListener("focus", function() {
+        window.setTimeout(function() {
+            document.documentElement.className = '';
+        }, 0);
+    }, false);
+    editor.focus();
+}
+</script>
+<body onload="init()">
+    <div id="editor" lang="testing-XX" contenteditable="true">strangeimpossibleword</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-3-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+    var editor = document.getElementById('editor');
+    editor.setAttribute('lang', 'testing-XX');
+    editor.addEventListener("focus", function() {
+        window.setTimeout(function() {
+            document.documentElement.className = '';
+        }, 0);
+    }, false);
+    editor.focus();
+}
+</script>
+<body onload="init()">
+    <textarea id="editor" spellcheck="false" lang="en-US">strangeimpossibleword</textarea>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/editor/338427-3.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function init() {
+    var editor = document.getElementById('editor');
+    editor.setAttribute('lang', 'testing-XX');
+    editor.addEventListener("focus", function() {
+        window.setTimeout(function() {
+            document.documentElement.className = '';
+        }, 0);
+    }, false);
+    editor.focus();
+}
+</script>
+<body onload="init()">
+    <textarea id="editor" lang="en-US">strangeimpossibleword</textarea>
+</body>
+</html>
--- a/layout/reftests/editor/reftest.list
+++ b/layout/reftests/editor/reftest.list
@@ -59,8 +59,11 @@ fails-if(Android) != spellcheck-hyphen-m
 == unneeded_scroll.html unneeded_scroll-ref.html
 == caret_on_presshell_reinit.html caret_on_presshell_reinit-ref.html
 == caret_on_presshell_reinit-2.html caret_on_presshell_reinit-ref.html
 == 642800.html 642800-ref.html
 == selection_visibility_after_reframe.html selection_visibility_after_reframe-ref.html
 != selection_visibility_after_reframe-2.html selection_visibility_after_reframe-ref.html
 != selection_visibility_after_reframe-3.html selection_visibility_after_reframe-ref.html
 == 672709.html 672709-ref.html
+== 338427-1.html 338427-1-ref.html
+skip-if(Android) == 338427-2.html 338427-2-ref.html
+skip-if(Android) == 338427-3.html 338427-3-ref.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/unicode/langattribute-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="dk">
+<head>
+    <title>testing lang attribute</title>
+    <style>
+    div[lang="fr"] {
+        color: green;
+    }
+    div[lang="de"] > input {
+        color: blue;
+    }
+    div:not([lang]) {
+        color: yellow;
+    }
+    </style>
+</head>
+<body>
+<div lang="fr">fr language</div>
+<div lang="de">
+    <input value="de language">
+</div>
+<div>
+    dk language
+</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/unicode/langattribute.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="dk">
+<head>
+    <title>testing lang attribute</title>
+    <style>
+    div:lang(fr) {
+        color: green;
+    }
+    input:lang(de) {
+        color: blue;
+    }
+    div:lang(dk) {
+        color: yellow;
+    }
+    </style>
+</head>
+<body>
+<div lang="fr">fr language</div>
+<div lang="de">
+    <input value="de language">
+</div>
+<div>
+    dk language
+</div>
+</body>
+</html>
--- a/layout/reftests/unicode/reftest.list
+++ b/layout/reftests/unicode/reftest.list
@@ -1,6 +1,7 @@
 == unicode-attribute-selector.html unicode-ref.html
 == unicode-element-selector.html unicode-ref.html
 == unicode-lang.html unicode-ref.html
 == unicode-media-query-media-type.html unicode-ref-print.html
 == unicode-media-query-query.html unicode-ref-print.html
 == unicode-pseudo-selector.html unicode-ref.html
+== langattribute.html langattribute-ref.html
--- a/layout/style/nsCSSRuleProcessor.cpp
+++ b/layout/style/nsCSSRuleProcessor.cpp
@@ -1190,39 +1190,16 @@ nsCSSRuleProcessor::HasSystemMetric(nsIA
 nsCSSRuleProcessor::GetWindowsThemeIdentifier()
 {
   if (!sSystemMetrics)
     InitSystemMetrics();
   return sWinThemeId;
 }
 #endif
 
-// If we have a useful @lang, then aLang will end up nonempty.
-static void GetLang(nsIContent* aContent, nsString& aLang)
-{
-  for (nsIContent* content = aContent; content;
-       content = content->GetParent()) {
-    if (content->GetAttrCount() > 0) {
-      // xml:lang has precedence over lang on HTML elements (see
-      // XHTML1 section C.7).
-      PRBool hasAttr = content->GetAttr(kNameSpaceID_XML, nsGkAtoms::lang,
-                                        aLang);
-      if (!hasAttr && content->IsHTML()) {
-        hasAttr = content->GetAttr(kNameSpaceID_None, nsGkAtoms::lang,
-                                   aLang);
-      }
-      NS_ASSERTION(hasAttr || aLang.IsEmpty(),
-                   "GetAttr that returns false should not make string non-empty");
-      if (hasAttr) {
-        return;
-      }
-    }
-  }
-}
-
 /* static */
 nsEventStates
 nsCSSRuleProcessor::GetContentState(Element* aElement)
 {
   // FIXME: RequestLinkStateUpdate is a hack; see bug 660959.
   aElement->RequestLinkStateUpdate();
   nsEventStates state = aElement->State();
 
@@ -1692,17 +1669,17 @@ static PRBool SelectorMatches(Element* a
             return PR_FALSE;
           }
 
           // We have to determine the language of the current element.  Since
           // this is currently no property and since the language is inherited
           // from the parent we have to be prepared to look at all parent
           // nodes.  The language itself is encoded in the LANG attribute.
           nsAutoString language;
-          GetLang(aElement, language);
+          aElement->GetLang(language);
           if (!language.IsEmpty()) {
             if (!nsStyleUtil::DashMatchCompare(language,
                                                nsDependentString(pseudoClass->u.mString),
                                                nsASCIICaseInsensitiveStringComparator())) {
               return PR_FALSE;
             }
             // This pseudo-class matched; move on to the next thing
             break;
--- a/toolkit/content/InlineSpellChecker.jsm
+++ b/toolkit/content/InlineSpellChecker.jsm
@@ -126,18 +126,22 @@ InlineSpellChecker.prototype = {
   // this prepends up to "maxNumber" suggestions at the given menu position
   // for the word under the cursor. Returns the number of suggestions inserted.
   addSuggestionsToMenu: function(menu, insertBefore, maxNumber)
   {
     if (! this.mInlineSpellChecker || ! this.mOverMisspelling)
       return 0; // nothing to do
 
     var spellchecker = this.mInlineSpellChecker.spellChecker;
-    if (! spellchecker.CheckCurrentWord(this.mMisspelling))
-      return 0;  // word seems not misspelled after all (?)
+    try {
+      if (! spellchecker.CheckCurrentWord(this.mMisspelling))
+        return 0;  // word seems not misspelled after all (?)
+    } catch(e) {
+        return 0;
+    }
 
     this.mMenu = menu;
     this.mSpellSuggestions = [];
     this.mSuggestionItems = [];
     for (var i = 0; i < maxNumber; i ++) {
       var suggestion = spellchecker.GetSuggestedWord();
       if (! suggestion.length)
         break;
@@ -187,17 +191,20 @@ InlineSpellChecker.prototype = {
 
     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 = spellchecker.GetCurrentDictionary();
+    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 {