Bug 1337694 - Add language negotiation heuristics to LocaleService. r=jfkthame

MozReview-Commit-ID: 1WjJiKgyaWA

--HG--
extra : rebase_source : 7baac37cf78599605ed274d7165ad8746626828c
This commit is contained in:
Zibi Braniecki 2017-02-24 17:23:39 -08:00
parent b0f3064b89
commit 76830b8830
7 changed files with 704 additions and 6 deletions

View File

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

View File

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

View File

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

View 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"));
}

View File

@ -6,6 +6,7 @@
UNIFIED_SOURCES += [
'TestLocaleService.cpp',
'TestLocaleServiceNegotiate.cpp',
]
if CONFIG['ENABLE_INTL_API']:

View 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`);
}
}
}
}

View File

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