mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
Bug 1337694 - Add language negotiation heuristics to LocaleService. r=jfkthame
MozReview-Commit-ID: 1WjJiKgyaWA --HG-- extra : rebase_source : 7baac37cf78599605ed274d7165ad8746626828c
This commit is contained in:
parent
b0f3064b89
commit
76830b8830
@ -5,11 +5,16 @@
|
||||
|
||||
#include "LocaleService.h"
|
||||
|
||||
#include <algorithm> // find_if()
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "nsIObserverService.h"
|
||||
#include "nsIToolkitChromeRegistry.h"
|
||||
|
||||
#ifdef ENABLE_INTL_API
|
||||
#include "unicode/uloc.h"
|
||||
#endif
|
||||
|
||||
using namespace mozilla::intl;
|
||||
|
||||
NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService)
|
||||
@ -77,6 +82,170 @@ LocaleService::Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// After trying each step of the negotiation algorithm for each requested locale,
|
||||
// if a match was found we use this macro to decide whether to return immediately,
|
||||
// skip to the next requested locale, or continue searching for additional matches,
|
||||
// according to the desired negotiation strategy.
|
||||
#define HANDLE_STRATEGY \
|
||||
switch (aStrategy) { \
|
||||
case LangNegStrategy::Lookup: \
|
||||
return; \
|
||||
case LangNegStrategy::Matching: \
|
||||
continue; \
|
||||
case LangNegStrategy::Filtering: \
|
||||
break; \
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the raw algorithm for language negotiation based roughly
|
||||
* on RFC4647 language filtering, with changes from LDML language matching.
|
||||
*
|
||||
* The exact algorithm is custom, and consist of 5 level strategy:
|
||||
*
|
||||
* 1) Attempt to find an exact match for each requested locale in available
|
||||
* locales.
|
||||
* Example: ['en-US'] * ['en-US'] = ['en-US']
|
||||
*
|
||||
* 2) Attempt to match a requested locale to an available locale treated
|
||||
* as a locale range.
|
||||
* Example: ['en-US'] * ['en'] = ['en']
|
||||
* ^^
|
||||
* |-- becomes 'en-*-*-*'
|
||||
*
|
||||
* 3) Attempt to use the maximized version of the requested locale, to
|
||||
* find the best match in available locales.
|
||||
* Example: ['en'] * ['en-GB', 'en-US'] = ['en-US']
|
||||
* ^^
|
||||
* |-- ICU likelySubtags expands it to 'en-Latn-US'
|
||||
*
|
||||
* 4) Attempt to look up for a different variant of the same locale.
|
||||
* Example: ['ja-JP-win'] * ['ja-JP-mac'] = ['ja-JP-mac']
|
||||
* ^^^^^^^^^
|
||||
* |----------- replace variant with range: 'ja-JP-*'
|
||||
*
|
||||
* 5) Attempt to look up for a different region of the same locale.
|
||||
* Example: ['en-GB'] * ['en-AU'] = ['en-AU']
|
||||
* ^^^^^
|
||||
* |----- replace region with range: 'en-*'
|
||||
*
|
||||
* It uses one of the strategies described in LocaleService.h.
|
||||
*/
|
||||
void
|
||||
LocaleService::FilterMatches(const nsTArray<nsCString>& aRequested,
|
||||
const nsTArray<nsCString>& aAvailable,
|
||||
LangNegStrategy aStrategy,
|
||||
nsTArray<nsCString>& aRetVal)
|
||||
{
|
||||
// Local copy of the list of available locales, in Locale form for flexible
|
||||
// matching. We will remove entries from this list as they get appended to
|
||||
// aRetVal, so that no available locale will be found more than once.
|
||||
AutoTArray<Locale, 100> availLocales;
|
||||
for (auto& avail : aAvailable) {
|
||||
availLocales.AppendElement(Locale(avail, true));
|
||||
}
|
||||
|
||||
// Helper to erase an entry from availLocales once we have copied it to
|
||||
// the result list. Returns an iterator pointing to the entry that was
|
||||
// immediately after the one that was erased (or availLocales.end() if
|
||||
// the target was the last in the array).
|
||||
auto eraseFromAvail = [&](nsTArray<Locale>::iterator aIter) {
|
||||
nsTArray<Locale>::size_type index = aIter - availLocales.begin();
|
||||
availLocales.RemoveElementAt(index);
|
||||
return availLocales.begin() + index;
|
||||
};
|
||||
|
||||
for (auto& requested : aRequested) {
|
||||
|
||||
// 1) Try to find a simple (case-insensitive) string match for the request.
|
||||
auto matchesExactly = [&](const Locale& aLoc) {
|
||||
return requested.Equals(aLoc.AsString(),
|
||||
nsCaseInsensitiveCStringComparator());
|
||||
};
|
||||
auto match = std::find_if(availLocales.begin(), availLocales.end(),
|
||||
matchesExactly);
|
||||
if (match != availLocales.end()) {
|
||||
aRetVal.AppendElement(match->AsString());
|
||||
eraseFromAvail(match);
|
||||
}
|
||||
|
||||
if (!aRetVal.IsEmpty()) {
|
||||
HANDLE_STRATEGY;
|
||||
}
|
||||
|
||||
// 2) Try to match against the available locales treated as ranges.
|
||||
auto findRangeMatches = [&](const Locale& aReq) {
|
||||
auto matchesRange = [&](const Locale& aLoc) {
|
||||
return aReq.Matches(aLoc);
|
||||
};
|
||||
bool foundMatch = false;
|
||||
auto match = availLocales.begin();
|
||||
while ((match = std::find_if(match, availLocales.end(),
|
||||
matchesRange)) != availLocales.end()) {
|
||||
aRetVal.AppendElement(match->AsString());
|
||||
match = eraseFromAvail(match);
|
||||
foundMatch = true;
|
||||
if (aStrategy != LangNegStrategy::Filtering) {
|
||||
return true; // we only want the first match
|
||||
}
|
||||
}
|
||||
return foundMatch;
|
||||
};
|
||||
|
||||
Locale requestedLocale = Locale(requested, false);
|
||||
if (findRangeMatches(requestedLocale)) {
|
||||
HANDLE_STRATEGY;
|
||||
}
|
||||
|
||||
// 3) Try to match against a maximized version of the requested locale
|
||||
if (requestedLocale.AddLikelySubtags()) {
|
||||
if (findRangeMatches(requestedLocale)) {
|
||||
HANDLE_STRATEGY;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Try to match against a variant as a range
|
||||
requestedLocale.SetVariantRange();
|
||||
if (findRangeMatches(requestedLocale)) {
|
||||
HANDLE_STRATEGY;
|
||||
}
|
||||
|
||||
// 5) Try to match against a region as a range
|
||||
requestedLocale.SetRegionRange();
|
||||
if (findRangeMatches(requestedLocale)) {
|
||||
HANDLE_STRATEGY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
LocaleService::NegotiateLanguages(const nsTArray<nsCString>& aRequested,
|
||||
const nsTArray<nsCString>& aAvailable,
|
||||
const nsACString& aDefaultLocale,
|
||||
LangNegStrategy aStrategy,
|
||||
nsTArray<nsCString>& aRetVal)
|
||||
{
|
||||
// If the strategy is Lookup, we require the defaultLocale to be set.
|
||||
if (aStrategy == LangNegStrategy::Lookup && aDefaultLocale.IsEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterMatches(aRequested, aAvailable, aStrategy, aRetVal);
|
||||
|
||||
if (aStrategy == LangNegStrategy::Lookup) {
|
||||
if (aRetVal.Length() == 0) {
|
||||
// If the strategy is Lookup and Filtering returned no matches, use
|
||||
// the default locale.
|
||||
aRetVal.AppendElement(aDefaultLocale);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* mozILocaleService methods
|
||||
*/
|
||||
@ -106,3 +275,198 @@ LocaleService::GetAppLocale(nsACString& aRetVal)
|
||||
aRetVal = mAppLocales[0];
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static LocaleService::LangNegStrategy
|
||||
ToLangNegStrategy(int32_t aStrategy)
|
||||
{
|
||||
switch (aStrategy) {
|
||||
case 1:
|
||||
return LocaleService::LangNegStrategy::Matching;
|
||||
case 2:
|
||||
return LocaleService::LangNegStrategy::Lookup;
|
||||
default:
|
||||
return LocaleService::LangNegStrategy::Filtering;
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
LocaleService::NegotiateLanguages(const char** aRequested,
|
||||
const char** aAvailable,
|
||||
const char* aDefaultLocale,
|
||||
int32_t aStrategy,
|
||||
uint32_t aRequestedCount,
|
||||
uint32_t aAvailableCount,
|
||||
uint32_t* aCount, char*** aRetVal)
|
||||
{
|
||||
if (aStrategy < 0 || aStrategy > 2) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// Check that the given string contains only ASCII characters valid in tags
|
||||
// (i.e. alphanumerics, plus '-' and '_'), and is non-empty.
|
||||
auto validTagChars = [](const char* s) {
|
||||
if (!*s) {
|
||||
return false;
|
||||
}
|
||||
while (*s) {
|
||||
if (isalnum((unsigned char)*s) || *s == '-' || *s == '_' || *s == '*') {
|
||||
s++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
AutoTArray<nsCString, 100> requestedLocales;
|
||||
for (uint32_t i = 0; i < aRequestedCount; i++) {
|
||||
if (!validTagChars(aRequested[i])) {
|
||||
continue;
|
||||
}
|
||||
requestedLocales.AppendElement(aRequested[i]);
|
||||
}
|
||||
|
||||
AutoTArray<nsCString, 100> availableLocales;
|
||||
for (uint32_t i = 0; i < aAvailableCount; i++) {
|
||||
if (!validTagChars(aAvailable[i])) {
|
||||
continue;
|
||||
}
|
||||
availableLocales.AppendElement(aAvailable[i]);
|
||||
}
|
||||
|
||||
nsAutoCString defaultLocale(aDefaultLocale);
|
||||
|
||||
LangNegStrategy strategy = ToLangNegStrategy(aStrategy);
|
||||
|
||||
AutoTArray<nsCString, 100> supportedLocales;
|
||||
bool result = NegotiateLanguages(requestedLocales, availableLocales,
|
||||
defaultLocale, strategy, supportedLocales);
|
||||
|
||||
if (!result) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*aRetVal =
|
||||
static_cast<char**>(moz_xmalloc(sizeof(char*) * supportedLocales.Length()));
|
||||
|
||||
*aCount = 0;
|
||||
for (const auto& supported : supportedLocales) {
|
||||
(*aRetVal)[(*aCount)++] = moz_xstrdup(supported.get());
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
LocaleService::Locale::Locale(const nsCString& aLocale, bool aRange)
|
||||
: mLocaleStr(aLocale)
|
||||
{
|
||||
int32_t partNum = 0;
|
||||
|
||||
nsAutoCString normLocale(aLocale);
|
||||
normLocale.ReplaceChar('_', '-');
|
||||
|
||||
for (const nsCSubstring& part : normLocale.Split('-')) {
|
||||
switch (partNum) {
|
||||
case 0:
|
||||
if (part.EqualsLiteral("*") ||
|
||||
part.Length() == 2 || part.Length() == 3) {
|
||||
mLanguage.Assign(part);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (part.EqualsLiteral("*") || part.Length() == 4) {
|
||||
mScript.Assign(part);
|
||||
break;
|
||||
}
|
||||
|
||||
// fallover to region case
|
||||
partNum++;
|
||||
MOZ_FALLTHROUGH;
|
||||
case 2:
|
||||
if (part.EqualsLiteral("*") || part.Length() == 2) {
|
||||
mRegion.Assign(part);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (part.EqualsLiteral("*") || part.Length() == 3) {
|
||||
mVariant.Assign(part);
|
||||
}
|
||||
break;
|
||||
}
|
||||
partNum++;
|
||||
}
|
||||
|
||||
if (aRange) {
|
||||
if (mLanguage.IsEmpty()) {
|
||||
mLanguage.Assign(NS_LITERAL_CSTRING("*"));
|
||||
}
|
||||
if (mScript.IsEmpty()) {
|
||||
mScript.Assign(NS_LITERAL_CSTRING("*"));
|
||||
}
|
||||
if (mRegion.IsEmpty()) {
|
||||
mRegion.Assign(NS_LITERAL_CSTRING("*"));
|
||||
}
|
||||
if (mVariant.IsEmpty()) {
|
||||
mVariant.Assign(NS_LITERAL_CSTRING("*"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
LocaleService::Locale::Matches(const LocaleService::Locale& aLocale) const
|
||||
{
|
||||
auto subtagMatches = [](const nsCString& aSubtag1,
|
||||
const nsCString& aSubtag2) {
|
||||
return aSubtag1.EqualsLiteral("*") ||
|
||||
aSubtag2.EqualsLiteral("*") ||
|
||||
aSubtag1.Equals(aSubtag2, nsCaseInsensitiveCStringComparator());
|
||||
};
|
||||
|
||||
return subtagMatches(mLanguage, aLocale.mLanguage) &&
|
||||
subtagMatches(mScript, aLocale.mScript) &&
|
||||
subtagMatches(mRegion, aLocale.mRegion) &&
|
||||
subtagMatches(mVariant, aLocale.mVariant);
|
||||
}
|
||||
|
||||
void
|
||||
LocaleService::Locale::SetVariantRange()
|
||||
{
|
||||
mVariant.AssignLiteral("*");
|
||||
}
|
||||
|
||||
void
|
||||
LocaleService::Locale::SetRegionRange()
|
||||
{
|
||||
mRegion.AssignLiteral("*");
|
||||
}
|
||||
|
||||
bool
|
||||
LocaleService::Locale::AddLikelySubtags()
|
||||
{
|
||||
#ifdef ENABLE_INTL_API
|
||||
const int32_t kLocaleMax = 160;
|
||||
char maxLocale[kLocaleMax];
|
||||
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
uloc_addLikelySubtags(mLocaleStr.get(), maxLocale, kLocaleMax, &status);
|
||||
|
||||
if (U_FAILURE(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsDependentCString maxLocStr(maxLocale);
|
||||
Locale loc = Locale(maxLocStr, false);
|
||||
|
||||
if (loc == *this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mLanguage = loc.mLanguage;
|
||||
mScript = loc.mScript;
|
||||
mRegion = loc.mRegion;
|
||||
mVariant = loc.mVariant;
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
@ -21,6 +21,21 @@ namespace intl {
|
||||
* It's intended to be the core place for collecting available and
|
||||
* requested languages and negotiating them to produce a fallback
|
||||
* chain of locales for the application.
|
||||
*
|
||||
* The terms `Locale ID` and `Language ID` are used slightly differently
|
||||
* by different organizations. Mozilla uses the term `Language ID` to describe
|
||||
* a string that contains information about the language itself, script,
|
||||
* region and variant. For example "en-Latn-US-mac" is a correct Language ID.
|
||||
*
|
||||
* Locale ID contains a Language ID plus a number of extension tags that
|
||||
* contain information that go beyond language inforamation such as
|
||||
* 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
|
||||
{
|
||||
@ -28,6 +43,18 @@ public:
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_MOZILOCALESERVICE
|
||||
|
||||
/**
|
||||
* List of available language negotiation strategies.
|
||||
*
|
||||
* See the mozILocaleService.idl for detailed description of the
|
||||
* strategies.
|
||||
*/
|
||||
enum class LangNegStrategy {
|
||||
Filtering,
|
||||
Matching,
|
||||
Lookup
|
||||
};
|
||||
|
||||
/**
|
||||
* Create (if necessary) and return a raw pointer to the singleton instance.
|
||||
* Use this accessor in C++ code that just wants to call a method on the
|
||||
@ -49,7 +76,7 @@ public:
|
||||
/**
|
||||
* Returns a list of locales that the application should be localized to.
|
||||
*
|
||||
* The result is a sorted list of valid locale IDs and it should be
|
||||
* The result is a ordered list of valid locale IDs and it should be
|
||||
* used for all APIs that accept list of locales, like ECMA402 and L10n APIs.
|
||||
*
|
||||
* This API always returns at least one locale.
|
||||
@ -72,15 +99,82 @@ public:
|
||||
*/
|
||||
void Refresh();
|
||||
|
||||
protected:
|
||||
nsTArray<nsCString> mAppLocales;
|
||||
/**
|
||||
* Negotiates the best locales out of an ordered list of requested locales and
|
||||
* a list of available locales.
|
||||
*
|
||||
* Internally it uses the following naming scheme:
|
||||
*
|
||||
* Requested - locales requested by the user
|
||||
* Available - locales for which the data is available
|
||||
* Supported - locales negotiated by the algorithm
|
||||
*
|
||||
* Additionally, if defaultLocale is provided, it adds it to the end of the
|
||||
* result list as a "last resort" locale.
|
||||
*
|
||||
* Strategy is one of the three strategies described at the top of this file.
|
||||
*
|
||||
* The result list is ordered according to the order of the requested locales.
|
||||
*
|
||||
* (See mozILocaleService.idl for a JS-callable version of this.)
|
||||
*/
|
||||
bool NegotiateLanguages(const nsTArray<nsCString>& aRequested,
|
||||
const nsTArray<nsCString>& aAvailable,
|
||||
const nsACString& aDefaultLocale,
|
||||
LangNegStrategy aLangNegStrategy,
|
||||
nsTArray<nsCString>& aRetVal);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Locale object, a BCP47-style tag decomposed into subtags for
|
||||
* matching purposes.
|
||||
*
|
||||
* If constructed with aRange = true, any missing subtags will be
|
||||
* set to "*".
|
||||
*/
|
||||
class Locale
|
||||
{
|
||||
public:
|
||||
Locale(const nsCString& aLocale, bool aRange);
|
||||
|
||||
bool Matches(const Locale& aLocale) const;
|
||||
|
||||
void SetVariantRange();
|
||||
void SetRegionRange();
|
||||
|
||||
bool AddLikelySubtags(); // returns false if nothing changed
|
||||
|
||||
const nsCString& AsString() const {
|
||||
return mLocaleStr;
|
||||
}
|
||||
|
||||
bool operator== (const Locale& aOther) {
|
||||
const auto& cmp = nsCaseInsensitiveCStringComparator();
|
||||
return mLanguage.Equals(aOther.mLanguage, cmp) &&
|
||||
mScript.Equals(aOther.mScript, cmp) &&
|
||||
mRegion.Equals(aOther.mRegion, cmp) &&
|
||||
mVariant.Equals(aOther.mVariant, cmp);
|
||||
}
|
||||
|
||||
private:
|
||||
const nsCString& mLocaleStr;
|
||||
nsCString mLanguage;
|
||||
nsCString mScript;
|
||||
nsCString mRegion;
|
||||
nsCString mVariant;
|
||||
};
|
||||
|
||||
void FilterMatches(const nsTArray<nsCString>& aRequested,
|
||||
const nsTArray<nsCString>& aAvailable,
|
||||
LangNegStrategy aStrategy,
|
||||
nsTArray<nsCString>& aRetVal);
|
||||
|
||||
virtual ~LocaleService() {};
|
||||
|
||||
nsTArray<nsCString> mAppLocales;
|
||||
|
||||
static StaticRefPtr<LocaleService> sInstance;
|
||||
};
|
||||
|
||||
} // intl
|
||||
} // namespace mozilla
|
||||
|
||||
|
@ -16,10 +16,45 @@
|
||||
[scriptable, uuid(C27F8983-B48B-4D1A-92D7-FEB8106F212D)]
|
||||
interface mozILocaleService : nsISupports
|
||||
{
|
||||
/**
|
||||
* List of language negotiation strategies to use.
|
||||
* For an example list of requested and available locales:
|
||||
*
|
||||
* Requested: ['es-MX', 'fr-FR']
|
||||
* Available: ['fr', 'fr-CA', 'es', 'es-MX', 'it']
|
||||
* DefaultLocale: ['en-US']
|
||||
*
|
||||
* each of those strategies will build a different result:
|
||||
*
|
||||
*
|
||||
* filtering (default) -
|
||||
* Matches as many of the available locales as possible.
|
||||
*
|
||||
* Result:
|
||||
* Supported: ['es-MX', 'es', 'fr', 'fr-CA', 'en-US']
|
||||
*
|
||||
* matching -
|
||||
* Matches the best match from the available locales for every requested
|
||||
* locale.
|
||||
*
|
||||
* Result:
|
||||
* Supported: ['es-MX', 'fr', 'en-US']
|
||||
*
|
||||
* lookup -
|
||||
* Matches a single best locale. This strategy always returns a list
|
||||
* of the length 1 and requires a defaultLocale to be set.
|
||||
*
|
||||
* Result:
|
||||
* Supported: ['es-MX']
|
||||
*/
|
||||
const long langNegStrategyFiltering = 0;
|
||||
const long langNegStrategyMatching = 1;
|
||||
const long langNegStrategyLookup = 2;
|
||||
|
||||
/**
|
||||
* Returns a list of locales that the application should be localized to.
|
||||
*
|
||||
* The result is a sorted list of valid locale IDs and it should be
|
||||
* The result is a ordered list of valid locale IDs and it should be
|
||||
* used for all APIs that accept list of locales, like ECMA402 and L10n APIs.
|
||||
*
|
||||
* This API always returns at least one locale.
|
||||
@ -31,6 +66,34 @@ interface mozILocaleService : nsISupports
|
||||
void getAppLocales([optional] out unsigned long aCount,
|
||||
[retval, array, size_is(aCount)] out string aLocales);
|
||||
|
||||
/**
|
||||
* Negotiates the best locales out of a ordered list of requested locales and
|
||||
* a list of available locales.
|
||||
*
|
||||
* Internally it uses the following naming scheme:
|
||||
*
|
||||
* Requested - locales requested by the user
|
||||
* Available - locales for which the data is available
|
||||
* Supported - locales negotiated by the algorithm
|
||||
*
|
||||
* Additionally, if defaultLocale is provided, it adds it to the end of the
|
||||
* result list as a "last resort" locale.
|
||||
*
|
||||
* Strategy is one of the three strategies described at the top of this file.
|
||||
*
|
||||
* The result list is ordered according to the order of the requested locales.
|
||||
*
|
||||
* (See LocaleService.h for a more C++-friendly version of this.)
|
||||
*/
|
||||
void negotiateLanguages([array, size_is(aRequestedCount)] in string aRequested,
|
||||
[array, size_is(aAvailableCount)] in string aAvailable,
|
||||
[optional] in string aDefaultLocale,
|
||||
[optional] in long langNegStrategy,
|
||||
[optional] in unsigned long aRequestedCount,
|
||||
[optional] in unsigned long aAvailableCount,
|
||||
[optional] out unsigned long aCount,
|
||||
[retval, array, size_is(aCount)] out string aLocales);
|
||||
|
||||
/**
|
||||
* Returns the best locale that the application should be localized to.
|
||||
*
|
||||
|
32
intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp
Normal file
32
intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "mozilla/intl/LocaleService.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "nsIToolkitChromeRegistry.h"
|
||||
|
||||
using namespace mozilla::intl;
|
||||
|
||||
TEST(Intl_Locale_LocaleService, Negotiate) {
|
||||
nsTArray<nsCString> requestedLocales;
|
||||
nsTArray<nsCString> availableLocales;
|
||||
nsTArray<nsCString> supportedLocales;
|
||||
nsAutoCString defaultLocale("en-US");
|
||||
LocaleService::LangNegStrategy strategy =
|
||||
LocaleService::LangNegStrategy::Filtering;
|
||||
|
||||
requestedLocales.AppendElement(NS_LITERAL_CSTRING("sr"));
|
||||
|
||||
availableLocales.AppendElement(NS_LITERAL_CSTRING("sr-Cyrl"));
|
||||
availableLocales.AppendElement(NS_LITERAL_CSTRING("sr-Latn"));
|
||||
|
||||
LocaleService::GetInstance()->NegotiateLanguages(
|
||||
requestedLocales, availableLocales, defaultLocale, strategy, supportedLocales);
|
||||
|
||||
ASSERT_TRUE(supportedLocales.Length() == 2);
|
||||
ASSERT_TRUE(supportedLocales[0].Equals("sr-Cyrl"));
|
||||
ASSERT_TRUE(supportedLocales[1].Equals("en-US"));
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'TestLocaleService.cpp',
|
||||
'TestLocaleServiceNegotiate.cpp',
|
||||
]
|
||||
|
||||
if CONFIG['ENABLE_INTL_API']:
|
||||
|
143
intl/locale/tests/unit/test_localeService_negotiateLanguages.js
Normal file
143
intl/locale/tests/unit/test_localeService_negotiateLanguages.js
Normal file
@ -0,0 +1,143 @@
|
||||
/* 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 localeService =
|
||||
Components.classes["@mozilla.org/intl/localeservice;1"]
|
||||
.getService(Components.interfaces.mozILocaleService);
|
||||
|
||||
const data = {
|
||||
"filtering": {
|
||||
"exact match": [
|
||||
[["en"], ["en"], ["en"]],
|
||||
[["en-US"], ["en-US"], ["en-US"]],
|
||||
[["en-Latn-US"], ["en-Latn-US"], ["en-Latn-US"]],
|
||||
[["en-Latn-US-mac"], ["en-Latn-US-mac"], ["en-Latn-US-mac"]],
|
||||
[["fr-FR"], ["de", "it", "fr-FR"], ["fr-FR"]],
|
||||
[["fr", "pl", "de-DE"], ["pl", "en-US", "de-DE"], ["pl", "de-DE"]],
|
||||
],
|
||||
"available as range": [
|
||||
[["en-US"], ["en"], ["en"]],
|
||||
[["en-Latn-US"], ["en-US"], ["en-US"]],
|
||||
[["en-US-mac"], ["en-US"], ["en-US"]],
|
||||
[["fr-CA", "de-DE"], ["fr", "it", "de"], ["fr", "de"]],
|
||||
[["ja-JP-mac"], ["ja"], ["ja"]],
|
||||
[["en-Latn-GB", "en-Latn-IN"], ["en-IN", "en-GB"], ["en-GB", "en-IN"]],
|
||||
],
|
||||
"should match on likely subtag": [
|
||||
[["en"], ["en-GB", "de", "en-US"], ["en-US", "en-GB"]],
|
||||
[["en"], ["en-Latn-GB", "de", "en-Latn-US"], ["en-Latn-US", "en-Latn-GB"]],
|
||||
[["fr"], ["fr-CA", "fr-FR"], ["fr-FR", "fr-CA"]],
|
||||
[["az-IR"], ["az-Latn", "az-Arab"], ["az-Arab"]],
|
||||
[["sr-RU"], ["sr-Cyrl", "sr-Latn"], ["sr-Latn"]],
|
||||
[["sr"], ["sr-Latn", "sr-Cyrl"], ["sr-Cyrl"]],
|
||||
[["zh-GB"], ["zh-Hans", "zh-Hant"], ["zh-Hant"]],
|
||||
[["sr", "ru"], ["sr-Latn", "ru"], ["ru"]],
|
||||
[["sr-RU"], ["sr-Latn-RO", "sr-Cyrl"], ["sr-Latn-RO"]],
|
||||
],
|
||||
"should match on a requested locale as a range": [
|
||||
[["en-*-US"], ["en-US"], ["en-US"]],
|
||||
[["en-Latn-US-*"], ["en-Latn-US"], ["en-Latn-US"]],
|
||||
[["en-*-US-*"], ["en-US"], ["en-US"]],
|
||||
],
|
||||
"should match cross-region": [
|
||||
[["en"], ["en-US"], ["en-US"]],
|
||||
[["en-US"], ["en-GB"], ["en-GB"]],
|
||||
[["en-Latn-US"], ["en-Latn-GB"], ["en-Latn-GB"]],
|
||||
// This is a cross-region check, because the requested Locale
|
||||
// is really lang: en, script: *, region: undefined
|
||||
[["en-*"], ["en-US"], ["en-US"]],
|
||||
],
|
||||
"should match cross-variant": [
|
||||
[["en-US-mac"], ["en-US-win"], ["en-US-win"]],
|
||||
],
|
||||
"should prioritize properly": [
|
||||
// exact match first
|
||||
[["en-US"], ["en-US-mac", "en", "en-US"], ["en-US", "en", "en-US-mac"]],
|
||||
// available as range second
|
||||
[["en-Latn-US"], ["en-GB", "en-US"], ["en-US", "en-GB"]],
|
||||
// likely subtags third
|
||||
[["en"], ["en-Cyrl-US", "en-Latn-US"], ["en-Latn-US"]],
|
||||
// variant range fourth
|
||||
[["en-US-mac"], ["en-US-win", "en-GB-mac"], ["en-US-win", "en-GB-mac"]],
|
||||
// regional range fifth
|
||||
[["en-US-mac"], ["en-GB-win"], ["en-GB-win"]],
|
||||
],
|
||||
"should prioritize properly (extra tests)": [
|
||||
[["en-US"], ["en-GB", "en"], ["en", "en-GB"]],
|
||||
],
|
||||
"should handle default locale properly": [
|
||||
[["fr"], ["de", "it"], []],
|
||||
[["fr"], ["de", "it"], "en-US", ["en-US"]],
|
||||
[["fr"], ["de", "en-US"], "en-US", ["en-US"]],
|
||||
[["fr", "de-DE"], ["de-DE", "fr-CA"], "en-US", ["fr-CA", "de-DE", "en-US"]],
|
||||
],
|
||||
"should handle all matches on the 1st higher than any on the 2nd": [
|
||||
[["fr-CA-mac", "de-DE"], ["de-DE", "fr-FR-win"], ["fr-FR-win", "de-DE"]],
|
||||
],
|
||||
"should handle cases and underscores": [
|
||||
[["fr_FR"], ["fr-FR"], ["fr-FR"]],
|
||||
[["fr_fr"], ["fr-fr"], ["fr-fr"]],
|
||||
[["fr_Fr"], ["fr-fR"], ["fr-fR"]],
|
||||
[["fr_lAtN_fr"], ["fr-Latn-FR"], ["fr-Latn-FR"]],
|
||||
[["fr_FR"], ["fr_FR"], ["fr_FR"]],
|
||||
[["fr-FR"], ["fr_FR"], ["fr_FR"]],
|
||||
[["fr_Cyrl_FR_mac"], ["fr_Cyrl_fr-mac"], ["fr_Cyrl_fr-mac"]],
|
||||
],
|
||||
"should not crash on invalid input": [
|
||||
[null, ["fr-FR"], []],
|
||||
[undefined, ["fr-FR"], []],
|
||||
[2, ["fr-FR"], []],
|
||||
["fr-FR", ["fr-FR"], []],
|
||||
[["fr-FR"], null, []],
|
||||
[["fr-FR"], undefined, []],
|
||||
[["fr-FR"], 2, []],
|
||||
[["fr-FR"], "fr-FR", []],
|
||||
[["2"], ["ąóżł"], []],
|
||||
[[[]], ["fr-FR"], []],
|
||||
[[[]], [[2]], []],
|
||||
],
|
||||
},
|
||||
"matching": {
|
||||
"should match only one per requested": [
|
||||
[
|
||||
["fr", "en"],
|
||||
["en-US", "fr-FR", "en", "fr"], null,
|
||||
localeService.langNegStrategyMatching, ["fr", "en"]
|
||||
],
|
||||
]
|
||||
},
|
||||
"lookup": {
|
||||
"should match only one": [
|
||||
[
|
||||
["fr-FR", "en"],
|
||||
["en-US", "fr-FR", "en", "fr"], 'en-US',
|
||||
localeService.langNegStrategyLookup, ["fr-FR"]
|
||||
],
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function run_test()
|
||||
{
|
||||
|
||||
const nl = localeService.negotiateLanguages;
|
||||
|
||||
const json = JSON.stringify;
|
||||
for (const strategy in data) {
|
||||
for (const groupName in data[strategy]) {
|
||||
const group = data[strategy][groupName];
|
||||
for (const test of group) {
|
||||
const requested = test[0];
|
||||
const available = test[1];
|
||||
const defaultLocale = test.length > 3 ? test[2] : undefined;
|
||||
const strategy = test.length > 4 ? test[3] : undefined;
|
||||
const supported = test[test.length - 1];
|
||||
|
||||
const result = nl(test[0], test[1], defaultLocale, strategy);
|
||||
deepEqual(result, supported,
|
||||
`\nExpected ${json(requested)} * ${json(available)} = ${json(supported)}.\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ skip-if = toolkit == "android" # bug 1309447
|
||||
[test_pluralForm_english.js]
|
||||
[test_pluralForm_makeGetter.js]
|
||||
|
||||
[test_localeService.js]
|
||||
[test_osPreferences.js]
|
||||
skip-if = toolkit == "android" # bug 1344596
|
||||
[test_localeService.js]
|
||||
[test_localeService_negotiateLanguages.js]
|
||||
|
Loading…
Reference in New Issue
Block a user