зеркало из https://github.com/mozilla/gecko-dev.git
Bug 889335 - Implement navigator.languages and languagechange event. r=sicking,smaug
This commit is contained in:
Родитель
8bb825ef6e
Коммит
6311b5211c
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
|
@ -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?
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -52,6 +52,7 @@ interface NavigatorID {
|
|||
[NoInterfaceObject]
|
||||
interface NavigatorLanguage {
|
||||
readonly attribute DOMString? language;
|
||||
[Pure, Cached, Frozen] readonly attribute sequence<DOMString> languages;
|
||||
};
|
||||
|
||||
[NoInterfaceObject]
|
||||
|
|
|
@ -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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче