Bug 889335 - Implement navigator.languages and languagechange event. r=sicking,smaug

This commit is contained in:
Mounir Lamouri 2014-05-07 11:22:03 +01:00
parent 9da606e74a
commit 49cb274eeb
11 changed files with 339 additions and 40 deletions

View File

@ -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")

View File

@ -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<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());
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<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

View File

@ -251,6 +251,8 @@ public:
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 */);

View File

@ -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<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;
}

View File

@ -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]

View File

@ -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>

View File

@ -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 <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?

View File

@ -24,7 +24,7 @@ interface nsIVariant;
* @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
@ -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;

View File

@ -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;

View File

@ -52,6 +52,7 @@ interface NavigatorID {
[NoInterfaceObject]
interface NavigatorLanguage {
readonly attribute DOMString? language;
[Pure, Cached, Frozen] readonly attribute sequence<DOMString> languages;
};
[NoInterfaceObject]

View File

@ -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)