diff --git a/content/base/src/nsGkAtomList.h b/content/base/src/nsGkAtomList.h index 61c05906eb99..c887a8545be4 100644 --- a/content/base/src/nsGkAtomList.h +++ b/content/base/src/nsGkAtomList.h @@ -747,6 +747,7 @@ 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") diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index cd625615ef6c..e8dd0d08327b 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -351,59 +351,87 @@ Navigator::GetAppName(nsAString& aAppName) } /** - * 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. + * 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"). * - * Do not use UI language (chosen app locale) here. - * See RFC 2616, Section 15.1.4 "Privacy Issues Connected to Accept Headers" + * "en", "en-US" and "i-cherokee" and "" are valid languages tokens. * - * "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. + * An empty array will be returned if there is no valid languages. */ -NS_IMETHODIMP -Navigator::GetLanguage(nsAString& aLanguage) +void +Navigator::GetAcceptLanguages(nsTArray& 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()); + while (langTokenizer.hasMoreTokens()) { + nsDependentSubstring lang = langTokenizer.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; - } - - 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); + // 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('-')); } - pos += code.Length() + 1; // 1 is the separator - first = false; + // 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); + } +} + +/** + * 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 languages; + GetLanguages(languages); + if (languages.Length() >= 1) { + aLanguage.Assign(languages[0]); + } else { + aLanguage.Truncate(); } - return NS_OK; + return NS_OK; +} + +void +Navigator::GetLanguages(nsTArray& 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 diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index c586b2e0561c..0fef4cb8afc1 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -251,6 +251,8 @@ public: JS::MutableHandle aDesc); void GetOwnPropertyNames(JSContext* aCx, nsTArray& aNames, ErrorResult& aRv); + void GetLanguages(nsTArray& aLanguages); + void GetAcceptLanguages(nsTArray& aLanguages); // WebIDL helper methods static bool HasBatterySupport(JSContext* /* unused*/, JSObject* /*unused */); diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index a9e1f502a880..c42670bffbd4 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -218,6 +218,7 @@ #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 @@ -1137,6 +1138,8 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalWindow *aOuterWindow) // 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 @@ -1419,6 +1422,8 @@ nsGlobalWindow::CleanUp() 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(); @@ -11208,6 +11213,32 @@ nsGlobalWindow::Observe(nsISupports* aSubject, const char* aTopic, } #endif // MOZ_B2G + if (!nsCRT::strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + MOZ_ASSERT(!nsCRT::strcmp(aData, "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 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; } diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index e77a9d1db63a..6f431c9f5437 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -44,6 +44,7 @@ support-files = [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] diff --git a/dom/base/test/test_navigator_language.html b/dom/base/test/test_navigator_language.html new file mode 100644 index 000000000000..f468cb1b1c60 --- /dev/null +++ b/dom/base/test/test_navigator_language.html @@ -0,0 +1,227 @@ + + + + + + Test for NavigatorLanguage + + + + +Mozilla Bug 889335 +

+ +
+
+ + + diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h index cbb6f54efda6..83702a6dfe8d 100644 --- a/dom/events/EventNameList.h +++ b/dom/events/EventNameList.h @@ -464,6 +464,10 @@ 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 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? diff --git a/dom/interfaces/base/nsIDOMWindow.idl b/dom/interfaces/base/nsIDOMWindow.idl index a66afbe08ccd..e8d844ea6701 100644 --- a/dom/interfaces/base/nsIDOMWindow.idl +++ b/dom/interfaces/base/nsIDOMWindow.idl @@ -24,7 +24,7 @@ interface nsIVariant; * @see */ -[scriptable, uuid(8c115ab3-cf96-492c-850c-3b18056b45e2)] +[scriptable, uuid(fbefa573-0ba2-4d15-befb-d60277643a0b)] interface nsIDOMWindow : nsISupports { // the current browsing context @@ -478,6 +478,7 @@ interface nsIDOMWindow : nsISupports [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; diff --git a/dom/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl index 216ddd686dae..5ad29bec157f 100644 --- a/dom/webidl/EventHandler.webidl +++ b/dom/webidl/EventHandler.webidl @@ -123,6 +123,7 @@ interface WindowEventHandlers { attribute EventHandler onbeforeprint; attribute OnBeforeUnloadEventHandler onbeforeunload; attribute EventHandler onhashchange; + attribute EventHandler onlanguagechange; attribute EventHandler onmessage; attribute EventHandler onoffline; attribute EventHandler ononline; diff --git a/dom/webidl/Navigator.webidl b/dom/webidl/Navigator.webidl index 0634130a8458..79b4c939abe6 100644 --- a/dom/webidl/Navigator.webidl +++ b/dom/webidl/Navigator.webidl @@ -52,6 +52,7 @@ interface NavigatorID { [NoInterfaceObject] interface NavigatorLanguage { readonly attribute DOMString? language; + [Pure, Cached, Frozen] readonly attribute sequence languages; }; [NoInterfaceObject] diff --git a/widget/BasicEvents.h b/widget/BasicEvents.h index 1505217502e0..0f4ec9102f8f 100644 --- a/widget/BasicEvents.h +++ b/widget/BasicEvents.h @@ -120,6 +120,8 @@ enum nsEventStructType // 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)