Bug 889335 - Implement navigator.languages and languagechange event. r=sicking,smaug
authorMounir Lamouri <mounir@lamouri.fr>
Mon, 12 May 2014 13:48:15 +0200
changeset 182612 45ae4b13806977b919af3fcaf1273fb99a794d12
parent 182611 bb0df5c0ba1e008ec7cdb758dc946609e25e1e8c
child 182613 5ceb693e97330c2f341eb8ae36a5fc650c120658
push id43345
push usermounir@lamouri.fr
push dateMon, 12 May 2014 11:51:27 +0000
treeherdermozilla-inbound@45ae4b138069 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssicking, smaug
bugs889335
milestone32.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 889335 - Implement navigator.languages and languagechange event. r=sicking,smaug
content/base/src/nsGkAtomList.h
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/base/nsGlobalWindow.cpp
dom/base/test/mochitest.ini
dom/base/test/test_navigator_language.html
dom/events/EventNameList.h
dom/interfaces/base/nsIDOMWindow.idl
dom/webidl/EventHandler.webidl
dom/webidl/Navigator.webidl
widget/BasicEvents.h
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -742,16 +742,17 @@ GK_ATOM(oniccdetected, "oniccdetected")
 GK_ATOM(oniccinfochange, "oniccinfochange")
 GK_ATOM(oniccundetected, "oniccundetected")
 GK_ATOM(onincoming, "onincoming")
 GK_ATOM(oninput, "oninput")
 GK_ATOM(oninvalid, "oninvalid")
 GK_ATOM(onkeydown, "onkeydown")
 GK_ATOM(onkeypress, "onkeypress")
 GK_ATOM(onkeyup, "onkeyup")
