From 76830b88309aa7abb34bd4292fbac1b1e2377325 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Fri, 24 Feb 2017 17:23:39 -0800 Subject: [PATCH] Bug 1337694 - Add language negotiation heuristics to LocaleService. r=jfkthame MozReview-Commit-ID: 1WjJiKgyaWA --HG-- extra : rebase_source : 7baac37cf78599605ed274d7165ad8746626828c --- intl/locale/LocaleService.cpp | 364 ++++++++++++++++++ intl/locale/LocaleService.h | 102 ++++- intl/locale/mozILocaleService.idl | 65 +++- .../gtest/TestLocaleServiceNegotiate.cpp | 32 ++ intl/locale/tests/gtest/moz.build | 1 + .../test_localeService_negotiateLanguages.js | 143 +++++++ intl/locale/tests/unit/xpcshell.ini | 3 +- 7 files changed, 704 insertions(+), 6 deletions(-) create mode 100644 intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp create mode 100644 intl/locale/tests/unit/test_localeService_negotiateLanguages.js diff --git a/intl/locale/LocaleService.cpp b/intl/locale/LocaleService.cpp index df7bebac5f35..b8c99ef5e1fb 100644 --- a/intl/locale/LocaleService.cpp +++ b/intl/locale/LocaleService.cpp @@ -5,11 +5,16 @@ #include "LocaleService.h" +#include // 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& aRequested, + const nsTArray& aAvailable, + LangNegStrategy aStrategy, + nsTArray& 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 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::iterator aIter) { + nsTArray::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& aRequested, + const nsTArray& aAvailable, + const nsACString& aDefaultLocale, + LangNegStrategy aStrategy, + nsTArray& 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 requestedLocales; + for (uint32_t i = 0; i < aRequestedCount; i++) { + if (!validTagChars(aRequested[i])) { + continue; + } + requestedLocales.AppendElement(aRequested[i]); + } + + AutoTArray 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 supportedLocales; + bool result = NegotiateLanguages(requestedLocales, availableLocales, + defaultLocale, strategy, supportedLocales); + + if (!result) { + return NS_ERROR_INVALID_ARG; + } + + *aRetVal = + static_cast(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 +} diff --git a/intl/locale/LocaleService.h b/intl/locale/LocaleService.h index 068ae84c09db..c03dc7e21f1f 100644 --- a/intl/locale/LocaleService.h +++ b/intl/locale/LocaleService.h @@ -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 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& aRequested, + const nsTArray& aAvailable, + const nsACString& aDefaultLocale, + LangNegStrategy aLangNegStrategy, + nsTArray& 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& aRequested, + const nsTArray& aAvailable, + LangNegStrategy aStrategy, + nsTArray& aRetVal); + virtual ~LocaleService() {}; + nsTArray mAppLocales; + static StaticRefPtr sInstance; }; - } // intl } // namespace mozilla diff --git a/intl/locale/mozILocaleService.idl b/intl/locale/mozILocaleService.idl index 297ae7c063c1..40a23d363805 100644 --- a/intl/locale/mozILocaleService.idl +++ b/intl/locale/mozILocaleService.idl @@ -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. * diff --git a/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp b/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp new file mode 100644 index 000000000000..b0333b3c88b4 --- /dev/null +++ b/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp @@ -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 requestedLocales; + nsTArray availableLocales; + nsTArray 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")); +} diff --git a/intl/locale/tests/gtest/moz.build b/intl/locale/tests/gtest/moz.build index 69bde4c1d12a..9943ec9ba1d0 100644 --- a/intl/locale/tests/gtest/moz.build +++ b/intl/locale/tests/gtest/moz.build @@ -6,6 +6,7 @@ UNIFIED_SOURCES += [ 'TestLocaleService.cpp', + 'TestLocaleServiceNegotiate.cpp', ] if CONFIG['ENABLE_INTL_API']: diff --git a/intl/locale/tests/unit/test_localeService_negotiateLanguages.js b/intl/locale/tests/unit/test_localeService_negotiateLanguages.js new file mode 100644 index 000000000000..29af067708d1 --- /dev/null +++ b/intl/locale/tests/unit/test_localeService_negotiateLanguages.js @@ -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`); + } + } + } +} diff --git a/intl/locale/tests/unit/xpcshell.ini b/intl/locale/tests/unit/xpcshell.ini index c7e1049ae695..271ccac06d5d 100644 --- a/intl/locale/tests/unit/xpcshell.ini +++ b/intl/locale/tests/unit/xpcshell.ini @@ -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]