Bug 1344445 - Add LocaleService::GetRequestedLocales. r?jfkthame
MozReview-Commit-ID: 76xYTKk6OHh
--- a/intl/locale/LocaleService.cpp
+++ b/intl/locale/LocaleService.cpp
@@ -3,26 +3,37 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LocaleService.h"
#include <algorithm> // find_if()
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/intl/OSPreferences.h"
#include "nsIObserverService.h"
#include "nsIToolkitChromeRegistry.h"
#ifdef ENABLE_INTL_API
#include "unicode/uloc.h"
#endif
+#define MATCH_OS_LOCALE_PREF "intl.locale.matchOS"
+#define SELECTED_LOCALE_PREF "general.useragent.locale"
+
+static const char* kObservedPrefs[] = {
+ MATCH_OS_LOCALE_PREF,
+ SELECTED_LOCALE_PREF,
+ nullptr
+};
+
using namespace mozilla::intl;
-NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService)
+NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService, nsIObserver)
mozilla::StaticRefPtr<LocaleService> LocaleService::sInstance;
/**
* This function performs the actual language negotiation for the API.
*
* Currently it collects the locale ID used by nsChromeRegistry and
* adds hardcoded "en-US" locale as a fallback.
@@ -48,30 +59,77 @@ ReadAppLocales(nsTArray<nsCString>& aRet
}
}
LocaleService*
LocaleService::GetInstance()
{
if (!sInstance) {
sInstance = new LocaleService();
+
+ // We're going to observe for requested languages changes which come
+ // from prefs.
+ DebugOnly<nsresult> rv = Preferences::AddStrongObservers(sInstance, kObservedPrefs);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
ClearOnShutdown(&sInstance);
}
return sInstance;
}
+LocaleService::~LocaleService()
+{
+ Preferences::RemoveObservers(sInstance, kObservedPrefs);
+}
+
void
LocaleService::GetAppLocales(nsTArray<nsCString>& aRetVal)
{
if (mAppLocales.IsEmpty()) {
ReadAppLocales(mAppLocales);
}
aRetVal = mAppLocales;
}
+bool
+LocaleService::GetRequestedLocales(nsTArray<nsCString>& aRetVal)
+{
+ nsAutoCString locale;
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService("@mozilla.org/preferences-service;1"));
+
+ // First, we'll try to check if the user has `matchOS` pref selected
+ bool matchOSLocale = Preferences::GetBool(MATCH_OS_LOCALE_PREF);
+
+ if (matchOSLocale) {
+ // If he has, we'll pick the locale from the system
+ if (OSPreferences::GetInstance()->GetSystemLocales(aRetVal)) {
+ // If we succeeded, return.
+ return true;
+ }
+ }
+
+ // Otherwise, we'll try to get the requested locale from the prefs.
+
+ // In some cases, mainly on Fennec and on Linux version,
+ // `general.useragent.locale` is a special 'localized' value, like:
+ // "chrome://global/locale/intl.properties"
+ if (!NS_SUCCEEDED(Preferences::GetLocalizedCString(SELECTED_LOCALE_PREF, &locale))) {
+ // If not, we can attempt to retrieve it as a simple string value.
+ //rv = prefs->GetCharPref(SELECTED_LOCALE_PREF, getter_Copies(locale));
+ if (!NS_SUCCEEDED(Preferences::GetCString(SELECTED_LOCALE_PREF, &locale))) {
+ return false;
+ }
+ }
+
+ // At the moment we just take a single locale, but in the future
+ // we'll want to allow user to specify a list of requested locales.
+ aRetVal.AppendElement(locale);
+ return true;
+}
+
void
LocaleService::Refresh()
{
nsTArray<nsCString> newLocales;
ReadAppLocales(newLocales);
if (mAppLocales != newLocales) {
mAppLocales = Move(newLocales);
@@ -240,33 +298,56 @@ LocaleService::NegotiateLanguages(const
} else if (!aDefaultLocale.IsEmpty() && !aRetVal.Contains(aDefaultLocale)) {
// If it's not a Lookup strategy, add the default locale only if it's
// set and it's not in the results already.
aRetVal.AppendElement(aDefaultLocale);
}
return true;
}
+NS_IMETHODIMP
+LocaleService::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ // At the moment the only thing we're observing are settings indicating
+ // user requested locales.
+ NS_ConvertUTF16toUTF8 pref(aData);
+ if (pref.EqualsLiteral(MATCH_OS_LOCALE_PREF) || pref.EqualsLiteral(SELECTED_LOCALE_PREF)) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
+ }
+ }
+ return NS_OK;
+}
/**
* mozILocaleService methods
*/
+
+static char**
+CreateOutArray(const nsTArray<nsCString>& aArray)
+{
+ uint32_t n = aArray.Length();
+ char** result = static_cast<char**>(moz_xmalloc(n * sizeof(char*)));
+ for (uint32_t i = 0; i < n; i++) {
+ result[i] = moz_xstrdup(aArray[i].get());
+ }
+ return result;
+}
+
NS_IMETHODIMP
LocaleService::GetAppLocales(uint32_t* aCount, char*** aOutArray)
{
if (mAppLocales.IsEmpty()) {
ReadAppLocales(mAppLocales);
}
*aCount = mAppLocales.Length();
- *aOutArray = static_cast<char**>(moz_xmalloc(*aCount * sizeof(char*)));
-
- for (uint32_t i = 0; i < *aCount; i++) {
- (*aOutArray)[i] = moz_xstrdup(mAppLocales[i].get());
- }
+ *aOutArray = CreateOutArray(mAppLocales);
return NS_OK;
}
NS_IMETHODIMP
LocaleService::GetAppLocale(nsACString& aRetVal)
{
if (mAppLocales.IsEmpty()) {
@@ -465,8 +546,25 @@ LocaleService::Locale::AddLikelySubtags(
mScript = loc.mScript;
mRegion = loc.mRegion;
mVariant = loc.mVariant;
return true;
#else
return false;
#endif
}
+
+NS_IMETHODIMP
+LocaleService::GetRequestedLocales(uint32_t* aCount, char*** aOutArray)
+{
+ AutoTArray<nsCString, 16> requestedLocales;
+ bool res = GetRequestedLocales(requestedLocales);
+
+ if (!res) {
+ NS_ERROR("Couldn't retrieve selected locales from prefs!");
+ return NS_ERROR_FAILURE;
+ }
+
+ *aCount = requestedLocales.Length();
+ *aOutArray = CreateOutArray(requestedLocales);
+
+ return NS_OK;
+}
--- a/intl/locale/LocaleService.h
+++ b/intl/locale/LocaleService.h
@@ -2,16 +2,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_intl_LocaleService_h__
#define mozilla_intl_LocaleService_h__
#include "mozilla/StaticPtr.h"
+#include "nsIObserver.h"
#include "nsString.h"
#include "nsTArray.h"
#include "mozILocaleService.h"
namespace mozilla {
namespace intl {
@@ -32,20 +33,22 @@ namespace intl {
* preferred currency, date/time formatting etc.
*
* An example of a Locale ID is `en-Latn-US-x-hc-h12-ca-gregory`
*
* At the moment we do not support full extension tag system, but we
* try to be specific when naming APIs, so the service is for locales,
* but we negotiate between languages etc.
*/
-class LocaleService : public mozILocaleService
+class LocaleService : public mozILocaleService,
+ public nsIObserver
{
public:
NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
NS_DECL_MOZILOCALESERVICE
/**
* List of available language negotiation strategies.
*
* See the mozILocaleService.idl for detailed description of the
* strategies.
*/
@@ -87,16 +90,36 @@ public:
* nsTArray<nsCString> appLocales;
* LocaleService::GetInstance()->GetAppLocales(appLocales);
*
* (See mozILocaleService.idl for a JS-callable version of this.)
*/
void GetAppLocales(nsTArray<nsCString>& aRetVal);
/**
+ * Returns a list of locales that the user requested the app to be
+ * localized to.
+ *
+ * The result is a sorted list of valid locale IDs and it should be
+ * used as a requestedLocales input list for languages negotiation.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ *
+ * Usage:
+ * nsTArray<nsCString> appLocales;
+ * LocaleService::GetInstance()->GetRequestedLocales(appLocales);
+ *
+ * Returns a boolean indicating if the attempt to retrieve prefs
+ * was successful.
+ *
+ * (See mozILocaleService.idl for a JS-callable version of this.)
+ */
+ bool GetRequestedLocales(nsTArray<nsCString>& aRetVal);
+
+ /**
* Triggers a refresh of the language negotiation process.
*
* If the result differs from the previous list, it will additionally
* trigger a global event "intl:app-locales-changed".
*/
void Refresh();
/**
@@ -164,17 +187,17 @@ private:
nsCString mVariant;
};
void FilterMatches(const nsTArray<nsCString>& aRequested,
const nsTArray<nsCString>& aAvailable,
LangNegStrategy aStrategy,
nsTArray<nsCString>& aRetVal);
- virtual ~LocaleService() {};
+ virtual ~LocaleService();
nsTArray<nsCString> mAppLocales;
static StaticRefPtr<LocaleService> sInstance;
};
} // intl
} // namespace mozilla
--- a/intl/locale/mozILocaleService.idl
+++ b/intl/locale/mozILocaleService.idl
@@ -103,9 +103,21 @@ interface mozILocaleService : nsISupport
* Where possible, getAppLocales() should be preferred over this API and
* all callsites should handle some form of "best effort" language
* negotiation to respect user preferences in case the use case does
* not have data for the first locale in the list.
*
* Example: "zh-Hans-HK"
*/
ACString getAppLocale();
+
+ /**
+ * Returns a list of locales that the user requested the app to be
+ * localized to.
+ *
+ * The result is an ordered list of locale IDs which should be
+ * used as a requestedLocales input list for language negotiation.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ void getRequestedLocales([optional] out unsigned long aCount,
+ [retval, array, size_is(aCount)] out string aLocales);
};
--- a/intl/locale/tests/gtest/TestLocaleService.cpp
+++ b/intl/locale/tests/gtest/TestLocaleService.cpp
@@ -35,16 +35,24 @@ TEST(Intl_Locale_LocaleService, GetAppLo
TEST(Intl_Locale_LocaleService, GetAppLocales_lastIsEnUS) {
nsTArray<nsCString> appLocales;
LocaleService::GetInstance()->GetAppLocales(appLocales);
int32_t len = appLocales.Length();
ASSERT_TRUE(appLocales[len - 1].EqualsLiteral("en-US"));
}
+TEST(Intl_Locale_LocaleService, GetRequestedLocales) {
+ nsTArray<nsCString> reqLocales;
+ LocaleService::GetInstance()->GetRequestedLocales(reqLocales);
+
+ int32_t len = reqLocales.Length();
+ ASSERT_TRUE(len > 0);
+}
+
TEST(Intl_Locale_LocaleService, GetAppLocale) {
nsTArray<nsCString> appLocales;
LocaleService::GetInstance()->GetAppLocales(appLocales);
nsAutoCString locale;
LocaleService::GetInstance()->GetAppLocale(locale);
ASSERT_TRUE(appLocales[0] == locale);
--- a/intl/locale/tests/unit/test_localeService.js
+++ b/intl/locale/tests/unit/test_localeService.js
@@ -1,27 +1,131 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
+const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].
+ getService(Ci.mozIOSPreferences);
+
/**
- * Make sure the locale service can be instantiated,
- * and returns something plausible for getAppLocale and getAppLocales.
+ * Make sure the locale service can be instantiated.
*/
-
function run_test()
{
- var localeService =
+ const localeService =
Components.classes["@mozilla.org/intl/localeservice;1"]
.getService(Components.interfaces.mozILocaleService);
- var appLocale = localeService.getAppLocale();
+ run_next_test();
+}
+
+add_test(function test_getAppLocales() {
+ const localeService =
+ Components.classes["@mozilla.org/intl/localeservice;1"]
+ .getService(Components.interfaces.mozILocaleService);
+
+ const appLocale = localeService.getAppLocale();
do_check_true(appLocale != "", "appLocale is non-empty");
- var appLocales = localeService.getAppLocales();
+ const appLocales = localeService.getAppLocales();
do_check_true(Array.isArray(appLocales), "appLocales returns an array");
do_check_true(appLocale == appLocales[0], "appLocale matches first entry in appLocales");
- var enUScount = 0;
- appLocales.forEach(function(loc) { if (loc == "en-US") { enUScount++; } });
- do_check_true(enUScount == 1, "en-US is present exactly one time");
-}
+ const enUSLocales = appLocales.filter(loc => loc === "en-US");
+ do_check_true(enUSLocales.length == 1, "en-US is present exactly one time");
+
+ run_next_test();
+});
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed";
+
+add_test(function test_getRequestedLocales() {
+ const localeService =
+ Components.classes["@mozilla.org/intl/localeservice;1"]
+ .getService(Components.interfaces.mozILocaleService);
+
+ const requestedLocales = localeService.getRequestedLocales();
+ do_check_true(Array.isArray(requestedLocales), "requestedLocales returns an array");
+
+ run_next_test();
+});
+
+/**
+ * In this test we verify that after we set an observer on the LocaleService
+ * event for requested locales change, it will be fired when the
+ * pref for matchOS is set to true.
+ *
+ * Then, we test that when the matchOS is set to true, we will retrieve
+ * OS locale from getRequestedLocales.
+ */
+add_test(function test_getRequestedLocales_matchOS() {
+ do_test_pending();
+
+ const localeService =
+ Components.classes["@mozilla.org/intl/localeservice;1"]
+ .getService(Components.interfaces.mozILocaleService);
+
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "ar-IR");
+
+ const observer = {
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case REQ_LOC_CHANGE_EVENT:
+ const reqLocs = localeService.getRequestedLocales();
+ do_check_true(reqLocs[0] === osPrefs.systemLocale);
+ Services.obs.removeObserver(observer, REQ_LOC_CHANGE_EVENT);
+ do_test_finished();
+ }
+ }
+ };
+
+ Services.obs.addObserver(observer, REQ_LOC_CHANGE_EVENT, false);
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, true);
+
+ run_next_test();
+});
+
+/**
+ * In this test we verify that after we set an observer on the LocaleService
+ * event for requested locales change, it will be fired when the
+ * pref for browser UI locale changes.
+ */
+add_test(function test_getRequestedLocales_matchOS() {
+ do_test_pending();
+
+ const localeService =
+ Components.classes["@mozilla.org/intl/localeservice;1"]
+ .getService(Components.interfaces.mozILocaleService);
+
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "ar-IR");
+
+ const observer = {
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case REQ_LOC_CHANGE_EVENT:
+ const reqLocs = localeService.getRequestedLocales();
+ do_check_true(reqLocs[0] === "sr-RU");
+ Services.obs.removeObserver(observer, REQ_LOC_CHANGE_EVENT);
+ do_test_finished();
+ }
+ }
+ };
+
+ Services.obs.addObserver(observer, REQ_LOC_CHANGE_EVENT, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "sr-RU");
+
+ run_next_test();
+});
+
+do_register_cleanup(() => {
+ Services.prefs.clearUserPref(PREF_SELECTED_LOCALE);
+ Services.prefs.clearUserPref(PREF_MATCH_OS_LOCALE);
+});