Bug 338427 - Spellchecker should respect the langi attribute; r=ehsan
authorarno renevier <arno@renevier.net>
Fri, 12 Aug 2011 15:12:45 -0400
changeset 74356 f3f7872db0aef93fcd565ee96a25fb8c8f02179f
parent 74355 5ca0d6677b2c94da15a7e65d8824f586898d485b
child 74357 37605826f6fb7b21edbf8665c799de8521029035
push id20986
push userkhuey@mozilla.com
push dateSun, 14 Aug 2011 11:45:15 +0000
treeherdermozilla-central@2de3cff973b2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs338427
milestone8.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 {