+GK_ATOM(onlanguagechange, "onlanguagechange")
 GK_ATOM(onlevelchange, "onlevelchange")
 GK_ATOM(onLoad, "onLoad")
 GK_ATOM(onload, "onload")
 GK_ATOM(onpopstate, "onpopstate")
 GK_ATOM(only, "only")               // this one is not an event
 GK_ATOM(onmessage, "onmessage")
 GK_ATOM(onmousedown, "onmousedown")
 GK_ATOM(onmouseenter, "onmouseenter")
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -346,69 +346,97 @@ Navigator::GetAppVersion(nsAString& aApp
 NS_IMETHODIMP
 Navigator::GetAppName(nsAString& aAppName)
 {
   NS_GetNavigatorAppName(aAppName);
   return NS_OK;
 }
 
 /**
- * JS property navigator.language, exposed to web content.
- * Take first value from Accept-Languages (HTTP header), which is
- * the "content language" freely set by the user in the Pref window.
- *
- * Do not use UI language (chosen app locale) here.
- * See RFC 2616, Section 15.1.4 "Privacy Issues Connected to Accept Headers"
+ * Returns the value of Accept-Languages (HTTP header) as a nsTArray of
+ * languages. The value is set in the preference by the user ("Content
+ * Languages").
  *
- * "en", "en-US" and "i-cherokee" and "" are valid.
- * Fallback in case of invalid pref should be "" (empty string), to
- * let site do fallback, e.g. to site's local language.
+ * "en", "en-US" and "i-cherokee" and "" are valid languages tokens.
+ *
+ * An empty array will be returned if there is no valid languages.
  */
-NS_IMETHODIMP
-Navigator::GetLanguage(nsAString& aLanguage)
+void
+Navigator::GetAcceptLanguages(nsTArray<nsString>& aLanguages)
 {
   // E.g. "de-de, en-us,en".
   const nsAdoptingString& acceptLang =
     Preferences::GetLocalizedString("intl.accept_languages");
 
-  // Take everything before the first "," or ";", without trailing space.
+  // Split values on commas.
   nsCharSeparatedTokenizer langTokenizer(acceptLang, ',');
-  const nsSubstring &firstLangPart = langTokenizer.nextToken();
-  nsCharSeparatedTokenizer qTokenizer(firstLangPart, ';');
-  aLanguage.Assign(qTokenizer.nextToken());
-
-  // Checks and fixups:
-  // replace "_" with "-" to avoid POSIX/Windows "en_US" notation.
-  if (aLanguage.Length() > 2 && aLanguage[2] == char16_t('_')) {
-    aLanguage.Replace(2, 1, char16_t('-')); // TODO replace all
-  }
-
-  // Use uppercase for country part, e.g. "en-US", not "en-us", see BCP47
-  // only uppercase 2-letter country codes, not "zh-Hant", "de-DE-x-goethe".
-  if (aLanguage.Length() <= 2) {
-    return NS_OK;
+  while (langTokenizer.hasMoreTokens()) {
+    nsDependentSubstring lang = langTokenizer.nextToken();
+
+    // Replace "_" with "-" to avoid POSIX/Windows "en_US" notation.
+    // NOTE: we should probably rely on the pref being set correctly.
+    if (lang.Length() > 2 && lang[2] == char16_t('_')) {
+      lang.Replace(2, 1, char16_t('-'));
+    }
+
+    // Use uppercase for country part, e.g. "en-US", not "en-us", see BCP47
+    // only uppercase 2-letter country codes, not "zh-Hant", "de-DE-x-goethe".
+    // NOTE: we should probably rely on the pref being set correctly.
+    if (lang.Length() > 2) {
+      nsCharSeparatedTokenizer localeTokenizer(lang, '-');
+      int32_t pos = 0;
+      bool first = true;
+      while (localeTokenizer.hasMoreTokens()) {
+        const nsSubstring& code = localeTokenizer.nextToken();
+
+        if (code.Length() == 2 && !first) {
+          nsAutoString upper(code);
+          ToUpperCase(upper);
+          lang.Replace(pos, code.Length(), upper);
+        }
+
+        pos += code.Length() + 1; // 1 is the separator
+        first = false;
+      }
+    }
+
+    aLanguages.AppendElement(lang);
   }
-
-  nsCharSeparatedTokenizer localeTokenizer(aLanguage, '-');
-  int32_t pos = 0;
-  bool first = true;
-  while (localeTokenizer.hasMoreTokens()) {
-    const nsSubstring& code = localeTokenizer.nextToken();
-
-    if (code.Length() == 2 && !first) {
-      nsAutoString upper(code);
-      ToUpperCase(upper);
-      aLanguage.Replace(pos, code.Length(), upper);
-    }
-
-    pos += code.Length() + 1; // 1 is the separator
-    first = false;
+}
+
+/**
+ * Do not use UI language (chosen app locale) here but the first value set in
+ * the Accept Languages header, see ::GetAcceptLanguages().
+ *
+ * See RFC 2616, Section 15.1.4 "Privacy Issues Connected to Accept Headers" for
+ * the reasons why.
+ */
+NS_IMETHODIMP
+Navigator::GetLanguage(nsAString& aLanguage)
+{
+  nsTArray<nsString> languages;
+  GetLanguages(languages);
+  if (languages.Length() >= 1) {
+    aLanguage.Assign(languages[0]);
+  } else {
+    aLanguage.Truncate();
   }
 
-  return NS_OK;
+    return NS_OK;
+}
+
+void
+Navigator::GetLanguages(nsTArray<nsString>& aLanguages)
+{
+  GetAcceptLanguages(aLanguages);
+
+  // The returned value is cached by the binding code. The window listen to the
+  // accept languages change and will clear the cache when needed. It has to
+  // take care of dispatching the DOM event already and the invalidation and the
+  // event has to be timed correctly.
 }
 
 NS_IMETHODIMP
 Navigator::GetPlatform(nsAString& aPlatform)
 {
   return NS_GetNavigatorPlatform(aPlatform);
 }
 
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -246,16 +246,18 @@ public:
                               uint64_t aInnerWindowID,
                               ErrorResult& aRv);
 #endif // MOZ_MEDIA_NAVIGATOR
   bool DoNewResolve(JSContext* aCx, JS::Handle<JSObject*> aObject,
                     JS::Handle<jsid> aId,
                     JS::MutableHandle<JSPropertyDescriptor> aDesc);
   void GetOwnPropertyNames(JSContext* aCx, nsTArray<nsString>& aNames,
                            ErrorResult& aRv);
+  void GetLanguages(nsTArray<nsString>& aLanguages);
+  void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
 
   // WebIDL helper methods
   static bool HasBatterySupport(JSContext* /* unused*/, JSObject* /*unused */);
   static bool HasPowerSupport(JSContext* /* unused */, JSObject* aGlobal);
   static bool HasPhoneNumberSupport(JSContext* /* unused */, JSObject* aGlobal);
   static bool HasIdleSupport(JSContext* /* unused */, JSObject* aGlobal);
   static bool HasWakeLockSupport(JSContext* /* unused*/, JSObject* /*unused */);
   static bool HasDesktopNotificationSupport(JSContext* /* unused*/,
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -213,16 +213,17 @@
 #include "mozilla/dom/AudioContext.h"
 #include "mozilla/dom/BrowserElementDictionariesBinding.h"
 #include "mozilla/dom/Console.h"
 #include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "nsITabChild.h"
 #include "mozilla/dom/MediaQueryList.h"
 #include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/NavigatorBinding.h"
 #ifdef HAVE_SIDEBAR
 #include "mozilla/dom/ExternalBinding.h"
 #endif
 
 #ifdef MOZ_WEBSPEECH
 #include "mozilla/dom/SpeechSynthesis.h"
 #endif
 
@@ -1132,16 +1133,18 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
         // a strong reference.
         os->AddObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC,
                         false);
 
         // Watch for dom-storage2-changed so we can fire storage
         // events. Use a strong reference.
         os->AddObserver(mObserver, "dom-storage2-changed", false);
       }
+
+      Preferences::AddStrongObserver(mObserver, "intl.accept_languages");
     }
   } else {
     // |this| is an outer window. Outer windows start out frozen and
     // remain frozen until they get an inner window, so freeze this
     // outer window here.
     Freeze();
 
     mObserver = nullptr;
@@ -1414,16 +1417,18 @@ nsGlobalWindow::CleanUp()
     DisableNetworkEvent(NS_NETWORK_UPLOAD_EVENT);
     DisableNetworkEvent(NS_NETWORK_DOWNLOAD_EVENT);
 #endif // MOZ_B2G
 
     if (mIdleService) {
       mIdleService->RemoveIdleObserver(mObserver, MIN_IDLE_NOTIFICATION_TIME_S);
     }
 
+    Preferences::RemoveObserver(mObserver, "intl.accept_languages");
+
     // Drop its reference to this dying window, in case for some bogus reason
     // the object stays around.
     mObserver->Forget();
     NS_RELEASE(mObserver);
   }
 
   if (mNavigator) {
     mNavigator->Invalidate();
@@ -11214,16 +11219,42 @@ nsGlobalWindow::Observe(nsISupports* aSu
 
     event->SetTrusted(true);
 
     bool dummy;
     return DispatchEvent(event, &dummy);
   }
 #endif // MOZ_B2G
 
+  if (!nsCRT::strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+    MOZ_ASSERT(!nsCRT::strcmp(NS_ConvertUTF16toUTF8(aData).get(), "intl.accept_languages"));
+    MOZ_ASSERT(IsInnerWindow());
+
+    // The user preferred languages have changed, we need to fire an event on
+    // Window object and invalidate the cache for navigator.languages. It is
+    // done for every change which can be a waste of cycles but those should be
+    // fairly rare.
+    // We MUST invalidate navigator.languages before sending the event in the
+    // very likely situation where an event handler will try to read its value.
+
+    if (mNavigator) {
+      NavigatorBinding::ClearCachedLanguagesValue(mNavigator);
+    }
+
+    nsCOMPtr<nsIDOMEvent> event;
+    NS_NewDOMEvent(getter_AddRefs(event), this, nullptr, nullptr);
+    nsresult rv = event->InitEvent(NS_LITERAL_STRING("languagechange"), false, false);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    event->SetTrusted(true);
+
+    bool dummy;
+    return DispatchEvent(event, &dummy);
+  }
+
   NS_WARNING("unrecognized topic in nsGlobalWindow::Observe");
   return NS_ERROR_FAILURE;
 }
 
 nsresult
 nsGlobalWindow::CloneStorageEvent(const nsAString& aType,
                                   nsCOMPtr<nsIDOMStorageEvent>& aEvent)
 {
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -40,16 +40,17 @@ support-files =
 [test_messageChannel_post.html]
 [test_messageChannel_pref.html]
 [test_messageChannel_start.html]
 [test_messagemanager_targetchain.html]
 [test_messageChannel_transferable.html]
 [test_messageChannel_unshipped.html]
 [test_named_frames.html]
 [test_navigator_resolve_identity.html]
+[test_navigator_language.html]
 [test_nondomexception.html]
 [test_openDialogChromeOnly.html]
 [test_postMessage_solidus.html]
 [test_screen_orientation.html]
 [test_settimeout_extra_arguments.html]
 [test_settimeout_inner.html]
 [test_setting_opener.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_navigator_language.html
@@ -0,0 +1,227 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=889335
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for NavigatorLanguage</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=889335">Mozilla Bug 889335</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript;version=1.7">
+  "use strict";
+
+  SimpleTest.waitForExplicitFinish();
+
+  /** Test for NavigatorLanguage **/
+  var prefValue = null;
+  var actualLanguageChangesFromHandler = 0;
+  var actualLanguageChangesFromAVL = 0;
+  var expectedLanguageChanges = 0;
+
+  function setUp() {
+    try {
+      prefValue = SpecialPowers.getCharPref('intl.accept_languages');
+    } catch (e) {
+    }
+  }
+
+  function tearDown() {
+    SpecialPowers.setCharPref('intl.accept_languages', prefValue);
+  }
+
+  var testValues = [
+    { accept_languages: 'foo', language: 'foo', languages: ['foo'] },
+    { accept_languages: '', language: '', languages: [] },
+    { accept_languages: 'foo,bar', language: 'foo', languages: [ 'foo', 'bar' ] },
+    { accept_languages: '  foo , bar ', language: 'foo', languages: [ 'foo', 'bar' ] },
+    { accept_languages: '  foo ; bar ', language: 'foo ; bar', languages: [ 'foo ; bar' ] },
+    { accept_languages: '_foo_', language: '_foo_', languages: ['_foo_'] },
+    { accept_languages: 'en_', language: 'en-', languages: ['en-'] },
+    { accept_languages: 'en__', language: 'en-_', languages: ['en-_'] },
+    { accept_languages: 'en_US, fr_FR', language: 'en-US', languages: ['en-US', 'fr-FR'] },
+    { accept_languages: 'en_US_CA', language: 'en-US_CA', languages: ['en-US_CA'] },
+    { accept_languages: 'en_us-ca', language: 'en-US-CA', languages: ['en-US-CA'] },
+    { accept_languages: 'en_us-cal, en_us-c', language: 'en-US-cal', languages: ['en-US-cal', 'en-US-c'] },
+  ];
+
+  var currentTestIdx = 0;
+  var tests = [];
+  function nextTest() {
+    currentTestIdx++;
+    if (currentTestIdx >= tests.length) {
+      tearDown();
+      SimpleTest.finish();
+    }
+
+    tests[currentTestIdx]();
+  }
+
+  // Check that the API is there.
+  tests.push(function testAPIPresence() {
+    ok('language' in window.navigator);
+    ok('languages' in window.navigator);
+    ok('onlanguagechange' in window);
+
+    nextTest();
+  });
+
+  // Check that calling navigator.languages return the same array, unless there
+  // was a change.
+  tests.push(function testArrayCached() {
+    var previous = navigator.languages;
+    is(navigator.languages, navigator.languages, "navigator.languages is cached");
+    is(navigator.languages, previous, "navigator.languages is cached");
+
+    window.onlanguagechange = function() {
+      isnot(navigator.languages, previous, "navigator.languages cached value was updated");
+      window.onlanguagechange = null;
+
+      nextTest();
+    }
+
+    setTimeout(function() {
+      SpecialPowers.setCharPref('intl.accept_languages', 'testArrayCached');
+    }, 0);
+  });
+
+  // Test that event handler inside the <body> works as expected and that the
+  // event has the expected properties.
+  tests.push(function testEventProperties() {
+    document.body.setAttribute('onlanguagechange',
+      "document.body.removeAttribute('onlanguagechange');" +
+      "is(event.cancelable, false); is(event.bubbles, false);" +
+      "nextTest();");
+
+    setTimeout(function() {
+      SpecialPowers.setCharPref('intl.accept_languages', 'testEventProperties');
+    }, 0);
+  });
+
+  // Check that the returned values such as the behavior when the underlying
+  // languages change.
+  tests.push(function testBasicBehaviour() {
+    function checkIfDoneAndProceed() {
+      if (actualLanguageChangesFromHandler == actualLanguageChangesFromAVL) {
+        if (genEvents.next().done) {
+          window.onlanguagechange = null;
+          window.removeEventListener('languagechange', languageChangeAVL);
+          nextTest();
+        }
+      }
+    }
+    window.onlanguagechange = function() {
+      actualLanguageChangesFromHandler++;
+      checkIfDoneAndProceed();
+    }
+    function languageChangeAVL() {
+      actualLanguageChangesFromAVL++;
+      checkIfDoneAndProceed();
+    }
+    window.addEventListener('languagechange', languageChangeAVL);
+
+    function* testEvents() {
+      for (var i = 0; i < testValues.length; ++i) {
+        var data = testValues[i];
+        setTimeout(function(data) {
+          SpecialPowers.setCharPref('intl.accept_languages', data.accept_languages);
+        }, 0, data);
+        expectedLanguageChanges++;
+        yield undefined;
+
+        is(actualLanguageChangesFromAVL, expectedLanguageChanges);
+        is(actualLanguageChangesFromHandler, expectedLanguageChanges);
+
+        is(navigator.language, data.language);
+        is(navigator.languages.length, data.languages.length);
+        if (navigator.languages.length > 0) {
+          is(navigator.languages[0], navigator.language)
+        }
+        for (var j = 0; j < navigator.languages.length; ++j) {
+          is(navigator.languages[j], data.languages[j]);
+        }
+      }
+    }
+
+    var genEvents = testEvents();
+    genEvents.next();
+  });
+
+  // Check that the orientationchange event isn't sent twice if the preference
+  // is set to the same value.
+  tests.push(function testOnlyFireIfRealChange() {
+    function* changeLanguage() {
+      setTimeout(function() {
+        SpecialPowers.setCharPref('intl.accept_languages', 'fr-CA');
+      });
+      yield undefined;
+
+      setTimeout(function() {
+        // Twice the same change, should fire only one event.
+        SpecialPowers.setCharPref('intl.accept_languages', 'fr-CA');
+        setTimeout(function() {
+          // A real change to tell the test it should now count how many changes were
+          // received until now.
+          SpecialPowers.setCharPref('intl.accept_languages', 'fr-FR');
+        });
+      });
+      yield undefined;
+    }
+
+    var genChanges = changeLanguage();
+
+    var doubleEventCount = 0;
+    window.onlanguagechange = function() {
+      if (navigator.language == 'fr-FR') {
+        is(1, doubleEventCount);
+        window.onlanguagechange = null;
+        nextTest();
+        return;
+      }
+
+      if (navigator.language == 'fr-CA') {
+        doubleEventCount++;
+      }
+      genChanges.next();
+    }
+
+    genChanges.next();
+  });
+
+  // Check that there is no crash when a change happen after a window listening
+  // to them is killed.
+  tests.push(function testThatAddingAnEventDoesNotHaveSideEffects() {
+    var frame = document.createElement('iframe');
+    frame.src = 'data:text/html,<script>window.onlanguagechange=function(){}<\/script>';
+    document.body.appendChild(frame);
+
+    frame.contentWindow.onload = function() {
+      document.body.removeChild(frame);
+      frame = null;
+
+      SpecialPowers.exactGC(window, function() {
+        // This should not crash.
+        SpecialPowers.setCharPref('intl.accept_languages', 'en-GB');
+
+        nextTest();
+      });
+    }
+  });
+
+  // There is one test using document.body.
+  addLoadEvent(function() {
+    setUp();
+    tests[0]();
+  });
+
+</script>
+</body>
+</html>
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -459,16 +459,20 @@ WINDOW_EVENT(beforeprint,
 BEFOREUNLOAD_EVENT(beforeunload,
                    NS_BEFORE_PAGE_UNLOAD,
                    EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly,
                    NS_EVENT)
 WINDOW_EVENT(hashchange,
              NS_HASHCHANGE,
              EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly,
              NS_EVENT)
+WINDOW_EVENT(languagechange,
+             NS_LANGUAGECHANGE,
+             EventNameType_HTMLBodyOrFramesetOnly,
+             NS_EVENT)
 // XXXbz Should the onmessage attribute on <body> really not work?  If so, do we
 // need a different macro to flag things like that (IDL, but not content
 // attributes on body/frameset), or is just using EventNameType_None enough?
 WINDOW_EVENT(message,
              NS_MESSAGE,
              EventNameType_None,
              NS_EVENT)
 WINDOW_EVENT(offline,
--- a/dom/interfaces/base/nsIDOMWindow.idl
+++ b/dom/interfaces/base/nsIDOMWindow.idl
@@ -19,17 +19,17 @@ interface nsIVariant;
  * The nsIDOMWindow interface is the primary interface for a DOM
  * window object. It represents a single window object that may
  * contain child windows if the document in the window contains a
  * HTML frameset document or if the document contains iframe elements.
  *
  * @see <http://www.whatwg.org/html/#window>
  */
 
-[scriptable, uuid(8c115ab3-cf96-492c-850c-3b18056b45e2)]
+[scriptable, uuid(fbefa573-0ba2-4d15-befb-d60277643a0b)]
 interface nsIDOMWindow : nsISupports
 {
   // the current browsing context
   readonly attribute nsIDOMWindow                       window;
 
   /* [replaceable] self */
   readonly attribute nsIDOMWindow                       self;
 
@@ -473,16 +473,17 @@ interface nsIDOMWindow : nsISupports
 
   /**
    * HTML5 event attributes that only apply to windows and <body>/<frameset>
    */
   [implicit_jscontext] attribute jsval onafterprint;
   [implicit_jscontext] attribute jsval onbeforeprint;
   [implicit_jscontext] attribute jsval onbeforeunload;
   [implicit_jscontext] attribute jsval onhashchange;
+  [implicit_jscontext] attribute jsval onlanguagechange;
   [implicit_jscontext] attribute jsval onmessage;
   [implicit_jscontext] attribute jsval onoffline;
   [implicit_jscontext] attribute jsval ononline;
   [implicit_jscontext] attribute jsval onpopstate;
   [implicit_jscontext] attribute jsval onpagehide;
   [implicit_jscontext] attribute jsval onpageshow;
   // Not supported yet
   // [implicit_jscontext] attribute jsval onredo;
--- a/dom/webidl/EventHandler.webidl
+++ b/dom/webidl/EventHandler.webidl
@@ -118,16 +118,17 @@ interface GlobalEventHandlers {
 };
 
 [NoInterfaceObject]
 interface WindowEventHandlers {
            attribute EventHandler onafterprint;
            attribute EventHandler onbeforeprint;
            attribute OnBeforeUnloadEventHandler onbeforeunload;
            attribute EventHandler onhashchange;
+           attribute EventHandler onlanguagechange;
            attribute EventHandler onmessage;
            attribute EventHandler onoffline;
            attribute EventHandler ononline;
            attribute EventHandler onpagehide;
            attribute EventHandler onpageshow;
            attribute EventHandler onpopstate;
            attribute EventHandler onresize;
            //(Not implemented)attribute EventHandler onstorage;
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -47,16 +47,17 @@ interface NavigatorID {
 
   // Everyone but WebKit/Blink supports this.  See bug 679971.
   boolean taintEnabled(); // constant false
 };
 
 [NoInterfaceObject]
 interface NavigatorLanguage {
   readonly attribute DOMString? language;
+  [Pure, Cached, Frozen] readonly attribute sequence<DOMString> languages;
 };
 
 [NoInterfaceObject]
 interface NavigatorOnLine {
   readonly attribute boolean onLine;
 };
 
 [NoInterfaceObject]
--- a/widget/BasicEvents.h
+++ b/widget/BasicEvents.h
@@ -115,16 +115,18 @@ enum nsEventStructType
 #define NS_MOZ_USER_IDLE                 (NS_WINDOW_START + 67)
 #define NS_MOZ_USER_ACTIVE               (NS_WINDOW_START + 68)
 
 // The resolution at which a plugin should draw has changed, for
 // example as the result of changing from a HiDPI mode to a non-
 // HiDPI mode.
 #define NS_PLUGIN_RESOLUTION_CHANGED     (NS_WINDOW_START + 69)
 
+#define NS_LANGUAGECHANGE                (NS_WINDOW_START + 70)
+
 #define NS_MOUSE_MESSAGE_START          300
 #define NS_MOUSE_MOVE                   (NS_MOUSE_MESSAGE_START)
 #define NS_MOUSE_BUTTON_UP              (NS_MOUSE_MESSAGE_START + 1)
 #define NS_MOUSE_BUTTON_DOWN            (NS_MOUSE_MESSAGE_START + 2)
 #define NS_MOUSE_ENTER                  (NS_MOUSE_MESSAGE_START + 22)
 #define NS_MOUSE_EXIT                   (NS_MOUSE_MESSAGE_START + 23)
 #define NS_MOUSE_DOUBLECLICK            (NS_MOUSE_MESSAGE_START + 24)
 #define NS_MOUSE_CLICK                  (NS_MOUSE_MESSAGE_START + 27)