summaryrefslogtreecommitdiffstats
path: root/intl/locale
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /intl/locale
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/locale')
-rw-r--r--intl/locale/AppDateTimeFormat.cpp263
-rw-r--r--intl/locale/AppDateTimeFormat.h89
-rw-r--r--intl/locale/LangPackMatcher.sys.mjs395
-rw-r--r--intl/locale/LocaleService.cpp693
-rw-r--r--intl/locale/LocaleService.h232
-rw-r--r--intl/locale/MozLocaleBindings.h25
-rw-r--r--intl/locale/OSPreferences.cpp585
-rw-r--r--intl/locale/OSPreferences.h185
-rw-r--r--intl/locale/PluralForm.sys.mjs311
-rw-r--r--intl/locale/Quotes.cpp93
-rw-r--r--intl/locale/Quotes.h35
-rw-r--r--intl/locale/android/OSPreferences_android.cpp69
-rw-r--r--intl/locale/android/moz.build13
-rw-r--r--intl/locale/cldr-quotes.inc45
-rw-r--r--intl/locale/cldr-quotes.pl108
-rw-r--r--intl/locale/components.conf26
-rw-r--r--intl/locale/encodingsgroups.properties40
-rw-r--r--intl/locale/gtk/OSPreferences_gtk.cpp120
-rw-r--r--intl/locale/gtk/moz.build15
-rw-r--r--intl/locale/language.properties289
-rw-r--r--intl/locale/mac/OSPreferences_mac.cpp158
-rw-r--r--intl/locale/mac/moz.build12
-rw-r--r--intl/locale/moz.build98
-rw-r--r--intl/locale/mozILocaleService.idl185
-rw-r--r--intl/locale/mozIOSPreferences.idl95
-rw-r--r--intl/locale/nsLanguageAtomService.cpp255
-rw-r--r--intl/locale/nsLanguageAtomService.h61
-rw-r--r--intl/locale/nsUConvPropertySearch.cpp40
-rw-r--r--intl/locale/nsUConvPropertySearch.h35
-rw-r--r--intl/locale/props2arrays.py26
-rw-r--r--intl/locale/rust/fluent-langneg-ffi/Cargo.toml13
-rw-r--r--intl/locale/rust/fluent-langneg-ffi/cbindgen.toml17
-rw-r--r--intl/locale/rust/fluent-langneg-ffi/src/lib.rs79
-rw-r--r--intl/locale/rust/oxilangtag-ffi/Cargo.toml10
-rw-r--r--intl/locale/rust/oxilangtag-ffi/cbindgen.toml15
-rw-r--r--intl/locale/rust/oxilangtag-ffi/src/lib.rs126
-rw-r--r--intl/locale/rust/unic-langid-ffi/Cargo.toml11
-rw-r--r--intl/locale/rust/unic-langid-ffi/cbindgen.toml22
-rw-r--r--intl/locale/rust/unic-langid-ffi/src/lib.rs168
-rw-r--r--intl/locale/tests/LangPackMatcherTestUtils.sys.mjs124
-rw-r--r--intl/locale/tests/gtest/TestAppDateTimeFormat.cpp310
-rw-r--r--intl/locale/tests/gtest/TestLocaleService.cpp173
-rw-r--r--intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp53
-rw-r--r--intl/locale/tests/gtest/TestOSPreferences.cpp205
-rw-r--r--intl/locale/tests/gtest/moz.build14
-rw-r--r--intl/locale/tests/sort/us-ascii_base.txt95
-rw-r--r--intl/locale/tests/sort/us-ascii_base_case_res.txt96
-rw-r--r--intl/locale/tests/sort/us-ascii_base_nocase_res.txt96
-rw-r--r--intl/locale/tests/sort/us-ascii_sort.txt78
-rw-r--r--intl/locale/tests/sort/us-ascii_sort_case_res.txt79
-rw-r--r--intl/locale/tests/sort/us-ascii_sort_nocase_res.txt79
-rw-r--r--intl/locale/tests/unit/data/chrome.manifest1
-rw-r--r--intl/locale/tests/unit/data/intl_on_workers_worker.js6
-rw-r--r--intl/locale/tests/unit/test_bug1086527.js22
-rw-r--r--intl/locale/tests/unit/test_bug22310.js65
-rw-r--r--intl/locale/tests/unit/test_intl_on_workers.js29
-rw-r--r--intl/locale/tests/unit/test_langPackMatcher.js287
-rw-r--r--intl/locale/tests/unit/test_localeService.js240
-rw-r--r--intl/locale/tests/unit/test_localeService_negotiateLanguages.js205
-rw-r--r--intl/locale/tests/unit/test_osPreferences.js44
-rw-r--r--intl/locale/tests/unit/test_pluralForm.js390
-rw-r--r--intl/locale/tests/unit/test_pluralForm_english.js31
-rw-r--r--intl/locale/tests/unit/test_pluralForm_makeGetter.js38
-rw-r--r--intl/locale/tests/unit/xpcshell.ini21
-rw-r--r--intl/locale/windows/OSPreferences_win.cpp321
-rw-r--r--intl/locale/windows/moz.build13
66 files changed, 8172 insertions, 0 deletions
diff --git a/intl/locale/AppDateTimeFormat.cpp b/intl/locale/AppDateTimeFormat.cpp
new file mode 100644
index 0000000000..d967d312a4
--- /dev/null
+++ b/intl/locale/AppDateTimeFormat.cpp
@@ -0,0 +1,263 @@
+/* -*- 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 "nsCOMPtr.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+#include "mozilla/intl/DateTimePatternGenerator.h"
+#include "mozilla/intl/FormatBuffer.h"
+#include "mozilla/intl/LocaleService.h"
+#include "OSPreferences.h"
+#include "mozIOSPreferences.h"
+#ifdef DEBUG
+# include "nsThreadManager.h"
+#endif
+
+namespace mozilla::intl {
+
+nsCString* AppDateTimeFormat::sLocale = nullptr;
+nsTHashMap<nsCStringHashKey, UniquePtr<DateTimeFormat>>*
+ AppDateTimeFormat::sFormatCache;
+
+static const int32_t DATETIME_FORMAT_INITIAL_LEN = 127;
+
+/*static*/
+nsresult AppDateTimeFormat::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sLocale) {
+ return NS_OK;
+ }
+
+ sLocale = new nsCString();
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
+ sLocale->Assign(regionalPrefsLocales[0]);
+
+ return NS_OK;
+}
+
+// performs a locale sensitive date formatting operation on the PRTime parameter
+/*static*/
+nsresult AppDateTimeFormat::Format(const DateTimeFormat::StyleBag& aStyle,
+ const PRTime aPrTime,
+ nsAString& aStringOut) {
+ return AppDateTimeFormat::Format(
+ aStyle, (static_cast<double>(aPrTime) / PR_USEC_PER_MSEC), nullptr,
+ aStringOut);
+}
+
+// performs a locale sensitive date formatting operation on the PRExplodedTime
+// parameter
+/*static*/
+nsresult AppDateTimeFormat::Format(const DateTimeFormat::StyleBag& aStyle,
+ const PRExplodedTime* aExplodedTime,
+ nsAString& aStringOut) {
+ return AppDateTimeFormat::Format(
+ aStyle, (PR_ImplodeTime(aExplodedTime) / PR_USEC_PER_MSEC),
+ &(aExplodedTime->tm_params), aStringOut);
+}
+
+// performs a locale sensitive date formatting operation on the PRExplodedTime
+// parameter, using the specified options.
+/*static*/
+nsresult AppDateTimeFormat::Format(const DateTimeFormat::ComponentsBag& aBag,
+ const PRExplodedTime* aExplodedTime,
+ nsAString& aStringOut) {
+ // set up locale data
+ nsresult rv = Initialize();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aStringOut.Truncate();
+
+ nsAutoCString str;
+ nsAutoString timeZoneID;
+ BuildTimeZoneString(aExplodedTime->tm_params, timeZoneID);
+
+ auto genResult = DateTimePatternGenerator::TryCreate(sLocale->get());
+ NS_ENSURE_TRUE(genResult.isOk(), NS_ERROR_FAILURE);
+ auto dateTimePatternGenerator = genResult.unwrap();
+
+ auto result = DateTimeFormat::TryCreateFromComponents(
+ *sLocale, aBag, dateTimePatternGenerator.get(), Some(timeZoneID));
+ NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE);
+ auto dateTimeFormat = result.unwrap();
+
+ double unixEpoch =
+ static_cast<float>((PR_ImplodeTime(aExplodedTime) / PR_USEC_PER_MSEC));
+
+ aStringOut.SetLength(DATETIME_FORMAT_INITIAL_LEN);
+ nsTStringToBufferAdapter buffer(aStringOut);
+ NS_ENSURE_TRUE(dateTimeFormat->TryFormat(unixEpoch, buffer).isOk(),
+ NS_ERROR_FAILURE);
+
+ return rv;
+}
+
+/**
+ * An internal utility function to serialize a Maybe<DateTimeFormat::Style> to
+ * an int, to be used as a caching key.
+ */
+static int StyleToInt(const Maybe<DateTimeFormat::Style>& aStyle) {
+ if (aStyle.isSome()) {
+ switch (*aStyle) {
+ case DateTimeFormat::Style::Full:
+ return 1;
+ case DateTimeFormat::Style::Long:
+ return 2;
+ case DateTimeFormat::Style::Medium:
+ return 3;
+ case DateTimeFormat::Style::Short:
+ return 4;
+ }
+ }
+ return 0;
+}
+
+/*static*/
+nsresult AppDateTimeFormat::Format(const DateTimeFormat::StyleBag& aStyle,
+ const double aUnixEpoch,
+ const PRTimeParameters* aTimeParameters,
+ nsAString& aStringOut) {
+ nsresult rv = NS_OK;
+
+ // return, nothing to format
+ if (aStyle.date.isNothing() && aStyle.time.isNothing()) {
+ aStringOut.Truncate();
+ return NS_OK;
+ }
+
+ // set up locale data
+ rv = Initialize();
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoCString key;
+ key.AppendInt(StyleToInt(aStyle.date));
+ key.Append(':');
+ key.AppendInt(StyleToInt(aStyle.time));
+ if (aTimeParameters) {
+ key.Append(':');
+ key.AppendInt(aTimeParameters->tp_gmt_offset);
+ key.Append(':');
+ key.AppendInt(aTimeParameters->tp_dst_offset);
+ }
+
+ if (sFormatCache && sFormatCache->Count() == kMaxCachedFormats) {
+ // Don't allow a pathological page to extend the cache unreasonably.
+ NS_WARNING("flushing DateTimeFormat cache");
+ DeleteCache();
+ }
+ if (!sFormatCache) {
+ sFormatCache = new nsTHashMap<nsCStringHashKey, UniquePtr<DateTimeFormat>>(
+ kMaxCachedFormats);
+ }
+
+ UniquePtr<DateTimeFormat>& dateTimeFormat = sFormatCache->LookupOrInsert(key);
+
+ if (!dateTimeFormat) {
+ // We didn't have a cached formatter for this key, so create one.
+ int32_t dateFormatStyle = mozIOSPreferences::dateTimeFormatStyleNone;
+ if (aStyle.date.isSome()) {
+ switch (*aStyle.date) {
+ case DateTimeFormat::Style::Full:
+ case DateTimeFormat::Style::Long:
+ dateFormatStyle = mozIOSPreferences::dateTimeFormatStyleLong;
+ break;
+ case DateTimeFormat::Style::Medium:
+ case DateTimeFormat::Style::Short:
+ dateFormatStyle = mozIOSPreferences::dateTimeFormatStyleShort;
+ break;
+ }
+ }
+
+ int32_t timeFormatStyle = mozIOSPreferences::dateTimeFormatStyleNone;
+ if (aStyle.time.isSome()) {
+ switch (*aStyle.time) {
+ case DateTimeFormat::Style::Full:
+ case DateTimeFormat::Style::Long:
+ timeFormatStyle = mozIOSPreferences::dateTimeFormatStyleLong;
+ break;
+ case DateTimeFormat::Style::Medium:
+ case DateTimeFormat::Style::Short:
+ timeFormatStyle = mozIOSPreferences::dateTimeFormatStyleShort;
+ break;
+ }
+ }
+
+ nsAutoCString str;
+ rv = OSPreferences::GetInstance()->GetDateTimePattern(
+ dateFormatStyle, timeFormatStyle, nsDependentCString(sLocale->get()),
+ str);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString pattern = NS_ConvertUTF8toUTF16(str);
+
+ Maybe<Span<const char16_t>> timeZoneOverride = Nothing();
+ nsAutoString timeZoneID;
+ if (aTimeParameters) {
+ BuildTimeZoneString(*aTimeParameters, timeZoneID);
+ timeZoneOverride =
+ Some(Span<const char16_t>(timeZoneID.Data(), timeZoneID.Length()));
+ }
+
+ auto result = DateTimeFormat::TryCreateFromPattern(*sLocale, pattern,
+ timeZoneOverride);
+ NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE);
+ dateTimeFormat = result.unwrap();
+ }
+
+ MOZ_ASSERT(dateTimeFormat);
+
+ aStringOut.SetLength(DATETIME_FORMAT_INITIAL_LEN);
+ nsTStringToBufferAdapter buffer(aStringOut);
+ NS_ENSURE_TRUE(dateTimeFormat->TryFormat(aUnixEpoch, buffer).isOk(),
+ NS_ERROR_FAILURE);
+
+ return rv;
+}
+
+/*static*/
+void AppDateTimeFormat::BuildTimeZoneString(
+ const PRTimeParameters& aTimeParameters, nsAString& aStringOut) {
+ aStringOut.Truncate();
+ aStringOut.Append(u"GMT");
+ int32_t totalOffsetMinutes =
+ (aTimeParameters.tp_gmt_offset + aTimeParameters.tp_dst_offset) / 60;
+ if (totalOffsetMinutes != 0) {
+ char sign = totalOffsetMinutes < 0 ? '-' : '+';
+ int32_t hours = abs(totalOffsetMinutes) / 60;
+ int32_t minutes = abs(totalOffsetMinutes) % 60;
+ aStringOut.AppendPrintf("%c%02d:%02d", sign, hours, minutes);
+ }
+}
+
+/*static*/
+void AppDateTimeFormat::DeleteCache() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sFormatCache) {
+ delete sFormatCache;
+ sFormatCache = nullptr;
+ }
+}
+
+/*static*/
+void AppDateTimeFormat::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DeleteCache();
+ delete sLocale;
+}
+
+/*static*/
+void AppDateTimeFormat::ClearLocaleCache() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DeleteCache();
+ delete sLocale;
+ sLocale = nullptr;
+}
+
+} // namespace mozilla::intl
diff --git a/intl/locale/AppDateTimeFormat.h b/intl/locale/AppDateTimeFormat.h
new file mode 100644
index 0000000000..cdc8a499bb
--- /dev/null
+++ b/intl/locale/AppDateTimeFormat.h
@@ -0,0 +1,89 @@
+/* -*- 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/. */
+
+#ifndef mozilla_intl_AppDateTimeFormat_h
+#define mozilla_intl_AppDateTimeFormat_h
+
+#include <time.h>
+#include "gtest/MozGtestFriend.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+#include "prtime.h"
+#include "mozilla/intl/DateTimeFormat.h"
+
+namespace mozilla::intl {
+
+/**
+ * Get a DateTimeFormat for use in Gecko. This specialized DateTimeFormat
+ * respects the user's OS and app preferences, and provides caching of the
+ * underlying mozilla::intl resources.
+ *
+ * This class is not thread-safe as it lazily initializes a cache without
+ * any type of multi-threaded protections.
+ */
+class AppDateTimeFormat {
+ public:
+ /**
+ * Format a DateTime using the applied app and OS-level preferences, with a
+ * style bag and the PRTime.
+ */
+ static nsresult Format(const DateTimeFormat::StyleBag& aStyle,
+ const PRTime aPrTime, nsAString& aStringOut);
+
+ /**
+ * Format a DateTime using the applied app and OS-level preferences, with a
+ * style bag and the PRExplodedTime.
+ */
+ static nsresult Format(const DateTimeFormat::StyleBag& aStyle,
+ const PRExplodedTime* aExplodedTime,
+ nsAString& aStringOut);
+
+ /**
+ * Format a DateTime using the applied app and OS-level preferences, with a
+ * components bag and the PRExplodedTime.
+ */
+ static nsresult Format(const DateTimeFormat::ComponentsBag& aComponents,
+ const PRExplodedTime* aExplodedTime,
+ nsAString& aStringOut);
+
+ /**
+ * If the app locale changes, the cached locale needs to be reset.
+ */
+ static void ClearLocaleCache();
+
+ static void Shutdown();
+
+ private:
+ AppDateTimeFormat() = delete;
+
+ static nsresult Initialize();
+ static void DeleteCache();
+ static const size_t kMaxCachedFormats = 15;
+
+ FRIEND_TEST(AppDateTimeFormat, FormatPRExplodedTime);
+ FRIEND_TEST(AppDateTimeFormat, DateFormatSelectors);
+ FRIEND_TEST(AppDateTimeFormat, FormatPRExplodedTimeForeign);
+ FRIEND_TEST(AppDateTimeFormat, DateFormatSelectorsForeign);
+
+ /**
+ * Format a DateTime using the applied app and OS-level preferences, with a
+ * components bag and the PRExplodedTime.
+ */
+ static nsresult Format(const DateTimeFormat::StyleBag& aStyle,
+ const double aUnixEpoch,
+ const PRTimeParameters* aTimeParameters,
+ nsAString& aStringOut);
+
+ static void BuildTimeZoneString(const PRTimeParameters& aTimeParameters,
+ nsAString& aStringOut);
+
+ static nsCString* sLocale;
+ static nsTHashMap<nsCStringHashKey, UniquePtr<DateTimeFormat>>* sFormatCache;
+};
+
+} // namespace mozilla::intl
+
+#endif /* mozilla_intl_AppDateTimeFormat_h */
diff --git a/intl/locale/LangPackMatcher.sys.mjs b/intl/locale/LangPackMatcher.sys.mjs
new file mode 100644
index 0000000000..977398b082
--- /dev/null
+++ b/intl/locale/LangPackMatcher.sys.mjs
@@ -0,0 +1,395 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
+});
+
+if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // This check ensures that the `mockable` API calls can be consisently mocked in tests.
+ // If this requirement needs to be eased, please ensure the test logic remains valid.
+ throw new Error("This code is assumed to run in the parent process.");
+}
+
+/**
+ * Attempts to find an appropriate langpack for a given language. The async function
+ * is infallible, but may not return a langpack.
+ *
+ * @returns {{
+ * langPack: LangPack | null,
+ * langPackDisplayName: string | null
+ * }}
+ */
+async function negotiateLangPackForLanguageMismatch() {
+ const localeInfo = getAppAndSystemLocaleInfo();
+ const nullResult = {
+ langPack: null,
+ langPackDisplayName: null,
+ };
+ if (!localeInfo.systemLocale) {
+ // The system locale info was not valid.
+ return nullResult;
+ }
+
+ /**
+ * Fetch the available langpacks from AMO.
+ *
+ * @type {Array<LangPack>}
+ */
+ const availableLangpacks = await mockable.getAvailableLangpacks();
+ if (!availableLangpacks) {
+ return nullResult;
+ }
+
+ /**
+ * Figure out a langpack to recommend.
+ * @type {LangPack | null}
+ */
+ const langPack =
+ // First look for a langpack that matches the baseName, which may include a script.
+ // e.g. system "fr-FR" matches langpack "fr-FR"
+ // system "en-GB" matches langpack "en-GB".
+ // system "zh-Hant-CN" matches langpack "zh-Hant-CN".
+ availableLangpacks.find(
+ ({ target_locale }) => target_locale === localeInfo.systemLocale.baseName
+ ) ||
+ // Next try matching language and region while excluding script
+ // e.g. system "zh-Hant-TW" matches langpack "zh-TW" but not "zh-CN".
+ availableLangpacks.find(
+ ({ target_locale }) =>
+ target_locale ===
+ `${localeInfo.systemLocale.language}-${localeInfo.systemLocale.region}`
+ ) ||
+ // Next look for langpacks that just match the language.
+ // e.g. system "fr-FR" matches langpack "fr".
+ // system "en-AU" matches langpack "en".
+ availableLangpacks.find(
+ ({ target_locale }) => target_locale === localeInfo.systemLocale.language
+ ) ||
+ // Next look for a langpack that matches the language, but not the region.
+ // e.g. "es-CL" (Chilean Spanish) as a system language matching
+ // "es-ES" (European Spanish)
+ availableLangpacks.find(({ target_locale }) =>
+ target_locale.startsWith(`${localeInfo.systemLocale.language}-`)
+ ) ||
+ null;
+
+ if (!langPack) {
+ return nullResult;
+ }
+
+ return {
+ langPack,
+ langPackDisplayName: Services.intl.getLocaleDisplayNames(
+ undefined,
+ [langPack.target_locale],
+ { preferNative: true }
+ )[0],
+ };
+}
+
+// If a langpack is being installed, allow blocking on that.
+let installingLangpack = new Map();
+
+/**
+ * @typedef {LangPack}
+ * @type {object}
+ * @property {string} target_locale
+ * @property {string} url
+ * @property {string} hash
+ */
+
+/**
+ * Ensure that a given lanpack is installed.
+ *
+ * @param {LangPack} langPack
+ * @returns {Promise<boolean>} Success or failure.
+ */
+function ensureLangPackInstalled(langPack) {
+ if (!langPack) {
+ throw new Error("Expected a LangPack to install.");
+ }
+ // Make sure any outstanding calls get resolved before attempting another call.
+ // This guards against any quick page refreshes attempting to install the langpack
+ // twice.
+ const inProgress = installingLangpack.get(langPack.hash);
+ if (inProgress) {
+ return inProgress;
+ }
+ const promise = _ensureLangPackInstalledImpl(langPack);
+ installingLangpack.set(langPack.hash, promise);
+ promise.finally(() => {
+ installingLangpack.delete(langPack.hash);
+ });
+ return promise;
+}
+
+/**
+ * @param {LangPack} langPack
+ * @returns {boolean} Success or failure.
+ */
+async function _ensureLangPackInstalledImpl(langPack) {
+ const availablelocales = await getAvailableLocales();
+ if (availablelocales.includes(langPack.target_locale)) {
+ // The langpack is already installed.
+ return true;
+ }
+
+ return mockable.installLangPack(langPack);
+}
+
+/**
+ * These are all functions with side effects or configuration options that should be
+ * mockable for tests.
+ */
+const mockable = {
+ /**
+ * @returns {LangPack[] | null}
+ */
+ async getAvailableLangpacks() {
+ try {
+ return lazy.AddonRepository.getAvailableLangpacks();
+ } catch (error) {
+ console.error(
+ `Failed to get the list of available language packs: ${error?.message}`
+ );
+ return null;
+ }
+ },
+
+ /**
+ * Use the AddonManager to install an addon from the URL.
+ * @param {LangPack} langPack
+ */
+ async installLangPack(langPack) {
+ let install;
+ try {
+ install = await lazy.AddonManager.getInstallForURL(langPack.url, {
+ hash: langPack.hash,
+ telemetryInfo: {
+ source: "about:welcome",
+ },
+ });
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+
+ try {
+ await install.install();
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Returns the available locales, including the fallback locale, which may not include
+ * all of the resources, in cases where the defaultLocale is not "en-US".
+ *
+ * @returns {string[]}
+ */
+ getAvailableLocalesIncludingFallback() {
+ return Services.locale.availableLocales;
+ },
+
+ /**
+ * @returns {string}
+ */
+ getDefaultLocale() {
+ return Services.locale.defaultLocale;
+ },
+
+ /**
+ * @returns {string}
+ */
+ getLastFallbackLocale() {
+ return Services.locale.lastFallbackLocale;
+ },
+
+ /**
+ * @returns {string}
+ */
+ getAppLocaleAsBCP47() {
+ return Services.locale.appLocaleAsBCP47;
+ },
+
+ /**
+ * @returns {string}
+ */
+ getSystemLocale() {
+ // Allow the system locale to be overridden for manual testing.
+ const systemLocaleOverride = Services.prefs.getCharPref(
+ "intl.multilingual.aboutWelcome.systemLocaleOverride",
+ null
+ );
+ if (systemLocaleOverride) {
+ try {
+ // If the locale can't be parsed, ignore the pref.
+ new Services.intl.Locale(systemLocaleOverride);
+ return systemLocaleOverride;
+ } catch (_error) {}
+ }
+
+ const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ );
+ return osPrefs.systemLocale;
+ },
+
+ /**
+ * @param {string[]} locales The BCP 47 locale identifiers.
+ */
+ setRequestedAppLocales(locales) {
+ Services.locale.requestedLocales = locales;
+ },
+};
+
+/**
+ * This function is really only setting `Services.locale.requestedLocales`, but it's
+ * using the `mockable` object to allow this behavior to be mocked in tests.
+ *
+ * @param {string[]} locales The BCP 47 locale identifiers.
+ */
+function setRequestedAppLocales(locales) {
+ mockable.setRequestedAppLocales(locales);
+}
+
+/**
+ * A serializable Intl.Locale.
+ *
+ * @typedef StructuredLocale
+ * @type {object}
+ * @property {string} baseName
+ * @property {string} language
+ * @property {string} region
+ */
+
+/**
+ * In telemetry data, some of the system locales show up as blank. Guard against this
+ * and any other malformed locale information provided by the system by wrapping the call
+ * into a catch/try.
+ *
+ * @param {string} locale
+ * @returns {StructuredLocale | null}
+ */
+function getStructuredLocaleOrNull(localeString) {
+ try {
+ const locale = new Services.intl.Locale(localeString);
+ return {
+ baseName: locale.baseName,
+ language: locale.language,
+ region: locale.region,
+ };
+ } catch (_err) {
+ return null;
+ }
+}
+
+/**
+ * Determine the system and app locales, and how much the locales match.
+ *
+ * @returns {{
+ * systemLocale: StructuredLocale,
+ * appLocale: StructuredLocale,
+ * matchType: "unknown" | "language-mismatch" | "region-mismatch" | "match",
+ * }}
+ */
+function getAppAndSystemLocaleInfo() {
+ // Convert locale strings into structured locale objects.
+ const systemLocaleRaw = mockable.getSystemLocale();
+ const appLocaleRaw = mockable.getAppLocaleAsBCP47();
+
+ const systemLocale = getStructuredLocaleOrNull(systemLocaleRaw);
+ const appLocale = getStructuredLocaleOrNull(appLocaleRaw);
+
+ let matchType = "unknown";
+ if (systemLocale && appLocale) {
+ if (systemLocale.language !== appLocale.language) {
+ matchType = "language-mismatch";
+ } else if (systemLocale.region !== appLocale.region) {
+ matchType = "region-mismatch";
+ } else {
+ matchType = "match";
+ }
+ }
+
+ // Live reloading with bidi switching may not be supported.
+ let canLiveReload = null;
+ if (systemLocale && appLocale) {
+ const systemDirection = Services.intl.getScriptDirection(
+ systemLocale.language
+ );
+ const appDirection = Services.intl.getScriptDirection(appLocale.language);
+ const supportsBidiSwitching = Services.prefs.getBoolPref(
+ "intl.multilingual.liveReloadBidirectional",
+ false
+ );
+ canLiveReload = systemDirection === appDirection || supportsBidiSwitching;
+ }
+ return {
+ // Return the Intl.Locale in a serializable form.
+ systemLocaleRaw,
+ systemLocale,
+ appLocaleRaw,
+ appLocale,
+ matchType,
+ canLiveReload,
+
+ // These can be used as Fluent message args.
+ displayNames: {
+ systemLanguage: systemLocale
+ ? Services.intl.getLocaleDisplayNames(
+ undefined,
+ [systemLocale.baseName],
+ { preferNative: true }
+ )[0]
+ : null,
+ appLanguage: appLocale
+ ? Services.intl.getLocaleDisplayNames(undefined, [appLocale.baseName], {
+ preferNative: true,
+ })[0]
+ : null,
+ },
+ };
+}
+
+/**
+ * Filter the lastFallbackLocale from availableLocales if it doesn't have all
+ * of the needed strings.
+ *
+ * When the lastFallbackLocale isn't the defaultLocale, then by default only
+ * fluent strings are included. To fully use that locale you need the langpack
+ * to be installed, so if it isn't installed remove it from availableLocales.
+ */
+async function getAvailableLocales() {
+ const availableLocales = mockable.getAvailableLocalesIncludingFallback();
+ const defaultLocale = mockable.getDefaultLocale();
+ const lastFallbackLocale = mockable.getLastFallbackLocale();
+ // If defaultLocale isn't lastFallbackLocale, then we still need the langpack
+ // for lastFallbackLocale for it to be useful.
+ if (defaultLocale != lastFallbackLocale) {
+ let lastFallbackId = `langpack-${lastFallbackLocale}@firefox.mozilla.org`;
+ let lastFallbackInstalled = await lazy.AddonManager.getAddonByID(
+ lastFallbackId
+ );
+ if (!lastFallbackInstalled) {
+ return availableLocales.filter(locale => locale != lastFallbackLocale);
+ }
+ }
+ return availableLocales;
+}
+
+export var LangPackMatcher = {
+ negotiateLangPackForLanguageMismatch,
+ ensureLangPackInstalled,
+ getAppAndSystemLocaleInfo,
+ setRequestedAppLocales,
+ getAvailableLocales,
+ mockable,
+};
diff --git a/intl/locale/LocaleService.cpp b/intl/locale/LocaleService.cpp
new file mode 100644
index 0000000000..1527f27910
--- /dev/null
+++ b/intl/locale/LocaleService.cpp
@@ -0,0 +1,693 @@
+/* -*- Mode: C++; tab-width: 4; 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 "LocaleService.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Omnijar.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/OSPreferences.h"
+#include "nsDirectoryService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIObserverService.h"
+#include "nsStringEnumerator.h"
+#include "nsXULAppAPI.h"
+#include "nsZipArchive.h"
+#ifdef XP_WIN
+# include "WinUtils.h"
+#endif
+#ifdef MOZ_WIDGET_GTK
+# include "mozilla/WidgetUtilsGtk.h"
+#endif
+
+#define INTL_SYSTEM_LOCALES_CHANGED "intl:system-locales-changed"
+
+#define PSEUDO_LOCALE_PREF "intl.l10n.pseudo"
+#define REQUESTED_LOCALES_PREF "intl.locale.requested"
+#define WEB_EXPOSED_LOCALES_PREF "intl.locale.privacy.web_exposed"
+
+static const char* kObservedPrefs[] = {REQUESTED_LOCALES_PREF,
+ WEB_EXPOSED_LOCALES_PREF,
+ PSEUDO_LOCALE_PREF, nullptr};
+
+using namespace mozilla::intl::ffi;
+using namespace mozilla::intl;
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(LocaleService, mozILocaleService, nsIObserver,
+ nsISupportsWeakReference)
+
+mozilla::StaticRefPtr<LocaleService> LocaleService::sInstance;
+
+/**
+ * This function splits an input string by `,` delimiter, sanitizes the result
+ * language tags and returns them to the caller.
+ */
+static void SplitLocaleListStringIntoArray(nsACString& str,
+ nsTArray<nsCString>& aRetVal) {
+ if (str.Length() > 0) {
+ for (const nsACString& part : str.Split(',')) {
+ nsAutoCString locale(part);
+ if (LocaleService::CanonicalizeLanguageId(locale)) {
+ if (!aRetVal.Contains(locale)) {
+ aRetVal.AppendElement(locale);
+ }
+ }
+ }
+ }
+}
+
+static void ReadRequestedLocales(nsTArray<nsCString>& aRetVal) {
+ nsAutoCString str;
+ nsresult rv = Preferences::GetCString(REQUESTED_LOCALES_PREF, str);
+ // isRepack means this is a version of Firefox specifically
+ // built for one language.
+ const bool isRepack =
+#ifdef XP_WIN
+ !mozilla::widget::WinUtils::HasPackageIdentity();
+#elif defined(MOZ_WIDGET_GTK)
+ !widget::IsRunningUnderSnap();
+#else
+ true;
+#endif
+
+ // We handle four scenarios here:
+ //
+ // 1) The pref is not set - use default locale
+ // 2) The pref is not set and we're a packaged app - use OS locales
+ // 3) The pref is set to "" - use OS locales
+ // 4) The pref is set to a value - parse the locale list and use it
+ if (NS_SUCCEEDED(rv)) {
+ if (str.Length() == 0) {
+ // Case 3
+ OSPreferences::GetInstance()->GetSystemLocales(aRetVal);
+ } else {
+ // Case 4
+ SplitLocaleListStringIntoArray(str, aRetVal);
+ }
+ }
+
+ // This will happen when either the pref is not set,
+ // or parsing of the pref didn't produce any usable
+ // result.
+ if (aRetVal.IsEmpty()) {
+ if (isRepack) {
+ // Case 1
+ nsAutoCString defaultLocale;
+ LocaleService::GetInstance()->GetDefaultLocale(defaultLocale);
+ aRetVal.AppendElement(defaultLocale);
+ } else {
+ // Case 2
+ OSPreferences::GetInstance()->GetSystemLocales(aRetVal);
+ }
+ }
+}
+
+static void ReadWebExposedLocales(nsTArray<nsCString>& aRetVal) {
+ nsAutoCString str;
+ nsresult rv = Preferences::GetCString(WEB_EXPOSED_LOCALES_PREF, str);
+ if (NS_WARN_IF(NS_FAILED(rv)) || str.Length() == 0) {
+ return;
+ }
+
+ SplitLocaleListStringIntoArray(str, aRetVal);
+}
+
+LocaleService::LocaleService(bool aIsServer) : mIsServer(aIsServer) {}
+
+/**
+ * This function performs the actual language negotiation for the API.
+ *
+ * Currently it collects the locale ID used by nsChromeRegistry and
+ * adds hardcoded default locale as a fallback.
+ */
+void LocaleService::NegotiateAppLocales(nsTArray<nsCString>& aRetVal) {
+ if (mIsServer) {
+ nsAutoCString defaultLocale;
+ AutoTArray<nsCString, 100> availableLocales;
+ AutoTArray<nsCString, 10> requestedLocales;
+ GetDefaultLocale(defaultLocale);
+ GetAvailableLocales(availableLocales);
+ GetRequestedLocales(requestedLocales);
+
+ NegotiateLanguages(requestedLocales, availableLocales, defaultLocale,
+ kLangNegStrategyFiltering, aRetVal);
+ }
+
+ nsAutoCString lastFallbackLocale;
+ GetLastFallbackLocale(lastFallbackLocale);
+
+ if (!aRetVal.Contains(lastFallbackLocale)) {
+ // This part is used in one of the two scenarios:
+ //
+ // a) We're in a client mode, and no locale has been set yet,
+ // so we need to return last fallback locale temporarily.
+ // b) We're in a server mode, and the last fallback locale was excluded
+ // when negotiating against the requested locales.
+ // Since we currently package it as a last fallback at build
+ // time, we should also add it at the end of the list at
+ // runtime.
+ aRetVal.AppendElement(lastFallbackLocale);
+ }
+}
+
+LocaleService* LocaleService::GetInstance() {
+ if (!sInstance) {
+ sInstance = new LocaleService(XRE_IsParentProcess());
+
+ if (sInstance->IsServer()) {
+ // We're going to observe for requested languages changes which come
+ // from prefs.
+ DebugOnly<nsresult> rv =
+ Preferences::AddWeakObservers(sInstance, kObservedPrefs);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
+
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(sInstance, INTL_SYSTEM_LOCALES_CHANGED, true);
+ obs->AddObserver(sInstance, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ }
+ }
+ // DOM might use ICUUtils and LocaleService during UnbindFromTree by
+ // final cycle collection.
+ ClearOnShutdown(&sInstance, ShutdownPhase::CCPostLastCycleCollection);
+ }
+ return sInstance;
+}
+
+static void NotifyAppLocaleChanged() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);
+ }
+ // The locale in AppDateTimeFormat is cached statically.
+ AppDateTimeFormat::ClearLocaleCache();
+}
+
+void LocaleService::RemoveObservers() {
+ if (mIsServer) {
+ Preferences::RemoveObservers(this, kObservedPrefs);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, INTL_SYSTEM_LOCALES_CHANGED);
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+ }
+}
+
+void LocaleService::AssignAppLocales(const nsTArray<nsCString>& aAppLocales) {
+ MOZ_ASSERT(!mIsServer,
+ "This should only be called for LocaleService in client mode.");
+
+ mAppLocales = aAppLocales.Clone();
+ NotifyAppLocaleChanged();
+}
+
+void LocaleService::AssignRequestedLocales(
+ const nsTArray<nsCString>& aRequestedLocales) {
+ MOZ_ASSERT(!mIsServer,
+ "This should only be called for LocaleService in client mode.");
+
+ mRequestedLocales = aRequestedLocales.Clone();
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
+ }
+}
+
+void LocaleService::RequestedLocalesChanged() {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+
+ nsTArray<nsCString> newLocales;
+ ReadRequestedLocales(newLocales);
+
+ if (mRequestedLocales != newLocales) {
+ mRequestedLocales = std::move(newLocales);
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
+ }
+ LocalesChanged();
+ }
+}
+
+void LocaleService::WebExposedLocalesChanged() {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+
+ nsTArray<nsCString> newLocales;
+ ReadWebExposedLocales(newLocales);
+ if (mWebExposedLocales != newLocales) {
+ mWebExposedLocales = std::move(newLocales);
+ }
+}
+
+void LocaleService::LocalesChanged() {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+
+ // if mAppLocales has not been initialized yet, just return
+ if (mAppLocales.IsEmpty()) {
+ return;
+ }
+
+ nsTArray<nsCString> newLocales;
+ NegotiateAppLocales(newLocales);
+
+ if (mAppLocales != newLocales) {
+ mAppLocales = std::move(newLocales);
+ NotifyAppLocaleChanged();
+ }
+}
+
+bool LocaleService::IsLocaleRTL(const nsACString& aLocale) {
+ return unic_langid_is_rtl(&aLocale);
+}
+
+bool LocaleService::IsAppLocaleRTL() {
+ // Next, check if there is a pseudo locale `bidi` set.
+ nsAutoCString pseudoLocale;
+ if (NS_SUCCEEDED(Preferences::GetCString("intl.l10n.pseudo", pseudoLocale))) {
+ if (pseudoLocale.EqualsLiteral("bidi")) {
+ return true;
+ }
+ if (pseudoLocale.EqualsLiteral("accented")) {
+ return false;
+ }
+ }
+
+ nsAutoCString locale;
+ GetAppLocaleAsBCP47(locale);
+ return IsLocaleRTL(locale);
+}
+
+NS_IMETHODIMP
+LocaleService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+
+ if (!strcmp(aTopic, INTL_SYSTEM_LOCALES_CHANGED)) {
+ RequestedLocalesChanged();
+ WebExposedLocalesChanged();
+ } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ RemoveObservers();
+ } else {
+ NS_ConvertUTF16toUTF8 pref(aData);
+ // At the moment the only thing we're observing are settings indicating
+ // user requested locales.
+ if (pref.EqualsLiteral(REQUESTED_LOCALES_PREF)) {
+ RequestedLocalesChanged();
+ } else if (pref.EqualsLiteral(WEB_EXPOSED_LOCALES_PREF)) {
+ WebExposedLocalesChanged();
+ } else if (pref.EqualsLiteral(PSEUDO_LOCALE_PREF)) {
+ NotifyAppLocaleChanged();
+ }
+ }
+
+ return NS_OK;
+}
+
+bool LocaleService::LanguagesMatch(const nsACString& aRequested,
+ const nsACString& aAvailable) {
+ Locale requested;
+ auto requestedResult = LocaleParser::TryParse(aRequested, requested);
+ Locale available;
+ auto availableResult = LocaleParser::TryParse(aAvailable, available);
+
+ if (requestedResult.isErr() || availableResult.isErr()) {
+ return false;
+ }
+
+ if (requested.Canonicalize().isErr() || available.Canonicalize().isErr()) {
+ return false;
+ }
+
+ return requested.Language().Span() == available.Language().Span();
+}
+
+bool LocaleService::IsServer() { return mIsServer; }
+
+static bool GetGREFileContents(const char* aFilePath, nsCString* aOutString) {
+ // Look for the requested file in omnijar.
+ RefPtr<nsZipArchive> zip = Omnijar::GetReader(Omnijar::GRE);
+ if (zip) {
+ nsZipItemPtr<char> item(zip, aFilePath);
+ if (!item) {
+ return false;
+ }
+ aOutString->Assign(item.Buffer(), item.Length());
+ return true;
+ }
+
+ // If we didn't have an omnijar (i.e. we're running a non-packaged
+ // build), then look in the GRE directory.
+ nsCOMPtr<nsIFile> path;
+ if (NS_FAILED(nsDirectoryService::gService->Get(
+ NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(path)))) {
+ return false;
+ }
+
+ path->AppendRelativeNativePath(nsDependentCString(aFilePath));
+ bool result;
+ if (NS_FAILED(path->IsFile(&result)) || !result ||
+ NS_FAILED(path->IsReadable(&result)) || !result) {
+ return false;
+ }
+
+ // This is a small file, only used once, so it's not worth doing some fancy
+ // off-main-thread file I/O or whatever. Just read it.
+ FILE* fp;
+ if (NS_FAILED(path->OpenANSIFileDesc("r", &fp)) || !fp) {
+ return false;
+ }
+
+ fseek(fp, 0, SEEK_END);
+ long len = ftell(fp);
+ rewind(fp);
+ aOutString->SetLength(len);
+ size_t cc = fread(aOutString->BeginWriting(), 1, len, fp);
+
+ fclose(fp);
+
+ return cc == size_t(len);
+}
+
+void LocaleService::InitPackagedLocales() {
+ MOZ_ASSERT(mPackagedLocales.IsEmpty());
+
+ nsAutoCString localesString;
+ if (GetGREFileContents("res/multilocale.txt", &localesString)) {
+ localesString.Trim(" \t\n\r");
+ // This should never be empty in a correctly-built product.
+ MOZ_ASSERT(!localesString.IsEmpty());
+ SplitLocaleListStringIntoArray(localesString, mPackagedLocales);
+ }
+
+ // Last resort in case of broken build
+ if (mPackagedLocales.IsEmpty()) {
+ nsAutoCString defaultLocale;
+ GetDefaultLocale(defaultLocale);
+ mPackagedLocales.AppendElement(defaultLocale);
+ }
+}
+
+/**
+ * mozILocaleService methods
+ */
+
+NS_IMETHODIMP
+LocaleService::GetDefaultLocale(nsACString& aRetVal) {
+ // We don't allow this to change during a session (it's set at build/package
+ // time), so we cache the result the first time we're called.
+ if (mDefaultLocale.IsEmpty()) {
+ nsAutoCString locale;
+ // Try to get the package locale from update.locale in omnijar. If the
+ // update.locale file is not found, item.len will remain 0 and we'll
+ // just use our hard-coded default below.
+ GetGREFileContents("update.locale", &locale);
+ locale.Trim(" \t\n\r");
+#ifdef MOZ_UPDATER
+ // This should never be empty.
+ MOZ_ASSERT(!locale.IsEmpty());
+#endif
+ if (CanonicalizeLanguageId(locale)) {
+ mDefaultLocale.Assign(locale);
+ }
+
+ // Hard-coded fallback to allow us to survive even if update.locale was
+ // missing/broken in some way.
+ if (mDefaultLocale.IsEmpty()) {
+ GetLastFallbackLocale(mDefaultLocale);
+ }
+ }
+
+ aRetVal = mDefaultLocale;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetLastFallbackLocale(nsACString& aRetVal) {
+ aRetVal.AssignLiteral("en-US");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetAppLocalesAsLangTags(nsTArray<nsCString>& aRetVal) {
+ if (mAppLocales.IsEmpty()) {
+ NegotiateAppLocales(mAppLocales);
+ }
+ for (uint32_t i = 0; i < mAppLocales.Length(); i++) {
+ nsAutoCString locale(mAppLocales[i]);
+ if (locale.LowerCaseEqualsASCII("ja-jp-macos")) {
+ aRetVal.AppendElement("ja-JP-mac");
+ } else {
+ aRetVal.AppendElement(locale);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetAppLocalesAsBCP47(nsTArray<nsCString>& aRetVal) {
+ if (mAppLocales.IsEmpty()) {
+ NegotiateAppLocales(mAppLocales);
+ }
+ aRetVal = mAppLocales.Clone();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetAppLocaleAsLangTag(nsACString& aRetVal) {
+ AutoTArray<nsCString, 32> locales;
+ GetAppLocalesAsLangTags(locales);
+
+ aRetVal = locales[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetAppLocaleAsBCP47(nsACString& aRetVal) {
+ if (mAppLocales.IsEmpty()) {
+ NegotiateAppLocales(mAppLocales);
+ }
+ aRetVal = mAppLocales[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
+ bool useOSLocales =
+ Preferences::GetBool("intl.regional_prefs.use_os_locales", false);
+
+ // If the user specified that they want to use OS Regional Preferences
+ // locales, try to retrieve them and use.
+ if (useOSLocales) {
+ if (NS_SUCCEEDED(
+ OSPreferences::GetInstance()->GetRegionalPrefsLocales(aRetVal))) {
+ return NS_OK;
+ }
+
+ // If we fail to retrieve them, return the app locales.
+ GetAppLocalesAsBCP47(aRetVal);
+ return NS_OK;
+ }
+
+ // Otherwise, fetch OS Regional Preferences locales and compare the first one
+ // to the app locale. If the language subtag matches, we can safely use
+ // the OS Regional Preferences locale.
+ //
+ // This facilitates scenarios such as Firefox in "en-US" and User sets
+ // regional prefs to "en-GB".
+ nsAutoCString appLocale;
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocale);
+
+ if (NS_FAILED(OSPreferences::GetInstance()->GetRegionalPrefsLocales(
+ regionalPrefsLocales))) {
+ GetAppLocalesAsBCP47(aRetVal);
+ return NS_OK;
+ }
+
+ if (LocaleService::LanguagesMatch(appLocale, regionalPrefsLocales[0])) {
+ aRetVal = regionalPrefsLocales.Clone();
+ return NS_OK;
+ }
+
+ // Otherwise use the app locales.
+ GetAppLocalesAsBCP47(aRetVal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetWebExposedLocales(nsTArray<nsCString>& aRetVal) {
+ if (StaticPrefs::privacy_spoof_english() == 2) {
+ aRetVal = nsTArray<nsCString>({"en-US"_ns});
+ return NS_OK;
+ }
+
+ if (!mWebExposedLocales.IsEmpty()) {
+ aRetVal = mWebExposedLocales.Clone();
+ return NS_OK;
+ }
+
+ return GetRegionalPrefsLocales(aRetVal);
+}
+
+NS_IMETHODIMP
+LocaleService::NegotiateLanguages(const nsTArray<nsCString>& aRequested,
+ const nsTArray<nsCString>& aAvailable,
+ const nsACString& aDefaultLocale,
+ int32_t aStrategy,
+ nsTArray<nsCString>& aRetVal) {
+ if (aStrategy < 0 || aStrategy > 2) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+#ifdef DEBUG
+ Locale parsedLocale;
+ auto result = LocaleParser::TryParse(aDefaultLocale, parsedLocale);
+
+ MOZ_ASSERT(
+ aDefaultLocale.IsEmpty() || result.isOk(),
+ "If specified, default locale must be a well-formed BCP47 language tag.");
+#endif
+
+ if (aStrategy == kLangNegStrategyLookup && aDefaultLocale.IsEmpty()) {
+ NS_WARNING(
+ "Default locale should be specified when using lookup strategy.");
+ }
+
+ NegotiationStrategy strategy;
+ switch (aStrategy) {
+ case kLangNegStrategyFiltering:
+ strategy = NegotiationStrategy::Filtering;
+ break;
+ case kLangNegStrategyMatching:
+ strategy = NegotiationStrategy::Matching;
+ break;
+ case kLangNegStrategyLookup:
+ strategy = NegotiationStrategy::Lookup;
+ break;
+ }
+
+ fluent_langneg_negotiate_languages(&aRequested, &aAvailable, &aDefaultLocale,
+ strategy, &aRetVal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetRequestedLocales(nsTArray<nsCString>& aRetVal) {
+ if (mRequestedLocales.IsEmpty()) {
+ ReadRequestedLocales(mRequestedLocales);
+ }
+
+ aRetVal = mRequestedLocales.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetRequestedLocale(nsACString& aRetVal) {
+ if (mRequestedLocales.IsEmpty()) {
+ ReadRequestedLocales(mRequestedLocales);
+ }
+
+ if (mRequestedLocales.Length() > 0) {
+ aRetVal = mRequestedLocales[0];
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::SetRequestedLocales(const nsTArray<nsCString>& aRequested) {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+ if (!mIsServer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsAutoCString str;
+
+ for (auto& req : aRequested) {
+ nsAutoCString locale(req);
+ if (!CanonicalizeLanguageId(locale)) {
+ NS_ERROR("Invalid language tag provided to SetRequestedLocales!");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!str.IsEmpty()) {
+ str.AppendLiteral(",");
+ }
+ str.Append(locale);
+ }
+ Preferences::SetCString(REQUESTED_LOCALES_PREF, str);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetAvailableLocales(nsTArray<nsCString>& aRetVal) {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+ if (!mIsServer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (mAvailableLocales.IsEmpty()) {
+ // If there are no available locales set, it means that L10nRegistry
+ // did not register its locale pool yet. The best course of action
+ // is to use packaged locales until that happens.
+ GetPackagedLocales(mAvailableLocales);
+ }
+
+ aRetVal = mAvailableLocales.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetIsAppLocaleRTL(bool* aRetVal) {
+ (*aRetVal) = IsAppLocaleRTL();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::SetAvailableLocales(const nsTArray<nsCString>& aAvailable) {
+ MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
+ if (!mIsServer) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsTArray<nsCString> newLocales;
+
+ for (auto& avail : aAvailable) {
+ nsAutoCString locale(avail);
+ if (!CanonicalizeLanguageId(locale)) {
+ NS_ERROR("Invalid language tag provided to SetAvailableLocales!");
+ return NS_ERROR_INVALID_ARG;
+ }
+ newLocales.AppendElement(locale);
+ }
+
+ if (newLocales != mAvailableLocales) {
+ mAvailableLocales = std::move(newLocales);
+ LocalesChanged();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocaleService::GetPackagedLocales(nsTArray<nsCString>& aRetVal) {
+ if (mPackagedLocales.IsEmpty()) {
+ InitPackagedLocales();
+ }
+ aRetVal = mPackagedLocales.Clone();
+ return NS_OK;
+}
diff --git a/intl/locale/LocaleService.h b/intl/locale/LocaleService.h
new file mode 100644
index 0000000000..f5b2c8dadc
--- /dev/null
+++ b/intl/locale/LocaleService.h
@@ -0,0 +1,232 @@
+/* -*- 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/. */
+
+#ifndef mozilla_intl_LocaleService_h__
+#define mozilla_intl_LocaleService_h__
+
+#include "nsIObserver.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+#include "MozLocaleBindings.h"
+#include "mozilla/intl/ICU4CGlue.h"
+#include "mozILocaleService.h"
+
+namespace mozilla {
+namespace intl {
+
+/**
+ * LocaleService is a manager of language negotiation in Gecko.
+ *
+ * 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.
+ *
+ * Client / Server
+ *
+ * LocaleService may operate in one of two modes:
+ *
+ * server
+ * in the server mode, LocaleService is collecting and negotiating
+ * languages. It also subscribes to relevant observers.
+ * There should be at most one server per application instance.
+ *
+ * client
+ * in the client mode, LocaleService is not responsible for collecting
+ * or reacting to any system changes. It still distributes information
+ * about locales, but internally, it gets information from the server
+ * instance instead of collecting it on its own. This prevents any data
+ * desynchronization and minimizes the cost of running the service.
+ *
+ * In both modes, all get* methods should work the same way and all
+ * static methods are available.
+ *
+ * In the server mode, other components may inform LocaleService about their
+ * status either via calls to set* methods or via observer events.
+ * In the client mode, only the process communication should provide data
+ * to the LocaleService.
+ *
+ * At the moment desktop apps use the parent process in the server mode, and
+ * content processes in the client mode.
+ *
+ * Locale / Language
+ *
+ * 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 final : public mozILocaleService,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ 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.
+ */
+ static const int32_t kLangNegStrategyFiltering = 0;
+ static const int32_t kLangNegStrategyMatching = 1;
+ static const int32_t kLangNegStrategyLookup = 2;
+
+ explicit LocaleService(bool aIsServer);
+
+ /**
+ * 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
+ * instance, but does not need to hold a reference, as in
+ * nsAutoCString str;
+ * LocaleService::GetInstance()->GetAppLocaleAsLangTag(str);
+ */
+ static LocaleService* GetInstance();
+
+ /**
+ * Return an addRef'd pointer to the singleton instance. This is used by the
+ * XPCOM constructor that exists to support usage from JS.
+ */
+ static already_AddRefed<LocaleService> GetInstanceAddRefed() {
+ return RefPtr<LocaleService>(GetInstance()).forget();
+ }
+
+ /**
+ * Canonicalize a Unicode Language Identifier string.
+ *
+ * The operation is:
+ * * Normalizing casing (`eN-Us-Windows` -> `en-US-windows`)
+ * * Switching `_` to `-` (`en_US` -> `en-US`)
+ * * Rejecting invalid identifiers (`e21-X` sets aLocale to `und` and
+ * returns false)
+ * * Normalizing Mozilla's `ja-JP-mac` to `ja-JP-macos`
+ * * Cutting off POSIX dot postfix (`en-US.utf8` -> `en-US`)
+ *
+ * This operation should be used on any external input before
+ * it gets used in internal operations.
+ */
+ static bool CanonicalizeLanguageId(nsACString& aLocale) {
+ return ffi::unic_langid_canonicalize(&aLocale);
+ }
+ /**
+ * This method should only be called in the client mode.
+ *
+ * It replaces all the language negotiation and is supposed to be called
+ * in order to bring the client LocaleService in sync with the server
+ * LocaleService.
+ *
+ * Currently, it's called by the IPC code.
+ */
+ void AssignAppLocales(const nsTArray<nsCString>& aAppLocales);
+ void AssignRequestedLocales(const nsTArray<nsCString>& aRequestedLocales);
+
+ /**
+ * Those two functions allow to trigger cache invalidation on one of the
+ * three cached values.
+ *
+ * In most cases, the functions will be called by the observer in
+ * LocaleService itself, but in a couple special cases, we have the
+ * other component call this manually instead of sending a global event.
+ *
+ * If the result differs from the previous list, it will additionally
+ * trigger a corresponding event
+ *
+ * This code should be called only in the server mode..
+ */
+ void RequestedLocalesChanged();
+ void LocalesChanged();
+
+ /**
+ * This function keeps the pref setting updated.
+ */
+ void WebExposedLocalesChanged();
+
+ /**
+ * Returns whether the locale is RTL.
+ */
+ static bool IsLocaleRTL(const nsACString& aLocale);
+
+ /**
+ * Returns whether the current app locale is RTL.
+ *
+ * This method respects this override:
+ * - `intl.l10n.pseudo`
+ */
+ bool IsAppLocaleRTL();
+
+ static bool LanguagesMatch(const nsACString& aRequested,
+ const nsACString& aAvailable);
+
+ bool IsServer();
+
+ /**
+ * Create a component from intl/components with the current app's locale. This
+ * is a convenience method for efficient string management with the app
+ * locale.
+ */
+ template <typename T, typename... Args>
+ static Result<UniquePtr<T>, ICUError> TryCreateComponent(Args... args) {
+ // 32 is somewhat arbitrary for the length, but it should fit common
+ // locales, but locales such as the following will be heap allocated:
+ //
+ // "de-u-ca-gregory-fw-mon-hc-h23-co-phonebk-ka-noignore-kb-false-kc-
+ // false-kf-false-kh-false-kk-false-kn-false-kr-space-ks-level1-kv-space-cf-
+ // standard-cu-eur-ms-metric-nu-latn-lb-strict-lw-normal-ss-none-tz-atvie-em-
+ // default-rg-atzzzz-sd-atat1-va-posix"
+ nsAutoCStringN<32> appLocale;
+ mozilla::intl::LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocale);
+
+ return T::TryCreate(appLocale.get(), args...);
+ }
+
+ /**
+ * Create a component from intl/components with a given locale, but fallback
+ * to the app locale if it doesn't work.
+ */
+ template <typename T, typename... Args>
+ static Result<UniquePtr<T>, ICUError> TryCreateComponentWithLocale(
+ const char* aLocale, Args... args) {
+ auto result = T::TryCreate(aLocale, args...);
+ if (result.isOk()) {
+ return result;
+ }
+ return TryCreateComponent<T>(args...);
+ }
+
+ private:
+ void NegotiateAppLocales(nsTArray<nsCString>& aRetVal);
+
+ void InitPackagedLocales();
+
+ void RemoveObservers();
+
+ virtual ~LocaleService() = default;
+
+ nsAutoCStringN<16> mDefaultLocale;
+ nsTArray<nsCString> mAppLocales;
+ nsTArray<nsCString> mRequestedLocales;
+ nsTArray<nsCString> mAvailableLocales;
+ nsTArray<nsCString> mPackagedLocales;
+ nsTArray<nsCString> mWebExposedLocales;
+ const bool mIsServer;
+
+ static StaticRefPtr<LocaleService> sInstance;
+};
+} // namespace intl
+} // namespace mozilla
+
+#endif /* mozilla_intl_LocaleService_h__ */
diff --git a/intl/locale/MozLocaleBindings.h b/intl/locale/MozLocaleBindings.h
new file mode 100644
index 0000000000..2b71757a44
--- /dev/null
+++ b/intl/locale/MozLocaleBindings.h
@@ -0,0 +1,25 @@
+/* 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_locale_MozLocaleBindings_h
+#define mozilla_intl_locale_MozLocaleBindings_h
+
+#include "mozilla/intl/unic_langid_ffi_generated.h"
+#include "mozilla/intl/fluent_langneg_ffi_generated.h"
+
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+template <>
+class DefaultDelete<intl::ffi::LanguageIdentifier> {
+ public:
+ void operator()(intl::ffi::LanguageIdentifier* aPtr) const {
+ unic_langid_destroy(aPtr);
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/intl/locale/OSPreferences.cpp b/intl/locale/OSPreferences.cpp
new file mode 100644
index 0000000000..b87924b61a
--- /dev/null
+++ b/intl/locale/OSPreferences.cpp
@@ -0,0 +1,585 @@
+/* -*- 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/. */
+
+/**
+ * This is a shared part of the OSPreferences API implementation.
+ * It defines helper methods and public methods that are calling
+ * platform-specific private methods.
+ */
+
+#include "OSPreferences.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/intl/DateTimePatternGenerator.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Result.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+
+using namespace mozilla::intl;
+
+NS_IMPL_ISUPPORTS(OSPreferences, mozIOSPreferences)
+
+mozilla::StaticRefPtr<OSPreferences> OSPreferences::sInstance;
+
+// Return a new strong reference to the instance, creating it if necessary.
+already_AddRefed<OSPreferences> OSPreferences::GetInstanceAddRefed() {
+ RefPtr<OSPreferences> result = sInstance;
+ if (!result) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "OSPreferences should be initialized on main thread!");
+ if (!NS_IsMainThread()) {
+ return nullptr;
+ }
+ sInstance = new OSPreferences();
+ result = sInstance;
+
+ DebugOnly<nsresult> rv = Preferences::RegisterPrefixCallback(
+ PreferenceChanged, "intl.date_time.pattern_override");
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
+
+ ClearOnShutdown(&sInstance);
+ }
+ return result.forget();
+}
+
+// Return a raw pointer to the instance: not for off-main-thread use,
+// because ClearOnShutdown means it could go away unexpectedly.
+OSPreferences* OSPreferences::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sInstance) {
+ // This will create the static instance; then we just drop the extra
+ // reference.
+ RefPtr<OSPreferences> result = GetInstanceAddRefed();
+ }
+ return sInstance;
+}
+
+void OSPreferences::Refresh() {
+ nsTArray<nsCString> newLocales;
+ ReadSystemLocales(newLocales);
+
+ if (mSystemLocales != newLocales) {
+ mSystemLocales = std::move(newLocales);
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "intl:system-locales-changed", nullptr);
+ }
+ }
+}
+
+OSPreferences::~OSPreferences() {
+ Preferences::UnregisterPrefixCallback(PreferenceChanged,
+ "intl.date_time.pattern_override");
+ RemoveObservers();
+}
+
+/*static*/
+void OSPreferences::PreferenceChanged(const char* aPrefName,
+ void* /* aClosure */) {
+ if (sInstance) {
+ sInstance->mPatternCache.Clear();
+ }
+}
+
+/**
+ * This method should be called by every method of OSPreferences that
+ * retrieves a locale id from external source.
+ *
+ * It attempts to retrieve as much of the locale ID as possible, cutting
+ * out bits that are not understood (non-strict behavior of ICU).
+ *
+ * It returns true if the canonicalization was successful.
+ */
+bool OSPreferences::CanonicalizeLanguageTag(nsCString& aLoc) {
+ return LocaleService::CanonicalizeLanguageId(aLoc);
+}
+
+/**
+ * This method retrieves from mozilla::intl the best pattern for a given
+ * date/time style.
+ */
+bool OSPreferences::GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ DateTimeFormat::StyleBag style;
+
+ switch (aTimeStyle) {
+ case DateTimeFormatStyle::Short:
+ style.time = Some(DateTimeFormat::Style::Short);
+ break;
+ case DateTimeFormatStyle::Medium:
+ style.time = Some(DateTimeFormat::Style::Medium);
+ break;
+ case DateTimeFormatStyle::Long:
+ style.time = Some(DateTimeFormat::Style::Long);
+ break;
+ case DateTimeFormatStyle::Full:
+ style.time = Some(DateTimeFormat::Style::Full);
+ break;
+ case DateTimeFormatStyle::None:
+ case DateTimeFormatStyle::Invalid:
+ // Do nothing.
+ break;
+ }
+
+ switch (aDateStyle) {
+ case DateTimeFormatStyle::Short:
+ style.date = Some(DateTimeFormat::Style::Short);
+ break;
+ case DateTimeFormatStyle::Medium:
+ style.date = Some(DateTimeFormat::Style::Medium);
+ break;
+ case DateTimeFormatStyle::Long:
+ style.date = Some(DateTimeFormat::Style::Long);
+ break;
+ case DateTimeFormatStyle::Full:
+ style.date = Some(DateTimeFormat::Style::Full);
+ break;
+ case DateTimeFormatStyle::None:
+ case DateTimeFormatStyle::Invalid:
+ // Do nothing.
+ break;
+ }
+
+ nsAutoCString locale;
+ if (aLocale.IsEmpty()) {
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
+ locale.Assign(regionalPrefsLocales[0]);
+ } else {
+ locale.Assign(aLocale);
+ }
+
+ auto genResult =
+ DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
+ if (genResult.isErr()) {
+ return false;
+ }
+ auto generator = genResult.unwrap();
+
+ auto dfResult = DateTimeFormat::TryCreateFromStyle(
+ MakeStringSpan(locale.get()), style, generator.get(), Nothing());
+ if (dfResult.isErr()) {
+ return false;
+ }
+ auto df = dfResult.unwrap();
+
+ DateTimeFormat::PatternVector pattern;
+ auto patternResult = df->GetPattern(pattern);
+ if (patternResult.isErr()) {
+ return false;
+ }
+
+ aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
+ return true;
+}
+
+/**
+ * This method retrieves from mozilla::intl the best skeleton for a given
+ * date/time style.
+ *
+ * This is useful for cases where an OS does not provide its own patterns,
+ * but provide ability to customize the skeleton, like alter hourCycle setting.
+ *
+ * The returned value is a skeleton that matches the styles.
+ */
+bool OSPreferences::GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ nsAutoCString pattern;
+ if (!GetDateTimePatternForStyle(aDateStyle, aTimeStyle, aLocale, pattern)) {
+ return false;
+ }
+
+ auto genResult =
+ DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
+ if (genResult.isErr()) {
+ return false;
+ }
+
+ nsAutoString patternAsUtf16 = NS_ConvertUTF8toUTF16(pattern);
+ DateTimeFormat::SkeletonVector skeleton;
+ auto generator = genResult.unwrap();
+ auto skeletonResult = generator->GetSkeleton(patternAsUtf16, skeleton);
+ if (skeletonResult.isErr()) {
+ return false;
+ }
+
+ aRetVal = NS_ConvertUTF16toUTF8(skeleton.begin(), skeleton.length());
+ return true;
+}
+
+/**
+ * This method checks for preferences that override the defaults
+ */
+bool OSPreferences::OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ const auto PrefToMaybeString = [](const char* pref) -> Maybe<nsAutoCString> {
+ nsAutoCString value;
+ nsresult nr = Preferences::GetCString(pref, value);
+ if (NS_FAILED(nr) || value.IsEmpty()) {
+ return Nothing();
+ }
+ return Some(std::move(value));
+ };
+
+ Maybe<nsAutoCString> timeSkeleton;
+ switch (aTimeStyle) {
+ case DateTimeFormatStyle::Short:
+ timeSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.time_short");
+ break;
+ case DateTimeFormatStyle::Medium:
+ timeSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.time_medium");
+ break;
+ case DateTimeFormatStyle::Long:
+ timeSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.time_long");
+ break;
+ case DateTimeFormatStyle::Full:
+ timeSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.time_full");
+ break;
+ default:
+ break;
+ }
+
+ Maybe<nsAutoCString> dateSkeleton;
+ switch (aDateStyle) {
+ case DateTimeFormatStyle::Short:
+ dateSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.date_short");
+ break;
+ case DateTimeFormatStyle::Medium:
+ dateSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.date_medium");
+ break;
+ case DateTimeFormatStyle::Long:
+ dateSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.date_long");
+ break;
+ case DateTimeFormatStyle::Full:
+ dateSkeleton =
+ PrefToMaybeString("intl.date_time.pattern_override.date_full");
+ break;
+ default:
+ break;
+ }
+
+ nsAutoCString locale;
+ if (aLocale.IsEmpty()) {
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
+ locale.Assign(regionalPrefsLocales[0]);
+ } else {
+ locale.Assign(aLocale);
+ }
+
+ const auto FillConnectorPattern = [&locale](
+ const nsAutoCString& datePattern,
+ const nsAutoCString& timePattern) {
+ nsAutoCString pattern;
+ GetDateTimeConnectorPattern(nsDependentCString(locale.get()), pattern);
+ int32_t index = pattern.Find("{1}");
+ if (index != kNotFound) {
+ pattern.Replace(index, 3, datePattern);
+ }
+ index = pattern.Find("{0}");
+ if (index != kNotFound) {
+ pattern.Replace(index, 3, timePattern);
+ }
+ return pattern;
+ };
+
+ if (timeSkeleton && dateSkeleton) {
+ aRetVal.Assign(FillConnectorPattern(*dateSkeleton, *timeSkeleton));
+ } else if (timeSkeleton) {
+ if (aDateStyle != DateTimeFormatStyle::None) {
+ nsAutoCString pattern;
+ if (!ReadDateTimePattern(aDateStyle, DateTimeFormatStyle::None, aLocale,
+ pattern) &&
+ !GetDateTimePatternForStyle(aDateStyle, DateTimeFormatStyle::None,
+ aLocale, pattern)) {
+ return false;
+ }
+ aRetVal.Assign(FillConnectorPattern(pattern, *timeSkeleton));
+ } else {
+ aRetVal.Assign(*timeSkeleton);
+ }
+ } else if (dateSkeleton) {
+ if (aTimeStyle != DateTimeFormatStyle::None) {
+ nsAutoCString pattern;
+ if (!ReadDateTimePattern(DateTimeFormatStyle::None, aTimeStyle, aLocale,
+ pattern) &&
+ !GetDateTimePatternForStyle(DateTimeFormatStyle::None, aTimeStyle,
+ aLocale, pattern)) {
+ return false;
+ }
+ aRetVal.Assign(FillConnectorPattern(*dateSkeleton, pattern));
+ } else {
+ aRetVal.Assign(*dateSkeleton);
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * This function is a counterpart to GetDateTimeSkeletonForStyle.
+ *
+ * It takes a skeleton and returns the best available pattern for a given locale
+ * that represents the provided skeleton.
+ *
+ * For example:
+ * "Hm" skeleton for "en-US" will return "H:m"
+ */
+bool OSPreferences::GetPatternForSkeleton(const nsACString& aSkeleton,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ aRetVal.Truncate();
+
+ auto genResult =
+ DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
+ if (genResult.isErr()) {
+ return false;
+ }
+
+ nsAutoString skeletonAsUtf16 = NS_ConvertUTF8toUTF16(aSkeleton);
+ DateTimeFormat::PatternVector pattern;
+ auto generator = genResult.unwrap();
+ auto patternResult = generator->GetBestPattern(skeletonAsUtf16, pattern);
+ if (patternResult.isErr()) {
+ return false;
+ }
+
+ aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
+ return true;
+}
+
+/**
+ * This function returns a pattern that should be used to join date and time
+ * patterns into a single date/time pattern string.
+ *
+ * It's useful for OSes that do not provide an API to retrieve such combined
+ * pattern.
+ *
+ * An example output is "{1}, {0}".
+ */
+bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale,
+ nsACString& aRetVal) {
+ // Check for a valid override pref and use that if present.
+ nsAutoCString value;
+ nsresult nr = Preferences::GetCString(
+ "intl.date_time.pattern_override.connector_short", value);
+ if (NS_SUCCEEDED(nr) && value.Find("{0}") != kNotFound &&
+ value.Find("{1}") != kNotFound) {
+ aRetVal = std::move(value);
+ return true;
+ }
+
+ auto genResult =
+ DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
+ if (genResult.isErr()) {
+ return false;
+ }
+
+ auto generator = genResult.unwrap();
+ Span<const char16_t> result = generator->GetPlaceholderPattern();
+ aRetVal = NS_ConvertUTF16toUTF8(result.data(), result.size());
+ return true;
+}
+
+/**
+ * mozIOSPreferences methods
+ */
+NS_IMETHODIMP
+OSPreferences::GetSystemLocales(nsTArray<nsCString>& aRetVal) {
+ if (!mSystemLocales.IsEmpty()) {
+ aRetVal = mSystemLocales.Clone();
+ return NS_OK;
+ }
+
+ if (ReadSystemLocales(aRetVal)) {
+ mSystemLocales = aRetVal.Clone();
+ return NS_OK;
+ }
+
+ // If we failed to get the system locale, we still need
+ // to return something because there are tests out there that
+ // depend on system locale to be set.
+ aRetVal.AppendElement("en-US"_ns);
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+OSPreferences::GetSystemLocale(nsACString& aRetVal) {
+ if (!mSystemLocales.IsEmpty()) {
+ aRetVal = mSystemLocales[0];
+ } else {
+ AutoTArray<nsCString, 10> locales;
+ GetSystemLocales(locales);
+ if (!locales.IsEmpty()) {
+ aRetVal = locales[0];
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OSPreferences::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
+ if (!mRegionalPrefsLocales.IsEmpty()) {
+ aRetVal = mRegionalPrefsLocales.Clone();
+ return NS_OK;
+ }
+
+ if (ReadRegionalPrefsLocales(aRetVal)) {
+ mRegionalPrefsLocales = aRetVal.Clone();
+ return NS_OK;
+ }
+
+ // If we failed to read regional prefs locales,
+ // use system locales as last fallback.
+ return GetSystemLocales(aRetVal);
+}
+
+static OSPreferences::DateTimeFormatStyle ToDateTimeFormatStyle(
+ int32_t aTimeFormat) {
+ switch (aTimeFormat) {
+ // See mozIOSPreferences.idl for the integer values here.
+ case 0:
+ return OSPreferences::DateTimeFormatStyle::None;
+ case 1:
+ return OSPreferences::DateTimeFormatStyle::Short;
+ case 2:
+ return OSPreferences::DateTimeFormatStyle::Medium;
+ case 3:
+ return OSPreferences::DateTimeFormatStyle::Long;
+ case 4:
+ return OSPreferences::DateTimeFormatStyle::Full;
+ }
+ return OSPreferences::DateTimeFormatStyle::Invalid;
+}
+
+NS_IMETHODIMP
+OSPreferences::GetDateTimePattern(int32_t aDateFormatStyle,
+ int32_t aTimeFormatStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ DateTimeFormatStyle dateStyle = ToDateTimeFormatStyle(aDateFormatStyle);
+ if (dateStyle == DateTimeFormatStyle::Invalid) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ DateTimeFormatStyle timeStyle = ToDateTimeFormatStyle(aTimeFormatStyle);
+ if (timeStyle == DateTimeFormatStyle::Invalid) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // If the user is asking for None on both, date and time style,
+ // let's exit early.
+ if (timeStyle == DateTimeFormatStyle::None &&
+ dateStyle == DateTimeFormatStyle::None) {
+ return NS_OK;
+ }
+
+ // If the locale is not specified, default to first regional prefs locale
+ const nsACString* locale = &aLocale;
+ AutoTArray<nsCString, 10> rpLocales;
+ if (aLocale.IsEmpty()) {
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
+ MOZ_ASSERT(rpLocales.Length() > 0);
+ locale = &rpLocales[0];
+ }
+
+ // Create a cache key from the locale + style options
+ nsAutoCString key(*locale);
+ key.Append(':');
+ key.AppendInt(aDateFormatStyle);
+ key.Append(':');
+ key.AppendInt(aTimeFormatStyle);
+
+ nsCString pattern;
+ if (mPatternCache.Get(key, &pattern)) {
+ aRetVal = pattern;
+ return NS_OK;
+ }
+
+ if (!OverrideDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
+ if (!ReadDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
+ if (!GetDateTimePatternForStyle(dateStyle, timeStyle, *locale, pattern)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ if (mPatternCache.Count() == kMaxCachedPatterns) {
+ // Don't allow unlimited cache growth; just throw it away in the case of
+ // pathological behavior where a page keeps requesting different formats
+ // and locales.
+ NS_WARNING("flushing DateTimePattern cache");
+ mPatternCache.Clear();
+ }
+ mPatternCache.InsertOrUpdate(key, pattern);
+
+ aRetVal = pattern;
+ return NS_OK;
+}
+
+void OSPreferences::OverrideSkeletonHourCycle(bool aIs24Hour,
+ nsAutoCString& aSkeleton) {
+ if (aIs24Hour) {
+ // If aSkeleton contains 'h' or 'K', replace with 'H' or 'k' respectively,
+ // and delete 'a' if present.
+ if (aSkeleton.FindChar('h') == -1 && aSkeleton.FindChar('K') == -1) {
+ return;
+ }
+ for (int32_t i = 0; i < int32_t(aSkeleton.Length()); ++i) {
+ switch (aSkeleton[i]) {
+ case 'a':
+ aSkeleton.Cut(i, 1);
+ --i;
+ break;
+ case 'h':
+ aSkeleton.SetCharAt('H', i);
+ break;
+ case 'K':
+ aSkeleton.SetCharAt('k', i);
+ break;
+ }
+ }
+ } else {
+ // If skeleton contains 'H' or 'k', replace with 'h' or 'K' respectively,
+ // and add 'a' unless already present.
+ if (aSkeleton.FindChar('H') == -1 && aSkeleton.FindChar('k') == -1) {
+ return;
+ }
+ bool foundA = false;
+ for (size_t i = 0; i < aSkeleton.Length(); ++i) {
+ switch (aSkeleton[i]) {
+ case 'a':
+ foundA = true;
+ break;
+ case 'H':
+ aSkeleton.SetCharAt('h', i);
+ break;
+ case 'k':
+ aSkeleton.SetCharAt('K', i);
+ break;
+ }
+ }
+ if (!foundA) {
+ aSkeleton.Append(char16_t('a'));
+ }
+ }
+}
diff --git a/intl/locale/OSPreferences.h b/intl/locale/OSPreferences.h
new file mode 100644
index 0000000000..e8d95f823b
--- /dev/null
+++ b/intl/locale/OSPreferences.h
@@ -0,0 +1,185 @@
+/* -*- 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/. */
+
+#ifndef mozilla_intl_IntlOSPreferences_h__
+#define mozilla_intl_IntlOSPreferences_h__
+
+#include "mozilla/StaticPtr.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+#include "mozIOSPreferences.h"
+
+namespace mozilla {
+namespace intl {
+
+/**
+ * OSPreferences API provides a set of methods for retrieving information from
+ * the host environment on topics such as:
+ * - Internationalization
+ * - Localization
+ * - Regional preferences
+ *
+ * The API is meant to remain as simple as possible, relaying information from
+ * the host environment to the user without too much logic.
+ *
+ * Saying that, there are two exceptions to that paradigm.
+ *
+ * First one is normalization. We do intend to translate host environment
+ * concepts to unified Intl/L10n vocabulary used by Mozilla.
+ * That means that we will format locale IDs, timezone names, currencies etc.
+ * into a chosen format.
+ *
+ * Second is caching. This API does cache values and where possible will
+ * hook into the environment for some event-driven cache invalidation.
+ *
+ * This means that on platforms that do not support a mechanism to
+ * notify apps about changes, new OS-level settings may not be reflected
+ * in the app until it is relaunched.
+ */
+class OSPreferences : public mozIOSPreferences {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZIOSPREFERENCES
+
+ enum class DateTimeFormatStyle {
+ Invalid = -1,
+ None,
+ Short, // e.g. time: HH:mm, date: Y/m/d
+ Medium, // likely same as Short
+ Long, // e.g. time: including seconds, date: including weekday
+ Full // e.g. time: with timezone, date: with long weekday, month
+ };
+
+ /**
+ * Constructor, to do any necessary initialization such as registering for
+ * notifications from the system when prefs are modified.
+ */
+ OSPreferences();
+
+ /**
+ * 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
+ * instance, but does not need to hold a reference, as in
+ * nsAutoCString str;
+ * OSPreferences::GetInstance()->GetSystemLocale(str);
+ *
+ * NOTE that this is not safe for off-main-thread use, because it is possible
+ * that XPCOM shutdown on the main thread could invalidate it at any moment!
+ */
+ static OSPreferences* GetInstance();
+
+ /**
+ * Return an addRef'd pointer to the singleton instance. This is used by the
+ * XPCOM constructor that exists to support usage from JS.
+ */
+ static already_AddRefed<OSPreferences> GetInstanceAddRefed();
+
+ static bool GetPatternForSkeleton(const nsACString& aSkeleton,
+ const nsACString& aLocale,
+ nsACString& aRetVal);
+
+ static bool GetDateTimeConnectorPattern(const nsACString& aLocale,
+ nsACString& aRetVal);
+
+ /**
+ * Triggers a refresh of retrieving data from host environment.
+ *
+ * If the result differs from the previous list, it will additionally
+ * trigger global events for changed values:
+ *
+ * * SystemLocales: "intl:system-locales-changed"
+ *
+ * This method should not be called from anywhere except of per-platform
+ * hooks into OS events.
+ */
+ void Refresh();
+
+ protected:
+ nsTArray<nsCString> mSystemLocales;
+ nsTArray<nsCString> mRegionalPrefsLocales;
+
+ const size_t kMaxCachedPatterns = 15;
+ nsTHashMap<nsCStringHashKey, nsCString> mPatternCache;
+
+ private:
+ virtual ~OSPreferences();
+
+ static StaticRefPtr<OSPreferences> sInstance;
+
+ static bool CanonicalizeLanguageTag(nsCString& aLoc);
+
+ /**
+ * Helper methods to get formats from ICU; these will return false
+ * in case of error, in which case the caller cannot rely on aRetVal.
+ */
+ bool GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal);
+
+ bool GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal);
+
+ bool OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale, nsACString& aRetVal);
+
+ /**
+ * This is a host environment specific method that will be implemented
+ * separately for each platform.
+ *
+ * It is only called when the cache is empty or invalidated.
+ *
+ * The return value indicates whether the function successfully
+ * resolved at least one locale.
+ */
+ bool ReadSystemLocales(nsTArray<nsCString>& aRetVal);
+
+ bool ReadRegionalPrefsLocales(nsTArray<nsCString>& aRetVal);
+
+ /**
+ * This is a host environment specific method that will be implemented
+ * separately for each platform.
+ *
+ * It is `best-effort` kind of API that attempts to construct the best
+ * possible date/time pattern for the given styles and locales.
+ *
+ * In case we fail to, or don't know how to retrieve the pattern in a
+ * given environment this function will return false.
+ * Callers should always be prepared to handle that scenario.
+ *
+ * The heuristic may depend on the OS API and HIG guidelines.
+ */
+ bool ReadDateTimePattern(DateTimeFormatStyle aDateFormatStyle,
+ DateTimeFormatStyle aTimeFormatStyle,
+ const nsACString& aLocale, nsACString& aRetVal);
+
+ /**
+ * This is called to override the hour cycle in the skeleton based upon
+ * the OS preference for AM/PM or 24 hour display.
+ */
+ void OverrideSkeletonHourCycle(bool aIs24Hour, nsAutoCString& aSkeleton);
+
+ /**
+ * This is called by the destructor to clean up any OS specific observers
+ * that are registered.
+ */
+ void RemoveObservers();
+
+ /**
+ * This is called by the destructor to clean up any OS specific observers
+ * that are registered.
+ */
+ static void PreferenceChanged(const char* aPrefName, void* /* aClosure */);
+};
+
+} // namespace intl
+} // namespace mozilla
+
+#endif /* mozilla_intl_IntlOSPreferences_h__ */
diff --git a/intl/locale/PluralForm.sys.mjs b/intl/locale/PluralForm.sys.mjs
new file mode 100644
index 0000000000..a4d0ffc4ce
--- /dev/null
+++ b/intl/locale/PluralForm.sys.mjs
@@ -0,0 +1,311 @@
+/* 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/. */
+
+/**
+ * This module provides the PluralForm object which contains a method to figure
+ * out which plural form of a word to use for a given number based on the
+ * current localization. There is also a makeGetter method that creates a get
+ * function for the desired plural rule. This is useful for extensions that
+ * specify their own plural rule instead of relying on the browser default.
+ * (I.e., the extension hasn't been localized to the browser's locale.)
+ *
+ * See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+ *
+ * NOTE: any change to these plural forms need to be reflected in
+ * compare-locales:
+ * https://hg.mozilla.org/l10n/compare-locales/file/default/compare_locales/plurals.py
+ *
+ * List of methods:
+ *
+ * string pluralForm
+ * get(int aNum, string aWords)
+ *
+ * int numForms
+ * numForms()
+ *
+ * [string pluralForm get(int aNum, string aWords), int numForms numForms()]
+ * makeGetter(int aRuleNum)
+ * Note: Basically, makeGetter returns 2 functions that do "get" and "numForm"
+ */
+
+const kIntlProperties = "chrome://global/locale/intl.properties";
+
+// These are the available plural functions that give the appropriate index
+// based on the plural rule number specified. The first element is the number
+// of plural forms and the second is the function to figure out the index.
+/* eslint-disable no-nested-ternary */
+var gFunctions = [
+ // 0: Chinese
+ [1, n => 0],
+ // 1: English
+ [2, n => (n != 1 ? 1 : 0)],
+ // 2: French
+ [2, n => (n > 1 ? 1 : 0)],
+ // 3: Latvian
+ [3, n => (n % 10 == 1 && n % 100 != 11 ? 1 : n % 10 == 0 ? 0 : 2)],
+ // 4: Scottish Gaelic
+ [
+ 4,
+ n =>
+ n == 1 || n == 11 ? 0 : n == 2 || n == 12 ? 1 : n > 0 && n < 20 ? 2 : 3,
+ ],
+ // 5: Romanian
+ [3, n => (n == 1 ? 0 : n == 0 || (n % 100 > 0 && n % 100 < 20) ? 1 : 2)],
+ // 6: Lithuanian
+ [
+ 3,
+ n =>
+ n % 10 == 1 && n % 100 != 11
+ ? 0
+ : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20)
+ ? 2
+ : 1,
+ ],
+ // 7: Russian
+ [
+ 3,
+ n =>
+ n % 10 == 1 && n % 100 != 11
+ ? 0
+ : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
+ ? 1
+ : 2,
+ ],
+ // 8: Slovak
+ [3, n => (n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2)],
+ // 9: Polish
+ [
+ 3,
+ n =>
+ n == 1
+ ? 0
+ : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
+ ? 1
+ : 2,
+ ],
+ // 10: Slovenian
+ [
+ 4,
+ n =>
+ n % 100 == 1
+ ? 0
+ : n % 100 == 2
+ ? 1
+ : n % 100 == 3 || n % 100 == 4
+ ? 2
+ : 3,
+ ],
+ // 11: Irish Gaeilge
+ [
+ 5,
+ n =>
+ n == 1
+ ? 0
+ : n == 2
+ ? 1
+ : n >= 3 && n <= 6
+ ? 2
+ : n >= 7 && n <= 10
+ ? 3
+ : 4,
+ ],
+ // 12: Arabic
+ [
+ 6,
+ n =>
+ n == 0
+ ? 5
+ : n == 1
+ ? 0
+ : n == 2
+ ? 1
+ : n % 100 >= 3 && n % 100 <= 10
+ ? 2
+ : n % 100 >= 11 && n % 100 <= 99
+ ? 3
+ : 4,
+ ],
+ // 13: Maltese
+ [
+ 4,
+ n =>
+ n == 1
+ ? 0
+ : n == 0 || (n % 100 > 0 && n % 100 <= 10)
+ ? 1
+ : n % 100 > 10 && n % 100 < 20
+ ? 2
+ : 3,
+ ],
+ // 14: Unused
+ [3, n => (n % 10 == 1 ? 0 : n % 10 == 2 ? 1 : 2)],
+ // 15: Icelandic, Macedonian
+ [2, n => (n % 10 == 1 && n % 100 != 11 ? 0 : 1)],
+ // 16: Breton
+ [
+ 5,
+ n =>
+ n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91
+ ? 0
+ : n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92
+ ? 1
+ : (n % 10 == 3 || n % 10 == 4 || n % 10 == 9) &&
+ n % 100 != 13 &&
+ n % 100 != 14 &&
+ n % 100 != 19 &&
+ n % 100 != 73 &&
+ n % 100 != 74 &&
+ n % 100 != 79 &&
+ n % 100 != 93 &&
+ n % 100 != 94 &&
+ n % 100 != 99
+ ? 2
+ : n % 1000000 == 0 && n != 0
+ ? 3
+ : 4,
+ ],
+ // 17: Shuar
+ [2, n => (n != 0 ? 1 : 0)],
+ // 18: Welsh
+ [
+ 6,
+ n => (n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n == 3 ? 3 : n == 6 ? 4 : 5),
+ ],
+ // 19: Slavic languages (bs, hr, sr). Same as rule 7, but resulting in different CLDR categories
+ [
+ 3,
+ n =>
+ n % 10 == 1 && n % 100 != 11
+ ? 0
+ : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
+ ? 1
+ : 2,
+ ],
+];
+
+/* eslint-enable no-nested-ternary */
+
+export var PluralForm = {
+ /**
+ * Get the correct plural form of a word based on the number
+ *
+ * @param aNum
+ * The number to decide which plural form to use
+ * @param aWords
+ * A semi-colon (;) separated string of words to pick the plural form
+ * @return The appropriate plural form of the word
+ */
+ get get() {
+ // This method will lazily load to avoid perf when it is first needed and
+ // creates getPluralForm function. The function it creates is based on the
+ // value of pluralRule specified in the intl stringbundle.
+ // See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+
+ // Delete the getters to be overwritten
+ delete PluralForm.numForms;
+ delete PluralForm.get;
+
+ // Make the plural form get function and set it as the default get
+ [PluralForm.get, PluralForm.numForms] = PluralForm.makeGetter(
+ PluralForm.ruleNum
+ );
+ return PluralForm.get;
+ },
+
+ /**
+ * Create a pair of plural form functions for the given plural rule number.
+ *
+ * @param aRuleNum
+ * The plural rule number to create functions
+ * @return A pair: [function that gets the right plural form,
+ * function that returns the number of plural forms]
+ */
+ makeGetter(aRuleNum) {
+ // Default to "all plural" if the value is out of bounds or invalid
+ if (aRuleNum < 0 || aRuleNum >= gFunctions.length || isNaN(aRuleNum)) {
+ log(["Invalid rule number: ", aRuleNum, " -- defaulting to 0"]);
+ aRuleNum = 0;
+ }
+
+ // Get the desired pluralRule function
+ let [numForms, pluralFunc] = gFunctions[aRuleNum];
+
+ // Return functions that give 1) the number of forms and 2) gets the right
+ // plural form
+ return [
+ function (aNum, aWords) {
+ // Figure out which index to use for the semi-colon separated words
+ let index = pluralFunc(aNum ? Number(aNum) : 0);
+ let words = aWords ? aWords.split(/;/) : [""];
+
+ // Explicitly check bounds to avoid strict warnings
+ let ret = index < words.length ? words[index] : undefined;
+
+ // Check for array out of bounds or empty strings
+ if (ret == undefined || ret == "") {
+ // Report the caller to help figure out who is causing badness
+ let caller = Components.stack.caller
+ ? Components.stack.caller.name
+ : "top";
+
+ // Display a message in the error console
+ log([
+ "Index #",
+ index,
+ " of '",
+ aWords,
+ "' for value ",
+ aNum,
+ " is invalid -- plural rule #",
+ aRuleNum,
+ "; called by ",
+ caller,
+ ]);
+
+ // Default to the first entry (which might be empty, but not undefined)
+ ret = words[0];
+ }
+
+ return ret;
+ },
+ () => numForms,
+ ];
+ },
+
+ /**
+ * Get the number of forms for the current plural rule
+ *
+ * @return The number of forms
+ */
+ get numForms() {
+ // We lazily load numForms, so trigger the init logic with get()
+ PluralForm.get();
+ return PluralForm.numForms;
+ },
+
+ /**
+ * Get the plural rule number from the intl stringbundle
+ *
+ * @return The plural rule number
+ */
+ get ruleNum() {
+ return Number(
+ Services.strings
+ .createBundle(kIntlProperties)
+ .GetStringFromName("pluralRule")
+ );
+ },
+};
+
+/**
+ * Private helper function to log errors to the error console and command line
+ *
+ * @param aMsg
+ * Error message to log or an array of strings to concat
+ */
+function log(aMsg) {
+ let msg = "PluralForm.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Services.console.logStringMessage(msg);
+ dump(msg + "\n");
+}
diff --git a/intl/locale/Quotes.cpp b/intl/locale/Quotes.cpp
new file mode 100644
index 0000000000..87c47bfa5e
--- /dev/null
+++ b/intl/locale/Quotes.cpp
@@ -0,0 +1,93 @@
+/* -*- 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 "Quotes.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/intl/Locale.h"
+#include "nsTHashMap.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+using namespace mozilla::intl;
+
+namespace {
+struct LangQuotesRec {
+ const char* mLangs;
+ Quotes mQuotes;
+};
+
+#include "cldr-quotes.inc"
+
+static StaticAutoPtr<nsTHashMap<nsCStringHashKey, Quotes>> sQuotesForLang;
+} // anonymous namespace
+
+namespace mozilla {
+namespace intl {
+
+const Quotes* QuotesForLang(const nsAtom* aLang) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // On first use, initialize the hashtable from our CLDR-derived data array.
+ if (!sQuotesForLang) {
+ sQuotesForLang = new nsTHashMap<nsCStringHashKey, Quotes>(32);
+ ClearOnShutdown(&sQuotesForLang);
+ for (const auto& i : sLangQuotes) {
+ const char* s = i.mLangs;
+ size_t len;
+ while ((len = strlen(s))) {
+ sQuotesForLang->InsertOrUpdate(nsDependentCString(s, len), i.mQuotes);
+ s += len + 1;
+ }
+ }
+ }
+
+ nsAtomCString langStr(aLang);
+ const Quotes* entry = sQuotesForLang->Lookup(langStr).DataPtrOrNull();
+ if (entry) {
+ // Found an exact match for the requested lang.
+ return entry;
+ }
+
+ // Try parsing lang as a Locale and canonicalizing the subtags, then see if
+ // we can match it with region or script subtags, if present, or just the
+ // primary language tag.
+ Locale loc;
+ auto result = LocaleParser::TryParse(langStr, loc);
+ if (result.isErr()) {
+ return nullptr;
+ }
+ if (loc.Canonicalize().isErr()) {
+ return nullptr;
+ }
+ if (loc.Region().Present()) {
+ nsAutoCString langAndRegion;
+ langAndRegion.Append(loc.Language().Span());
+ langAndRegion.Append('-');
+ langAndRegion.Append(loc.Region().Span());
+ if ((entry = sQuotesForLang->Lookup(langAndRegion).DataPtrOrNull())) {
+ return entry;
+ }
+ }
+ if (loc.Script().Present()) {
+ nsAutoCString langAndScript;
+ langAndScript.Append(loc.Language().Span());
+ langAndScript.Append('-');
+ langAndScript.Append(loc.Script().Span());
+ if ((entry = sQuotesForLang->Lookup(langAndScript).DataPtrOrNull())) {
+ return entry;
+ }
+ }
+ Span<const char> langAsSpan = loc.Language().Span();
+ nsAutoCString lang(langAsSpan.data(), langAsSpan.size());
+ if ((entry = sQuotesForLang->Lookup(lang).DataPtrOrNull())) {
+ return entry;
+ }
+
+ return nullptr;
+}
+
+} // namespace intl
+} // namespace mozilla
diff --git a/intl/locale/Quotes.h b/intl/locale/Quotes.h
new file mode 100644
index 0000000000..7bd7d277ca
--- /dev/null
+++ b/intl/locale/Quotes.h
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+#ifndef mozilla_intl_Quotes_h__
+#define mozilla_intl_Quotes_h__
+
+#include "nsAtom.h"
+
+namespace mozilla {
+namespace intl {
+
+// Currently, all the quotation characters provided by CLDR are single BMP
+// codepoints, so they fit into char16_t fields. If there are ever multi-
+// character strings or non-BMP codepoints in a future version, we'll need
+// to extend this to a larger/more flexible structure, but for now it's
+// deliberately kept simple and lightweight.
+struct Quotes {
+ // Entries in order [open, close, alternativeOpen, alternativeClose]
+ char16_t mChars[4];
+};
+
+/**
+ * Return a pointer to the Quotes record for the given locale (lang attribute),
+ * or nullptr if none available.
+ * The returned value points to a hashtable entry, but will remain valid until
+ * shutdown begins, as the table is not modified after initialization.
+ */
+const Quotes* QuotesForLang(const nsAtom* aLang);
+
+} // namespace intl
+} // namespace mozilla
+
+#endif // mozilla_intl_Quotes_h__
diff --git a/intl/locale/android/OSPreferences_android.cpp b/intl/locale/android/OSPreferences_android.cpp
new file mode 100644
index 0000000000..8ef27c526b
--- /dev/null
+++ b/intl/locale/android/OSPreferences_android.cpp
@@ -0,0 +1,69 @@
+/* -*- 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 "OSPreferences.h"
+#include "mozilla/Preferences.h"
+
+#include "mozilla/java/GeckoAppShellWrappers.h"
+
+using namespace mozilla::intl;
+
+OSPreferences::OSPreferences() {}
+
+bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
+ if (!mozilla::jni::IsAvailable()) {
+ return false;
+ }
+
+ // XXX: Notice, this value may be empty on an early read. In that case
+ // we won't add anything to the return list so that it doesn't get
+ // cached in mSystemLocales.
+ auto locales = java::GeckoAppShell::GetDefaultLocales();
+ if (locales) {
+ for (size_t i = 0; i < locales->Length(); i++) {
+ jni::String::LocalRef locale = locales->GetElement(i);
+ aLocaleList.AppendElement(locale->ToCString());
+ }
+ return true;
+ }
+ return false;
+}
+
+bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
+ // For now we're just taking System Locales since we don't know of any better
+ // API for regional prefs.
+ return ReadSystemLocales(aLocaleList);
+}
+
+/*
+ * Similar to Gtk, Android does not provide a way to customize or format
+ * date/time patterns, so we're reusing ICU data here, but we do modify it
+ * according to the Android DateFormat is24HourFormat setting.
+ */
+bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ if (!mozilla::jni::IsAvailable()) {
+ return false;
+ }
+
+ nsAutoCString skeleton;
+ if (!GetDateTimeSkeletonForStyle(aDateStyle, aTimeStyle, aLocale, skeleton)) {
+ return false;
+ }
+
+ // Customize the skeleton if necessary to reflect user's 12/24hr pref
+ OverrideSkeletonHourCycle(java::GeckoAppShell::GetIs24HourFormat(), skeleton);
+
+ if (!GetPatternForSkeleton(skeleton, aLocale, aRetVal)) {
+ return false;
+ }
+
+ return true;
+}
+
+void OSPreferences::RemoveObservers() {}
diff --git a/intl/locale/android/moz.build b/intl/locale/android/moz.build
new file mode 100644
index 0000000000..b0cb0cc6a5
--- /dev/null
+++ b/intl/locale/android/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += ["OSPreferences_android.cpp"]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "..",
+]
diff --git a/intl/locale/cldr-quotes.inc b/intl/locale/cldr-quotes.inc
new file mode 100644
index 0000000000..84b5569308
--- /dev/null
+++ b/intl/locale/cldr-quotes.inc
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/*
+ * Derived from the Unicode Common Locale Data Repository by cldr-quotes.pl.
+ *
+ * For terms of use, see http://www.unicode.org/copyright.html.
+ */
+
+/*
+ * Created on Wed Oct 26 05:55:58 2022 from CLDR data file cldr-common-42.0.zip.
+ *
+ * * * * * This file contains MACHINE-GENERATED DATA, do not edit! * * * * *
+ *
+ * (generated by intl/locale/cldr-quotes.pl)
+ */
+
+static const LangQuotesRec sLangQuotes[] = {
+ // clang-format off
+ { "af\0ak\0as\0asa\0az\0bem\0bez\0bn\0brx\0ccp\0ceb\0cgg\0chr\0cy\0da\0dav\0dje\0doi\0dz\0ebu\0ee\0en\0es-419\0ff-Adlm\0fil\0fo\0gd\0gl\0gu\0guz\0hi\0id\0jmc\0jv\0kam\0kde\0kea\0khq\0ki\0kln\0km\0kn\0ko\0kok\0ksb\0ku\0lg\0ln\0lo\0lrc\0lu\0luo\0lv\0mas\0mer\0mfe\0mgo\0mi\0ml\0mn\0mr\0ms\0mt\0my\0naq\0nd\0ne\0nus\0nyn\0or\0pa\0pcm\0pis\0rof\0rwk\0saq\0sat\0sbp\0sd\0seh\0ses\0si\0sw\0ta\0te\0teo\0th\0to\0tr\0tt\0twq\0tzm\0uz-Cyrl\0vai\0vi\0vun\0wo\0xh\0xog\0yo\0yue-Hans\0zh\0zu\0", { { 0x201c, 0x201d, 0x2018, 0x2019 } } },
+ { "agq\0ff\0", { { 0x201e, 0x201d, 0x201a, 0x2019 } } },
+ { "am\0az-Cyrl\0fa\0fr-CH\0gsw\0jgo\0kkj\0mzn\0", { { 0xab, 0xbb, 0x2039, 0x203a } } },
+ { "ar\0ur\0", { { 0x201d, 0x201c, 0x2019, 0x2018 } } },
+ { "ast\0bm\0br\0ca\0dyo\0el\0es\0eu\0ewo\0it\0kab\0kk\0mg\0mua\0nnh\0pt-PT\0sc\0sg\0sq\0ti\0", { { 0xab, 0xbb, 0x201c, 0x201d } } },
+ { "bas\0be\0cv\0ky\0ru\0sah\0uk\0", { { 0xab, 0xbb, 0x201e, 0x201c } } },
+ { "bg\0lt\0", { { 0x201e, 0x201c, 0x201e, 0x201c } } },
+ { "bs-Cyrl\0cs\0de\0dsb\0et\0hr\0hsb\0is\0lb\0luy\0mk\0sk\0sl\0", { { 0x201e, 0x201c, 0x201a, 0x2018 } } },
+ { "bs\0", { { 0x201e, 0x201d, 0x2018, 0x2019 } } },
+ { "dua\0ksf\0no\0rw\0", { { 0xab, 0xbb, 0x2018, 0x2019 } } },
+ { "fi\0he\0lag\0rn\0sn\0sv\0", { { 0x201d, 0x201d, 0x2019, 0x2019 } } },
+ { "fr-CA\0", { { 0xab, 0xbb, 0x201d, 0x201c } } },
+ { "fr\0hy\0yav\0", { { 0xab, 0xbb, 0xab, 0xbb } } },
+ { "hu\0", { { 0x201e, 0x201d, 0xbb, 0xab } } },
+ { "ia\0ti-ER\0", { { 0x2018, 0x2019, 0x201c, 0x201d } } },
+ { "ja\0yue\0zh-Hant\0", { { 0x300c, 0x300d, 0x300e, 0x300f } } },
+ { "ka\0", { { 0x201e, 0x201c, 0xab, 0xbb } } },
+ { "nl\0", { { 0x2018, 0x2019, 0x2018, 0x2019 } } },
+ { "nmg\0pl\0ro\0", { { 0x201e, 0x201d, 0xab, 0xbb } } },
+ { "shi\0zgh\0", { { 0xab, 0xbb, 0x201e, 0x201d } } },
+ { "sr\0", { { 0x201e, 0x201c, 0x2018, 0x2018 } } },
+ { "tk\0", { { 0x201c, 0x201d, 0x201c, 0x201d } } },
+ { "uz\0", { { 0x201c, 0x201d, 0x2019, 0x2018 } } },
+ // clang-format on
+};
diff --git a/intl/locale/cldr-quotes.pl b/intl/locale/cldr-quotes.pl
new file mode 100644
index 0000000000..76b91fd5a4
--- /dev/null
+++ b/intl/locale/cldr-quotes.pl
@@ -0,0 +1,108 @@
+# 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/.
+
+# Tool to generate the cldr-quotes.inc file, to be #include'd in Quotes.cpp
+# to provide locale-appropriate opening and closing quote marks.
+
+# To regenerate cldr-quotes.inc for a new CLDR release, download the data file
+# "cldr-common-##.zip" from http://unicode.org/Public/cldr/latest into the
+# current directory, run
+#
+# perl cldr-quotes.pl <filename> > cldr-quotes.inc
+#
+# (where <filename> is the downloaded cldr-common-## archive), and
+# then use `hg diff` to check that the result looks sane.
+
+use warnings;
+use strict;
+
+use Encode;
+use IO::Uncompress::Unzip "unzip";
+
+die "Usage: perl cldr-quotes.pl <filename>" unless $#ARGV == 0;
+
+my $filename = $ARGV[0];
+
+my (%langQuotes, %quoteLangs);
+
+my $zip = IO::Uncompress::Unzip->new($filename) ||
+ die "unzip failed: $IO::Uncompress::Unzip::UnzipError\n";
+
+my $status = 1;
+while ($status > 0) {
+ my $name = $zip->getHeaderInfo()->{Name};
+ if ($name =~ m@common/main/([A-Za-z0-9_]+)\.xml@) {
+ my $lang = $1;
+ $lang =~ s/_/-/;
+ while (<$zip>) {
+ $langQuotes{$lang}[0] = $1 if (m!<quotationStart>(.+)<!);
+ $langQuotes{$lang}[1] = $1 if (m!<quotationEnd>(.+)<!);
+ $langQuotes{$lang}[2] = $1 if (m!<alternateQuotationStart>(.+)<!);
+ $langQuotes{$lang}[3] = $1 if (m!<alternateQuotationEnd>(.+)<!);
+ }
+ }
+ $status = $zip->nextStream();
+}
+$zip->close;
+
+foreach my $lang (sort keys %langQuotes) {
+ # We don't actually want to emit anything for the root locale
+ next if $lang eq "root";
+
+ # Inherit any missing entries from the locale's parent
+ my $parent = $lang;
+ while ($parent =~ m/\-/) {
+ # Strip off a trailing subtag to find a parent locale code
+ $parent =~ s/\-[^-]+$//;
+ # Fill in any values available from the parent
+ for (my $i = 0; $i < 4; $i++) {
+ $langQuotes{$lang}[$i] = $langQuotes{$parent}[$i] unless $langQuotes{$lang}[$i];
+ }
+ }
+
+ # Anything still missing is copied from the root locale
+ for (my $i = 0; $i < 4; $i++) {
+ $langQuotes{$lang}[$i] = $langQuotes{"root"}[$i] unless $langQuotes{$lang}[$i];
+ }
+
+ # If the locale ends up the same as its parent, skip
+ next if ($parent ne $lang) && (exists $langQuotes{$parent}) &&
+ (join(",", @{$langQuotes{$lang}}) eq join(",", @{$langQuotes{$parent}}));
+
+ # Create a string with the C source form for the array of 4 quote characters
+ my $quoteChars = join(", ", map { sprintf("0x%x", ord Encode::decode("UTF-8", $_)) } @{$langQuotes{$lang}});
+
+ # Record this locale in the list of those which use this particular set of quotes
+ $quoteLangs{$quoteChars} = [] unless exists $quoteLangs{$quoteChars};
+ push @{$quoteLangs{$quoteChars}}, $lang;
+}
+
+# Output each unique list of quotes, with the string of associated locales
+my $timestamp = gmtime();
+print <<__EOT__;
+/* 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/. */
+
+/*
+ * Derived from the Unicode Common Locale Data Repository by cldr-quotes.pl.
+ *
+ * For terms of use, see http://www.unicode.org/copyright.html.
+ */
+
+/*
+ * Created on $timestamp from CLDR data file $filename.
+ *
+ * * * * * This file contains MACHINE-GENERATED DATA, do not edit! * * * * *
+ *
+ * (generated by intl/locale/cldr-quotes.pl)
+ */
+
+__EOT__
+
+print "static const LangQuotesRec sLangQuotes[] = {\n";
+print " // clang-format off\n";
+print sort map { sprintf(" { \"%s\\0\", { { %s } } },\n", join("\\0", sort @{$quoteLangs{$_}}), $_) } (keys %quoteLangs);
+print " // clang-format on\n";
+print "};\n";
diff --git a/intl/locale/components.conf b/intl/locale/components.conf
new file mode 100644
index 0000000000..562674108f
--- /dev/null
+++ b/intl/locale/components.conf
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'js_name': 'locale',
+ 'cid': '{92735ff4-6384-4ad6-8508-757010e149ee}',
+ 'contract_ids': ['@mozilla.org/intl/localeservice;1'],
+ 'interfaces': ['mozILocaleService'],
+ 'singleton': True,
+ 'type': 'mozilla::intl::LocaleService',
+ 'headers': ['mozilla/intl/LocaleService.h'],
+ 'constructor': 'mozilla::intl::LocaleService::GetInstanceAddRefed',
+ },
+ {
+ 'cid': '{65944815-e9ae-48bd-a2bf-f1108720950c}',
+ 'contract_ids': ['@mozilla.org/intl/ospreferences;1'],
+ 'singleton': True,
+ 'type': 'mozilla::intl::OSPreferences',
+ 'headers': ['mozilla/intl/OSPreferences.h'],
+ 'constructor': 'mozilla::intl::OSPreferences::GetInstanceAddRefed',
+ },
+]
diff --git a/intl/locale/encodingsgroups.properties b/intl/locale/encodingsgroups.properties
new file mode 100644
index 0000000000..f631bfa64f
--- /dev/null
+++ b/intl/locale/encodingsgroups.properties
@@ -0,0 +1,40 @@
+# 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/.
+
+# x-unicode is assumed for encodings not listed here
+
+Big5=zh-TW
+EUC-JP=ja
+EUC-KR=ko
+gb18030=zh-CN
+GBK=zh-CN
+IBM866=x-cyrillic
+ISO-2022-JP=ja
+ISO-8859-3=x-western
+ISO-8859-4=x-western
+ISO-8859-5=x-cyrillic
+ISO-8859-6=ar
+ISO-8859-7=el
+ISO-8859-8=he
+ISO-8859-8-I=he
+ISO-8859-10=x-western
+ISO-8859-13=x-western
+ISO-8859-14=x-western
+ISO-8859-15=x-western
+ISO-8859-16=x-western
+ISO-8859-2=x-western
+KOI8-R=x-cyrillic
+KOI8-U=x-cyrillic
+Shift_JIS=ja
+windows-1250=x-western
+windows-1251=x-cyrillic
+windows-1252=x-western
+windows-1253=el
+windows-1254=x-western
+windows-1255=he
+windows-1256=ar
+windows-1257=x-western
+windows-1258=x-western
+windows-874=th
+x-mac-cyrillic=x-cyrillic
diff --git a/intl/locale/gtk/OSPreferences_gtk.cpp b/intl/locale/gtk/OSPreferences_gtk.cpp
new file mode 100644
index 0000000000..e7e5d6017d
--- /dev/null
+++ b/intl/locale/gtk/OSPreferences_gtk.cpp
@@ -0,0 +1,120 @@
+/* -*- 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 <locale.h>
+#include "mozilla/intl/Locale.h"
+#include "OSPreferences.h"
+
+#include "nsServiceManagerUtils.h"
+#include "nsIGSettingsService.h"
+
+using namespace mozilla;
+using namespace mozilla::intl;
+
+OSPreferences::OSPreferences() = default;
+
+bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
+ MOZ_ASSERT(aLocaleList.IsEmpty());
+
+ nsAutoCString defaultLang(Locale::GetDefaultLocale());
+
+ if (CanonicalizeLanguageTag(defaultLang)) {
+ aLocaleList.AppendElement(defaultLang);
+ return true;
+ }
+ return false;
+}
+
+bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
+ MOZ_ASSERT(aLocaleList.IsEmpty());
+
+ // For now we're just taking the LC_TIME from POSIX environment for all
+ // regional preferences.
+ nsAutoCString localeStr(setlocale(LC_TIME, nullptr));
+
+ if (CanonicalizeLanguageTag(localeStr)) {
+ aLocaleList.AppendElement(localeStr);
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * This looks up into gtk settings for hourCycle format.
+ *
+ * This works for all GUIs that use gtk settings like Gnome, Elementary etc.
+ *
+ * We're taking the current 12/24h settings irrelevant of the locale, because
+ * in the UI user selects this setting for all locales.
+ */
+static int HourCycle() {
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (!gsettings) {
+ return 0;
+ }
+
+ nsCOMPtr<nsIGSettingsCollection> desktop_settings;
+ gsettings->GetCollectionForSchema("org.gnome.desktop.interface"_ns,
+ getter_AddRefs(desktop_settings));
+ if (!desktop_settings) {
+ return 0;
+ }
+
+ nsAutoCString result;
+ desktop_settings->GetString("clock-format"_ns, result);
+ if (result == "12h") {
+ return 12;
+ }
+ if (result == "24h") {
+ return 24;
+ }
+ return 0;
+}
+
+/**
+ * Since Gtk does not provide a way to customize or format date/time patterns,
+ * we're reusing ICU data here, but we do modify it according to the only
+ * setting Gtk gives us - hourCycle.
+ *
+ * This means that for gtk we will return a pattern from ICU altered to
+ * represent h12/h24 hour cycle if the user modified the default value.
+ *
+ * In short, this should work like this:
+ *
+ * * gtk defaults, pl: 24h
+ * * gtk defaults, en: 12h
+ *
+ * * gtk 12h, pl: 12h
+ * * gtk 12h, en: 12h
+ *
+ * * gtk 24h, pl: 24h
+ * * gtk 12h, en: 12h
+ */
+bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ nsAutoCString skeleton;
+ if (!GetDateTimeSkeletonForStyle(aDateStyle, aTimeStyle, aLocale, skeleton)) {
+ return false;
+ }
+
+ // Customize the skeleton if necessary to reflect user's 12/24hr pref
+ int hourCycle = HourCycle();
+ if (hourCycle == 12 || hourCycle == 24) {
+ OverrideSkeletonHourCycle(hourCycle == 24, skeleton);
+ }
+
+ if (!GetPatternForSkeleton(skeleton, aLocale, aRetVal)) {
+ return false;
+ }
+
+ return true;
+}
+
+void OSPreferences::RemoveObservers() {}
diff --git a/intl/locale/gtk/moz.build b/intl/locale/gtk/moz.build
new file mode 100644
index 0000000000..3b977a9720
--- /dev/null
+++ b/intl/locale/gtk/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += ["OSPreferences_gtk.cpp"]
+
+CXXFLAGS += CONFIG["GLIB_CFLAGS"]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "..",
+]
diff --git a/intl/locale/language.properties b/intl/locale/language.properties
new file mode 100644
index 0000000000..b62bafd5c0
--- /dev/null
+++ b/intl/locale/language.properties
@@ -0,0 +1,289 @@
+# 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/.
+
+aa.accept = true
+ab.accept = true
+ach.accept = true
+ae.accept = true
+af.accept = true
+ak.accept = true
+am.accept = true
+an.accept = true
+ar.accept = true
+ar-ae.accept = true
+ar-bh.accept = true
+ar-dz.accept = true
+ar-eg.accept = true
+ar-iq.accept = true
+ar-jo.accept = true
+ar-kw.accept = true
+ar-lb.accept = true
+ar-ly.accept = true
+ar-ma.accept = true
+ar-om.accept = true
+ar-qa.accept = true
+ar-sa.accept = true
+ar-sy.accept = true
+ar-tn.accept = true
+ar-ye.accept = true
+as.accept = true
+ast.accept = true
+av.accept = true
+ay.accept = true
+az.accept = true
+ba.accept = true
+be.accept = true
+bg.accept = true
+bh.accept = true
+bi.accept = true
+bm.accept = true
+bn.accept = true
+bo.accept = true
+br.accept = true
+bs.accept = true
+ca.accept = true
+ca-valencia.accept = true
+cak.accept = true
+ce.accept = true
+ch.accept = true
+co.accept = true
+cr.accept = true
+crh.accept = true
+cs.accept = true
+csb.accept = true
+cu.accept = true
+cv.accept = true
+cy.accept = true
+da.accept = true
+de.accept = true
+de-at.accept = true
+de-ch.accept = true
+de-de.accept = true
+de-li.accept = true
+de-lu.accept = true
+dsb.accept = true
+dv.accept = true
+dz.accept = true
+ee.accept = true
+el.accept = true
+en.accept = true
+en-au.accept = true
+en-bz.accept = true
+en-ca.accept = true
+en-gb.accept = true
+en-ie.accept = true
+en-jm.accept = true
+en-nz.accept = true
+en-ph.accept = true
+en-tt.accept = true
+en-us.accept = true
+en-za.accept = true
+en-zw.accept = true
+eo.accept = true
+es.accept = true
+es-ar.accept = true
+es-bo.accept = true
+es-cl.accept = true
+es-co.accept = true
+es-cr.accept = true
+es-do.accept = true
+es-ec.accept = true
+es-es.accept = true
+es-gt.accept = true
+es-hn.accept = true
+es-mx.accept = true
+es-ni.accept = true
+es-pa.accept = true
+es-pe.accept = true
+es-pr.accept = true
+es-py.accept = true
+es-sv.accept = true
+es-uy.accept = true
+es-ve.accept = true
+et.accept = true
+eu.accept = true
+fa.accept = true
+fa-ir.accept = true
+ff.accept = true
+fi.accept = true
+fj.accept = true
+fo.accept = true
+fr.accept = true
+fr-be.accept = true
+fr-ca.accept = true
+fr-ch.accept = true
+fr-fr.accept = true
+fr-lu.accept = true
+fr-mc.accept = true
+fur.accept = true
+fy.accept = true
+ga.accept = true
+gd.accept = true
+gl.accept = true
+gn.accept = true
+gu.accept = true
+gv.accept = true
+ha.accept = true
+haw.accept = true
+he.accept = true
+hi.accept = true
+hil.accept = true
+ho.accept = true
+hr.accept = true
+hsb.accept = true
+ht.accept = true
+hu.accept = true
+hy.accept = true
+hz.accept = true
+ia.accept = true
+id.accept = true
+ie.accept = true
+ig.accept = true
+ii.accept = true
+ik.accept = true
+io.accept = true
+is.accept = true
+it.accept = true
+it-ch.accept = true
+iu.accept = true
+ja.accept = true
+jv.accept = true
+ka.accept = true
+kab.accept = true
+kg.accept = true
+ki.accept = true
+kk.accept = true
+kl.accept = true
+km.accept = true
+kn.accept = true
+ko.accept = true
+ko-kp.accept = true
+ko-kr.accept = true
+kok.accept = true
+kr.accept = true
+ks.accept = true
+ku.accept = true
+kv.accept = true
+kw.accept = true
+ky.accept = true
+la.accept = true
+lb.accept = true
+lg.accept = true
+li.accept = true
+lij.accept = true
+ln.accept = true
+lo.accept = true
+lt.accept = true
+ltg.accept = true
+lu.accept = true
+lv.accept = true
+mai.accept = true
+meh.accept = true
+mg.accept = true
+mh.accept = true
+mi.accept = true
+mix.accept = true
+mk.accept = true
+mk-mk.accept = true
+ml.accept = true
+mn.accept = true
+mr.accept = true
+ms.accept = true
+mt.accept = true
+my.accept = true
+na.accept = true
+nb.accept = true
+nd.accept = true
+ne.accept = true
+ng.accept = true
+nl.accept = true
+nl-be.accept = true
+nn.accept = true
+no.accept = true
+nr.accept = true
+nso.accept = true
+nv.accept = true
+ny.accept = true
+oc.accept = true
+oj.accept = true
+om.accept = true
+or.accept = true
+os.accept = true
+pa.accept = true
+pa-in.accept = true
+pa-pk.accept = true
+pi.accept = true
+pl.accept = true
+ps.accept = true
+pt.accept = true
+pt-br.accept = true
+pt-pt.accept = true
+qu.accept = true
+rm.accept = true
+rn.accept = true
+ro.accept = true
+ro-md.accept = true
+ro-ro.accept = true
+ru.accept = true
+ru-md.accept = true
+rw.accept = true
+sa.accept = true
+sc.accept = true
+sco.accept = true
+sd.accept = true
+sg.accept = true
+si.accept = true
+sk.accept = true
+sl.accept = true
+sm.accept = true
+so.accept = true
+son.accept = true
+son-ml.accept = true
+sq.accept = true
+sr.accept = true
+ss.accept = true
+st.accept = true
+su.accept = true
+sv.accept = true
+sv-fi.accept = true
+sv-se.accept = true
+sw.accept = true
+szl.accept = true
+ta.accept = true
+te.accept = true
+tg.accept = true
+th.accept = true
+ti.accept = true
+tig.accept = true
+tk.accept = true
+tl.accept = true
+tlh.accept = true
+tn.accept = true
+to.accept = true
+tr.accept = true
+trs.accept = true
+ts.accept = true
+tt.accept = true
+tw.accept = true
+ty.accept = true
+ug.accept = true
+uk.accept = true
+ur.accept = true
+uz.accept = true
+ve.accept = true
+vi.accept = true
+vo.accept = true
+wa.accept = true
+wo.accept = true
+xh.accept = true
+yi.accept = true
+yo.accept = true
+za.accept = true
+zam.accept = true
+zh.accept = true
+zh-cn.accept = true
+zh-hk.accept = true
+zh-sg.accept = true
+zh-tw.accept = true
+zu.accept = true
diff --git a/intl/locale/mac/OSPreferences_mac.cpp b/intl/locale/mac/OSPreferences_mac.cpp
new file mode 100644
index 0000000000..f8cd910b40
--- /dev/null
+++ b/intl/locale/mac/OSPreferences_mac.cpp
@@ -0,0 +1,158 @@
+/* -*- 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 "OSPreferences.h"
+#include "mozilla/intl/LocaleService.h"
+#include <Carbon/Carbon.h>
+
+using namespace mozilla::intl;
+
+static void LocaleChangedNotificationCallback(CFNotificationCenterRef center,
+ void* observer, CFStringRef name,
+ const void* object,
+ CFDictionaryRef userInfo) {
+ if (!::CFEqual(name, kCFLocaleCurrentLocaleDidChangeNotification)) {
+ return;
+ }
+ static_cast<OSPreferences*>(observer)->Refresh();
+}
+
+OSPreferences::OSPreferences() {
+ ::CFNotificationCenterAddObserver(
+ ::CFNotificationCenterGetLocalCenter(), this,
+ LocaleChangedNotificationCallback,
+ kCFLocaleCurrentLocaleDidChangeNotification, 0,
+ CFNotificationSuspensionBehaviorDeliverImmediately);
+}
+
+bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
+ MOZ_ASSERT(aLocaleList.IsEmpty());
+
+ CFArrayRef langs = ::CFLocaleCopyPreferredLanguages();
+ for (CFIndex i = 0; i < ::CFArrayGetCount(langs); i++) {
+ CFStringRef lang = (CFStringRef)::CFArrayGetValueAtIndex(langs, i);
+
+ AutoTArray<UniChar, 32> buffer;
+ int size = ::CFStringGetLength(lang);
+ buffer.SetLength(size);
+
+ CFRange range = ::CFRangeMake(0, size);
+ ::CFStringGetCharacters(lang, range, buffer.Elements());
+
+ // Convert the locale string to the format that Mozilla expects
+ NS_LossyConvertUTF16toASCII locale(
+ reinterpret_cast<const char16_t*>(buffer.Elements()), buffer.Length());
+
+ if (CanonicalizeLanguageTag(locale)) {
+ aLocaleList.AppendElement(locale);
+ }
+ }
+
+ ::CFRelease(langs);
+
+ return !aLocaleList.IsEmpty();
+}
+
+bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
+ // For now we're just taking System Locales since we don't know of any better
+ // API for regional prefs.
+ return ReadSystemLocales(aLocaleList);
+}
+
+static CFDateFormatterStyle ToCFDateFormatterStyle(
+ OSPreferences::DateTimeFormatStyle aFormatStyle) {
+ switch (aFormatStyle) {
+ case OSPreferences::DateTimeFormatStyle::None:
+ return kCFDateFormatterNoStyle;
+ case OSPreferences::DateTimeFormatStyle::Short:
+ return kCFDateFormatterShortStyle;
+ case OSPreferences::DateTimeFormatStyle::Medium:
+ return kCFDateFormatterMediumStyle;
+ case OSPreferences::DateTimeFormatStyle::Long:
+ return kCFDateFormatterLongStyle;
+ case OSPreferences::DateTimeFormatStyle::Full:
+ return kCFDateFormatterFullStyle;
+ case OSPreferences::DateTimeFormatStyle::Invalid:
+ MOZ_ASSERT_UNREACHABLE("invalid time format");
+ return kCFDateFormatterNoStyle;
+ }
+}
+
+// Given an 8-bit Gecko string, create a corresponding CFLocale;
+// if aLocale is empty, returns a copy of the system's current locale.
+// May return null on failure.
+// Follows Core Foundation's Create rule, so the caller is responsible to
+// release the returned reference.
+static CFLocaleRef CreateCFLocaleFor(const nsACString& aLocale) {
+ nsAutoCString reqLocale;
+ nsAutoCString systemLocale;
+
+ OSPreferences::GetInstance()->GetSystemLocale(systemLocale);
+
+ if (aLocale.IsEmpty()) {
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(reqLocale);
+ } else {
+ reqLocale.Assign(aLocale);
+ }
+
+ bool match = LocaleService::LanguagesMatch(reqLocale, systemLocale);
+ if (match) {
+ return ::CFLocaleCopyCurrent();
+ }
+
+ CFStringRef identifier = CFStringCreateWithBytesNoCopy(
+ kCFAllocatorDefault, (const uint8_t*)reqLocale.BeginReading(),
+ reqLocale.Length(), kCFStringEncodingASCII, false, kCFAllocatorNull);
+ if (!identifier) {
+ return nullptr;
+ }
+ CFLocaleRef locale = CFLocaleCreate(kCFAllocatorDefault, identifier);
+ CFRelease(identifier);
+ return locale;
+}
+
+/**
+ * Cocoa API maps nicely to our four styles of date/time.
+ *
+ * The only caveat is that Cocoa takes regional preferences modifications
+ * into account only when we pass an empty string as a locale.
+ *
+ * In all other cases it will return the default pattern for a given locale.
+ */
+bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ CFLocaleRef locale = CreateCFLocaleFor(aLocale);
+ if (!locale) {
+ return false;
+ }
+
+ CFDateFormatterRef formatter = CFDateFormatterCreate(
+ kCFAllocatorDefault, locale, ToCFDateFormatterStyle(aDateStyle),
+ ToCFDateFormatterStyle(aTimeStyle));
+ if (!formatter) {
+ return false;
+ }
+ CFStringRef format = CFDateFormatterGetFormat(formatter);
+ CFRelease(locale);
+
+ CFRange range = CFRangeMake(0, CFStringGetLength(format));
+ nsAutoString str;
+ str.SetLength(range.length);
+ CFStringGetCharacters(format, range,
+ reinterpret_cast<UniChar*>(str.BeginWriting()));
+ CFRelease(formatter);
+
+ aRetVal = NS_ConvertUTF16toUTF8(str);
+ return true;
+}
+
+void OSPreferences::RemoveObservers() {
+ ::CFNotificationCenterRemoveObserver(
+ ::CFNotificationCenterGetLocalCenter(), this,
+ kCTFontManagerRegisteredFontsChangedNotification, 0);
+}
diff --git a/intl/locale/mac/moz.build b/intl/locale/mac/moz.build
new file mode 100644
index 0000000000..68eb6d8d35
--- /dev/null
+++ b/intl/locale/mac/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += ["OSPreferences_mac.cpp"]
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "..",
+]
diff --git a/intl/locale/moz.build b/intl/locale/moz.build
new file mode 100644
index 0000000000..52d1a2fe45
--- /dev/null
+++ b/intl/locale/moz.build
@@ -0,0 +1,98 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
+
+TESTING_JS_MODULES += [
+ "tests/LangPackMatcherTestUtils.sys.mjs",
+]
+
+toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+if toolkit == "windows":
+ DIRS += ["windows"]
+elif toolkit == "cocoa":
+ DIRS += ["mac"]
+elif toolkit == "gtk":
+ DIRS += ["gtk"]
+elif toolkit == "android":
+ DIRS += ["android"]
+
+XPIDL_SOURCES += [
+ "mozILocaleService.idl",
+ "mozIOSPreferences.idl",
+]
+
+XPIDL_MODULE = "locale"
+
+EXPORTS += [
+ "nsLanguageAtomService.h",
+ "nsUConvPropertySearch.h",
+]
+
+EXPORTS.mozilla.intl += [
+ "AppDateTimeFormat.h",
+ "LocaleService.h",
+ "MozLocaleBindings.h",
+ "OSPreferences.h",
+ "Quotes.h",
+]
+
+UNIFIED_SOURCES += [
+ "AppDateTimeFormat.cpp",
+ "LocaleService.cpp",
+ "nsLanguageAtomService.cpp",
+ "nsUConvPropertySearch.cpp",
+ "OSPreferences.cpp",
+ "Quotes.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "LangPackMatcher.sys.mjs",
+ "PluralForm.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/intl/uconv",
+]
+
+RESOURCE_FILES += [
+ "language.properties",
+]
+
+prefixes = ("encodingsgroups",)
+
+for prefix in prefixes:
+ input_file = prefix + ".properties"
+ header = prefix + ".properties.h"
+ GeneratedFile(header, script="props2arrays.py", inputs=[input_file])
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ CbindgenHeader(
+ "fluent_langneg_ffi_generated.h",
+ inputs=["/intl/locale/rust/fluent-langneg-ffi"],
+ )
+ CbindgenHeader(
+ "oxilangtag_ffi_generated.h", inputs=["/intl/locale/rust/oxilangtag-ffi"]
+ )
+ CbindgenHeader(
+ "unic_langid_ffi_generated.h", inputs=["/intl/locale/rust/unic-langid-ffi"]
+ )
+
+ EXPORTS.mozilla.intl += [
+ "!fluent_langneg_ffi_generated.h",
+ "!oxilangtag_ffi_generated.h",
+ "!unic_langid_ffi_generated.h",
+ ]
diff --git a/intl/locale/mozILocaleService.idl b/intl/locale/mozILocaleService.idl
new file mode 100644
index 0000000000..71227a64d3
--- /dev/null
+++ b/intl/locale/mozILocaleService.idl
@@ -0,0 +1,185 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+%{C++
+// Define Contractid and CID
+#define MOZ_LOCALESERVICE_CID \
+ { 0x92735ff4, 0x6384, 0x4ad6, { 0x85, 0x08, 0x75, 0x70, 0x10, 0xe1, 0x49, 0xee } }
+
+#define MOZ_LOCALESERVICE_CONTRACTID "@mozilla.org/intl/localeservice;1"
+%}
+
+[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;
+
+ /**
+ * Default locale of the browser. The locale we are guaranteed to have
+ * resources for that should be used as a last resort fallack in cases
+ * where requested locales do not match available locales.
+ */
+ readonly attribute ACString defaultLocale;
+
+ /**
+ * Last fallback is the final fallback locale we're going to attempt if all
+ * else fails in any language negotiation or locale resource retrieval situations.
+ *
+ * At the moment it returns `en-US`.
+ */
+ readonly attribute ACString lastFallbackLocale;
+
+ /**
+ * Returns a list of locales that the application should be localized to.
+ *
+ * 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.
+ *
+ * When retrieving the locales for language negotiation and matching
+ * to language resources, use the language tag form.
+ * When retrieving the locales for Intl API or ICU locale settings,
+ * use the BCP47 form.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ readonly attribute Array<ACString> appLocalesAsLangTags;
+ readonly attribute Array<ACString> appLocalesAsBCP47;
+
+ /**
+ * Returns a list of locales to use for any regional specific operations
+ * like date formatting, calendars, unit formatting etc.
+ *
+ * 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.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ readonly attribute Array<ACString> regionalPrefsLocales;
+
+ readonly attribute Array<ACString> webExposedLocales;
+
+ /**
+ * 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 canonicalized and ordered according to the order
+ * of the requested locales.
+ *
+ * (See LocaleService.h for a more C++-friendly version of this.)
+ */
+ Array<ACString> negotiateLanguages(in Array<AUTF8String> aRequested,
+ in Array<AUTF8String> aAvailable,
+ [optional] in ACString aDefaultLocale,
+ [optional] in long langNegStrategy);
+
+ /**
+ * Returns the best locale that the application should be localized to.
+ *
+ * The result is a valid locale ID and it should be
+ * used for all APIs that do not handle language negotiation.
+ *
+ * When retrieving the locales for language negotiation and matching
+ * to language resources, use the language tag form.
+ * When retrieving the locales for Intl API or ICU locale settings,
+ * use the BCP47 form.
+ *
+ * Where possible, appLocales* 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"
+ */
+ readonly attribute ACString appLocaleAsLangTag;
+ readonly attribute ACString appLocaleAsBCP47;
+
+ /**
+ * 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"]
+ */
+ attribute Array<ACString> requestedLocales;
+
+ /**
+ * Returns the top-requested locale from the user, or an empty string if none is set.
+ */
+ readonly attribute ACString requestedLocale;
+
+ /**
+ * Returns a list of locales that the app can be localized to.
+ *
+ * The result is an unordered list of locale IDs which should be
+ * used as a availableLocales input list for language negotiation.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ attribute Array<ACString> availableLocales;
+
+ /**
+ * Returns whether the current app locale is RTL.
+ */
+ readonly attribute boolean isAppLocaleRTL;
+
+ /**
+ * Returns a list of locales packaged into the app bundle.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ readonly attribute Array<ACString> packagedLocales;
+};
diff --git a/intl/locale/mozIOSPreferences.idl b/intl/locale/mozIOSPreferences.idl
new file mode 100644
index 0000000000..f3e1c12440
--- /dev/null
+++ b/intl/locale/mozIOSPreferences.idl
@@ -0,0 +1,95 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+%{C++
+// Define Contractid and CID
+#define MOZ_OSPREFERENCES_CID \
+ { 0x65944815, 0xe9ae, 0x48bd, { 0xa2, 0xbf, 0xf1, 0x10, 0x87, 0x20, 0x95, 0x0c } }
+
+#define MOZ_OSPREFERENCES_CONTRACTID "@mozilla.org/intl/ospreferences;1"
+%}
+
+[scriptable, uuid(65944815-e9ae-48bd-a2bf-f1108720950c)]
+interface mozIOSPreferences : nsISupports
+{
+ const long dateTimeFormatStyleNone = 0;
+ const long dateTimeFormatStyleShort = 1;
+ const long dateTimeFormatStyleMedium = 2;
+ const long dateTimeFormatStyleLong = 3;
+ const long dateTimeFormatStyleFull = 4;
+
+ /**
+ * Returns a list of locales used by the host environment for UI
+ * localization.
+ *
+ * The result is a sorted list and we expect that the OS attempts to
+ * use the top locale from the list for which it has data.
+ *
+ * Each element of the list is a valid locale ID that can be passed to ICU
+ * and ECMA402 Intl APIs,
+ * At the same time each element is a valid BCP47 language tag that can be
+ * used for language negotiation.
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ readonly attribute Array<ACString> systemLocales;
+
+ /**
+ * Returns a list of locales used by host environment for regional
+ * preferences internationalization.
+ *
+ * The result is a sorted list and we expect that the OS attempts to
+ * use the top locale from the list for which it has data.
+ *
+ * Each element of the list is a valid locale ID that can be passed to ICU
+ * and ECMA402 Intl APIs,
+ *
+ * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
+ */
+ readonly attribute Array<ACString> regionalPrefsLocales;
+
+ /**
+ * Returns the best locale that the host environment is localized to.
+ *
+ * The result is a valid locale ID and it should be
+ * used for all APIs that do not handle language negotiation.
+ *
+ * In any scenario involving language negotiation, systemLocales should
+ * be preferred over the single value.
+ *
+ * Example: "zh-Hans-HK"
+ */
+ readonly attribute ACString systemLocale;
+
+ /**
+ * Returns the best possible date/time pattern for the host environment
+ * taking into account date/time regional settings user defined in the OS
+ * preferences.
+ *
+ * Notice, that depending on the OS it may take into account those settings
+ * for all locales, or only if the locale matches the OS locale.
+ *
+ * It takes two integer arguments that must be valid `dateTimeFormatStyle*`
+ * values (see constants defined above), and a string representing a
+ * BCP47 locale.
+ *
+ * It returns a string with a LDML date/time pattern.
+ *
+ * If no pattern can be retrieved from the host environment, it will
+ * lookup the best available pattern from ICU.
+ *
+ * Notice, this is a pretty unique method in this API in that it does
+ * more than look up into host environment.
+ * The reason for that is that constructing the right date/time pattern
+ * requires a lot of OS-specific logic and it ends up being easier to just
+ * handle all scenarios, including with cases where we fail to retrieve
+ * anything from the OS, here.
+ */
+ AUTF8String getDateTimePattern(in long timeFormatStyle,
+ in long dateFormatStyle,
+ [optional] in ACString locale);
+};
diff --git a/intl/locale/nsLanguageAtomService.cpp b/intl/locale/nsLanguageAtomService.cpp
new file mode 100644
index 0000000000..4a9d9b92d2
--- /dev/null
+++ b/intl/locale/nsLanguageAtomService.cpp
@@ -0,0 +1,255 @@
+/* -*- 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 "nsLanguageAtomService.h"
+#include "nsUConvPropertySearch.h"
+#include "nsUnicharUtils.h"
+#include "nsAtom.h"
+#include "nsGkAtoms.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/OSPreferences.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/ServoUtils.h"
+
+using namespace mozilla;
+using mozilla::intl::OSPreferences;
+
+static constexpr nsUConvProp encodingsGroups[] = {
+#include "encodingsgroups.properties.h"
+};
+
+// List of mozilla internal x-* tags that map to themselves (see bug 256257)
+static constexpr nsStaticAtom* kLangGroups[] = {
+ // This list must be sorted!
+ nsGkAtoms::x_armn, nsGkAtoms::x_cyrillic, nsGkAtoms::x_devanagari,
+ nsGkAtoms::x_geor, nsGkAtoms::x_math, nsGkAtoms::x_tamil,
+ nsGkAtoms::Unicode, nsGkAtoms::x_western
+ // These self-mappings are not necessary unless somebody use them to specify
+ // lang in (X)HTML/XML documents, which they shouldn't. (see bug 256257)
+ // x-beng=x-beng
+ // x-cans=x-cans
+ // x-ethi=x-ethi
+ // x-guru=x-guru
+ // x-gujr=x-gujr
+ // x-khmr=x-khmr
+ // x-mlym=x-mlym
+};
+
+// Map ISO 15924 script codes from BCP47 lang tag to mozilla's langGroups.
+static constexpr struct {
+ const char* mTag;
+ nsStaticAtom* mAtom;
+} kScriptLangGroup[] = {
+ // This list must be sorted by script code!
+ {"Arab", nsGkAtoms::ar},
+ {"Armn", nsGkAtoms::x_armn},
+ {"Beng", nsGkAtoms::x_beng},
+ {"Cans", nsGkAtoms::x_cans},
+ {"Cyrl", nsGkAtoms::x_cyrillic},
+ {"Deva", nsGkAtoms::x_devanagari},
+ {"Ethi", nsGkAtoms::x_ethi},
+ {"Geok", nsGkAtoms::x_geor},
+ {"Geor", nsGkAtoms::x_geor},
+ {"Grek", nsGkAtoms::el},
+ {"Gujr", nsGkAtoms::x_gujr},
+ {"Guru", nsGkAtoms::x_guru},
+ {"Hang", nsGkAtoms::ko},
+ // Hani is not mapped to a specific langGroup, we prefer to look at the
+ // primary language subtag in this case
+ {"Hans", nsGkAtoms::Chinese},
+ // Hant is special-cased in code
+ // Hant=zh-HK
+ // Hant=zh-TW
+ {"Hebr", nsGkAtoms::he},
+ {"Hira", nsGkAtoms::Japanese},
+ {"Jpan", nsGkAtoms::Japanese},
+ {"Kana", nsGkAtoms::Japanese},
+ {"Khmr", nsGkAtoms::x_khmr},
+ {"Knda", nsGkAtoms::x_knda},
+ {"Kore", nsGkAtoms::ko},
+ {"Latn", nsGkAtoms::x_western},
+ {"Mlym", nsGkAtoms::x_mlym},
+ {"Orya", nsGkAtoms::x_orya},
+ {"Sinh", nsGkAtoms::x_sinh},
+ {"Taml", nsGkAtoms::x_tamil},
+ {"Telu", nsGkAtoms::x_telu},
+ {"Thai", nsGkAtoms::th},
+ {"Tibt", nsGkAtoms::x_tibt}};
+
+static UniquePtr<nsLanguageAtomService> gLangAtomService;
+
+// static
+nsLanguageAtomService* nsLanguageAtomService::GetService() {
+ if (!gLangAtomService) {
+ gLangAtomService = MakeUnique<nsLanguageAtomService>();
+ }
+ return gLangAtomService.get();
+}
+
+// static
+void nsLanguageAtomService::Shutdown() { gLangAtomService = nullptr; }
+
+nsStaticAtom* nsLanguageAtomService::LookupLanguage(
+ const nsACString& aLanguage) {
+ nsAutoCString lowered(aLanguage);
+ ToLowerCase(lowered);
+
+ RefPtr<nsAtom> lang = NS_Atomize(lowered);
+ return GetLanguageGroup(lang);
+}
+
+already_AddRefed<nsAtom> nsLanguageAtomService::LookupCharSet(
+ NotNull<const Encoding*> aEncoding) {
+ nsAutoCString charset;
+ aEncoding->Name(charset);
+ nsAutoCString group;
+ if (NS_FAILED(nsUConvPropertySearch::SearchPropertyValue(
+ encodingsGroups, ArrayLength(encodingsGroups), charset, group))) {
+ return RefPtr<nsAtom>(nsGkAtoms::Unicode).forget();
+ }
+ return NS_Atomize(group);
+}
+
+nsAtom* nsLanguageAtomService::GetLocaleLanguage() {
+ do {
+ if (!mLocaleLanguage) {
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ if (NS_SUCCEEDED(OSPreferences::GetInstance()->GetRegionalPrefsLocales(
+ regionalPrefsLocales))) {
+ // use lowercase for all language atoms
+ ToLowerCase(regionalPrefsLocales[0]);
+ mLocaleLanguage = NS_Atomize(regionalPrefsLocales[0]);
+ } else {
+ nsAutoCString locale;
+ OSPreferences::GetInstance()->GetSystemLocale(locale);
+
+ ToLowerCase(locale); // use lowercase for all language atoms
+ mLocaleLanguage = NS_Atomize(locale);
+ }
+ }
+ } while (0);
+
+ return mLocaleLanguage;
+}
+
+nsStaticAtom* nsLanguageAtomService::GetLanguageGroup(nsAtom* aLanguage,
+ bool* aNeedsToCache) {
+ if (aNeedsToCache) {
+ if (nsStaticAtom* atom = mLangToGroup.Get(aLanguage)) {
+ return atom;
+ }
+ *aNeedsToCache = true;
+ return nullptr;
+ }
+
+ return mLangToGroup.LookupOrInsertWith(aLanguage, [&] {
+ AssertIsMainThreadOrServoFontMetricsLocked();
+ return GetUncachedLanguageGroup(aLanguage);
+ });
+}
+
+nsStaticAtom* nsLanguageAtomService::GetUncachedLanguageGroup(
+ nsAtom* aLanguage) const {
+ nsAutoCString langStr;
+ aLanguage->ToUTF8String(langStr);
+ ToLowerCase(langStr);
+
+ if (langStr[0] == 'x' && langStr[1] == '-') {
+ // Internal x-* langGroup codes map to themselves (see bug 256257)
+ for (nsStaticAtom* langGroup : kLangGroups) {
+ if (langGroup == aLanguage) {
+ return langGroup;
+ }
+ if (aLanguage->IsAsciiLowercase()) {
+ continue;
+ }
+ // Do the slow ascii-case-insensitive comparison just if needed.
+ nsDependentAtomString string(langGroup);
+ if (string.EqualsASCII(langStr.get(), langStr.Length())) {
+ return langGroup;
+ }
+ }
+ } else {
+ // If the lang code can be parsed as BCP47, look up its (likely) script.
+
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1618034:
+ // First strip any private subtags that would cause Locale to reject the
+ // tag as non-wellformed.
+ nsACString::const_iterator start, end;
+ langStr.BeginReading(start);
+ langStr.EndReading(end);
+ if (FindInReadable("-x-"_ns, start, end)) {
+ // The substring we want ends at the beginning of the "-x-" subtag.
+ langStr.Truncate(start.get() - langStr.BeginReading());
+ }
+
+ intl::Locale loc;
+ auto result = intl::LocaleParser::TryParse(langStr, loc);
+ if (!result.isOk()) {
+ // Did the author (wrongly) use '_' instead of '-' to separate subtags?
+ // If so, fix it up and re-try parsing.
+ if (langStr.Contains('_')) {
+ langStr.ReplaceChar('_', '-');
+
+ // Throw away the partially parsed locale and re-start parsing.
+ loc = {};
+ result = intl::LocaleParser::TryParse(langStr, loc);
+ }
+ }
+ if (result.isOk() && loc.Canonicalize().isOk()) {
+ // Fill in script subtag if not present.
+ if (loc.Script().Missing()) {
+ if (loc.AddLikelySubtags().isErr()) {
+ // Fall back to x-unicode if no match was found
+ return nsGkAtoms::Unicode;
+ }
+ }
+ // Traditional Chinese has separate prefs for Hong Kong / Taiwan;
+ // check the region subtag.
+ if (loc.Script().EqualTo("Hant")) {
+ if (loc.Region().EqualTo("HK")) {
+ return nsGkAtoms::HongKongChinese;
+ }
+ return nsGkAtoms::Taiwanese;
+ }
+ // Search list of known script subtags that map to langGroup codes.
+ size_t foundIndex;
+ Span<const char> scriptAsSpan = loc.Script().Span();
+ nsDependentCSubstring script(scriptAsSpan.data(), scriptAsSpan.size());
+ if (BinarySearchIf(
+ kScriptLangGroup, 0, ArrayLength(kScriptLangGroup),
+ [script](const auto& entry) -> int {
+ return Compare(script, nsDependentCString(entry.mTag));
+ },
+ &foundIndex)) {
+ return kScriptLangGroup[foundIndex].mAtom;
+ }
+ // Script subtag was not recognized (includes "Hani"); check the language
+ // subtag for CJK possibilities so that we'll prefer the appropriate font
+ // rather than falling back to the browser's hardcoded preference.
+ if (loc.Language().EqualTo("zh")) {
+ if (loc.Region().EqualTo("HK")) {
+ return nsGkAtoms::HongKongChinese;
+ }
+ if (loc.Region().EqualTo("TW")) {
+ return nsGkAtoms::Taiwanese;
+ }
+ return nsGkAtoms::Chinese;
+ }
+ if (loc.Language().EqualTo("ja")) {
+ return nsGkAtoms::Japanese;
+ }
+ if (loc.Language().EqualTo("ko")) {
+ return nsGkAtoms::ko;
+ }
+ }
+ }
+
+ // Fall back to x-unicode if no match was found
+ return nsGkAtoms::Unicode;
+}
diff --git a/intl/locale/nsLanguageAtomService.h b/intl/locale/nsLanguageAtomService.h
new file mode 100644
index 0000000000..de06c06d9d
--- /dev/null
+++ b/intl/locale/nsLanguageAtomService.h
@@ -0,0 +1,61 @@
+/* -*- 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/. */
+
+/*
+ * The nsILanguageAtomService provides a mapping from languages or charsets
+ * to language groups, and access to the system locale language.
+ */
+
+#ifndef nsLanguageAtomService_h_
+#define nsLanguageAtomService_h_
+
+#include "mozilla/NotNull.h"
+#include "nsCOMPtr.h"
+#include "nsAtom.h"
+#include "nsTHashMap.h"
+
+namespace mozilla {
+class Encoding;
+}
+
+class nsLanguageAtomService final {
+ using Encoding = mozilla::Encoding;
+ template <typename T>
+ using NotNull = mozilla::NotNull<T>;
+
+ public:
+ static nsLanguageAtomService* GetService();
+
+ static void Shutdown();
+
+ nsStaticAtom* LookupLanguage(const nsACString& aLanguage);
+ already_AddRefed<nsAtom> LookupCharSet(NotNull<const Encoding*> aCharSet);
+ nsAtom* GetLocaleLanguage();
+
+ // Returns the language group that the specified language is a part of.
+ //
+ // aNeedsToCache is used for two things. If null, it indicates that
+ // the nsLanguageAtomService is safe to cache the result of the
+ // language group lookup, either because we're on the main thread,
+ // or because we're on a style worker thread but the font lock has
+ // been acquired. If non-null, it indicates that it's not safe to
+ // cache the result of the language group lookup (because we're on
+ // a style worker thread without the lock acquired). In this case,
+ // GetLanguageGroup will store true in *aNeedsToCache true if we
+ // would have cached the result of a new lookup, and false if we
+ // were able to use an existing cached result. Thus, callers that
+ // get a true *aNeedsToCache outparam value should make an effort
+ // to re-call GetLanguageGroup when it is safe to cache, to avoid
+ // recomputing the language group again later.
+ nsStaticAtom* GetLanguageGroup(nsAtom* aLanguage,
+ bool* aNeedsToCache = nullptr);
+ nsStaticAtom* GetUncachedLanguageGroup(nsAtom* aLanguage) const;
+
+ private:
+ nsTHashMap<nsRefPtrHashKey<nsAtom>, nsStaticAtom*> mLangToGroup;
+ RefPtr<nsAtom> mLocaleLanguage;
+};
+
+#endif
diff --git a/intl/locale/nsUConvPropertySearch.cpp b/intl/locale/nsUConvPropertySearch.cpp
new file mode 100644
index 0000000000..114b85e0fe
--- /dev/null
+++ b/intl/locale/nsUConvPropertySearch.cpp
@@ -0,0 +1,40 @@
+/* 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 "nsUConvPropertySearch.h"
+#include "nsCRT.h"
+#include "nsString.h"
+#include "mozilla/BinarySearch.h"
+
+namespace {
+
+struct PropertyComparator {
+ const nsCString& mKey;
+ explicit PropertyComparator(const nsCString& aKey) : mKey(aKey) {}
+ int operator()(const nsUConvProp& aProperty) const {
+ return Compare(mKey, nsDependentCString(aProperty.mKey));
+ }
+};
+
+} // namespace
+
+// static
+nsresult nsUConvPropertySearch::SearchPropertyValue(
+ const nsUConvProp aProperties[], int32_t aNumberOfProperties,
+ const nsACString& aKey, nsACString& aValue) {
+ using mozilla::BinarySearchIf;
+
+ const nsCString& flat = PromiseFlatCString(aKey);
+ size_t index;
+ if (BinarySearchIf(aProperties, 0, aNumberOfProperties,
+ PropertyComparator(flat), &index)) {
+ nsDependentCString val(aProperties[index].mValue,
+ aProperties[index].mValueLength);
+ aValue.Assign(val);
+ return NS_OK;
+ }
+
+ aValue.Truncate();
+ return NS_ERROR_FAILURE;
+}
diff --git a/intl/locale/nsUConvPropertySearch.h b/intl/locale/nsUConvPropertySearch.h
new file mode 100644
index 0000000000..4914deb5ec
--- /dev/null
+++ b/intl/locale/nsUConvPropertySearch.h
@@ -0,0 +1,35 @@
+/* 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 nsUConvPropertySearch_h_
+#define nsUConvPropertySearch_h_
+
+#include "nsStringFwd.h"
+
+struct nsUConvProp {
+ const char* const mKey;
+ const char* const mValue;
+ const uint32_t mValueLength;
+};
+
+class nsUConvPropertySearch {
+ public:
+ /**
+ * Looks up a property by value.
+ *
+ * @param aProperties
+ * the static property array
+ * @param aKey
+ * the key to look up
+ * @param aValue
+ * the return value (empty string if not found)
+ * @return NS_OK if found or NS_ERROR_FAILURE if not found
+ */
+ static nsresult SearchPropertyValue(const nsUConvProp aProperties[],
+ int32_t aNumberOfProperties,
+ const nsACString& aKey,
+ nsACString& aValue);
+};
+
+#endif /* nsUConvPropertySearch_h_ */
diff --git a/intl/locale/props2arrays.py b/intl/locale/props2arrays.py
new file mode 100644
index 0000000000..0572cb1c5f
--- /dev/null
+++ b/intl/locale/props2arrays.py
@@ -0,0 +1,26 @@
+# 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/.
+
+
+def main(header, propFile):
+ mappings = {}
+
+ with open(propFile, "r") as f:
+ for line in f:
+ line = line.strip()
+ if not line.startswith("#"):
+ parts = line.split("=", 1)
+ if len(parts) == 2 and len(parts[0]) > 0:
+ mappings[parts[0].strip()] = parts[1].strip()
+
+ keys = mappings.keys()
+
+ header.write("// This is a generated file. Please do not edit.\n")
+ header.write("// Please edit the corresponding .properties file instead.\n")
+
+ entries = [
+ '{ "%s", "%s", %d }' % (key, mappings[key], len(mappings[key]))
+ for key in sorted(keys)
+ ]
+ header.write(",\n".join(entries) + "\n")
diff --git a/intl/locale/rust/fluent-langneg-ffi/Cargo.toml b/intl/locale/rust/fluent-langneg-ffi/Cargo.toml
new file mode 100644
index 0000000000..88d6bad4d4
--- /dev/null
+++ b/intl/locale/rust/fluent-langneg-ffi/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "fluent-langneg-ffi"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["Zibi Braniecki <zibi@braniecki.net>"]
+edition = "2018"
+
+[dependencies]
+nsstring = { path = "../../../../xpcom/rust/nsstring" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+fluent-langneg = { version = "0.13", features = ["cldr"] }
+unic-langid = "0.9"
+unic-langid-ffi = { path = "../unic-langid-ffi" }
diff --git a/intl/locale/rust/fluent-langneg-ffi/cbindgen.toml b/intl/locale/rust/fluent-langneg-ffi/cbindgen.toml
new file mode 100644
index 0000000000..98ec15d389
--- /dev/null
+++ b/intl/locale/rust/fluent-langneg-ffi/cbindgen.toml
@@ -0,0 +1,17 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+#ifndef mozilla_intl_locale_MozLocaleBindings_h
+#error "Don't include this file directly, instead include MozLocaleBindings.h"
+#endif
+"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla", "intl", "ffi"]
+
+[export.rename]
+"ThinVec" = "nsTArray"
diff --git a/intl/locale/rust/fluent-langneg-ffi/src/lib.rs b/intl/locale/rust/fluent-langneg-ffi/src/lib.rs
new file mode 100644
index 0000000000..591e9ef861
--- /dev/null
+++ b/intl/locale/rust/fluent-langneg-ffi/src/lib.rs
@@ -0,0 +1,79 @@
+/* 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/. */
+
+use fluent_langneg::negotiate::NegotiationStrategy as LangNegNegotiationStrategy;
+use fluent_langneg::negotiate_languages;
+use nsstring::nsACString;
+use nsstring::nsCString;
+use thin_vec::ThinVec;
+use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
+use unic_langid_ffi::new_langid_for_mozilla;
+
+/// We want to return the exact strings that were passed to us out of the
+/// available and default pool. Since for the negotiation we canonicalize them
+/// in `LanguageIdentifier`, this struct will preserve the original, non-canonicalized
+/// string, and then use it to populate return array.
+#[derive(Debug, PartialEq)]
+struct LangIdString<'l> {
+ pub source: &'l nsCString,
+ pub langid: LanguageIdentifier,
+}
+
+impl<'l> LangIdString<'l> {
+ pub fn try_new(s: &'l nsCString) -> Result<Self, LanguageIdentifierError> {
+ new_langid_for_mozilla(s).map(|l| LangIdString {
+ source: s,
+ langid: l,
+ })
+ }
+}
+
+impl<'l> AsRef<LanguageIdentifier> for LangIdString<'l> {
+ fn as_ref(&self) -> &LanguageIdentifier {
+ &self.langid
+ }
+}
+
+#[repr(C)]
+pub enum NegotiationStrategy {
+ Filtering,
+ Matching,
+ Lookup,
+}
+
+fn get_strategy(input: NegotiationStrategy) -> LangNegNegotiationStrategy {
+ match input {
+ NegotiationStrategy::Filtering => LangNegNegotiationStrategy::Filtering,
+ NegotiationStrategy::Matching => LangNegNegotiationStrategy::Matching,
+ NegotiationStrategy::Lookup => LangNegNegotiationStrategy::Lookup,
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fluent_langneg_negotiate_languages(
+ requested: &ThinVec<nsCString>,
+ available: &ThinVec<nsCString>,
+ default: &nsACString,
+ strategy: NegotiationStrategy,
+ result: &mut ThinVec<nsCString>,
+) {
+ let requested = requested
+ .iter()
+ .filter_map(|s| new_langid_for_mozilla(s).ok())
+ .collect::<Vec<_>>();
+
+ let available = available
+ .iter()
+ .filter_map(|s| LangIdString::try_new(s).ok())
+ .collect::<Vec<_>>();
+
+ let d: nsCString = default.into();
+ let default = LangIdString::try_new(&d).ok();
+
+ let strategy = get_strategy(strategy);
+
+ for l in negotiate_languages(&requested, &available, default.as_ref(), strategy) {
+ result.push(l.source.clone());
+ }
+}
diff --git a/intl/locale/rust/oxilangtag-ffi/Cargo.toml b/intl/locale/rust/oxilangtag-ffi/Cargo.toml
new file mode 100644
index 0000000000..ee3b1cf5c8
--- /dev/null
+++ b/intl/locale/rust/oxilangtag-ffi/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "oxilangtag-ffi"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["Jonathan Kew <jkew@mozilla.com>"]
+edition = "2021"
+
+[dependencies]
+nsstring = { path = "../../../../xpcom/rust/nsstring" }
+oxilangtag = "0.1.3"
diff --git a/intl/locale/rust/oxilangtag-ffi/cbindgen.toml b/intl/locale/rust/oxilangtag-ffi/cbindgen.toml
new file mode 100644
index 0000000000..21d703000b
--- /dev/null
+++ b/intl/locale/rust/oxilangtag-ffi/cbindgen.toml
@@ -0,0 +1,15 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla", "intl", "ffi"]
+
+[parse]
+parse_deps = true
+include = ["oxilangtag"]
diff --git a/intl/locale/rust/oxilangtag-ffi/src/lib.rs b/intl/locale/rust/oxilangtag-ffi/src/lib.rs
new file mode 100644
index 0000000000..5a30e9b77f
--- /dev/null
+++ b/intl/locale/rust/oxilangtag-ffi/src/lib.rs
@@ -0,0 +1,126 @@
+/* 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/. */
+
+use nsstring::nsACString;
+use oxilangtag::LanguageTag;
+
+pub struct LangTag; // Opaque type for ffi interface.
+
+/// Parse a string as a BCP47 language tag. Returns a `LangTag` object if the string is
+/// successfully parsed; this must be freed with `lang_tag_destroy`.
+///
+/// The string `tag` must outlive the `LangTag`.
+///
+/// Returns null if `tag` is not a well-formed BCP47 tag (including if it is not
+/// valid UTF-8).
+#[no_mangle]
+pub extern "C" fn lang_tag_new(tag: &nsACString) -> *mut LangTag {
+ if let Ok(tag_str) = core::str::from_utf8(tag.as_ref()) {
+ if let Ok(language_tag) = LanguageTag::parse(tag_str) {
+ return Box::into_raw(Box::new(language_tag)) as *mut LangTag;
+ }
+ }
+ std::ptr::null_mut()
+}
+
+/// Free a `LangTag` instance.
+#[no_mangle]
+pub extern "C" fn lang_tag_destroy(lang: *mut LangTag) {
+ if lang.is_null() {
+ return;
+ }
+ let _ = unsafe { Box::from_raw(lang as *mut LanguageTag<&str>) };
+}
+
+/// Matches an HTML language attribute against a CSS :lang() selector using the
+/// "extended filtering" algorithm.
+/// The attribute is a BCP47 language tag that was successfully parsed by oxilangtag;
+/// the selector is a string that is treated as a language range per RFC 4647.
+#[no_mangle]
+pub extern "C" fn lang_tag_matches(attribute: *const LangTag, selector: &nsACString) -> bool {
+ // This should only be called with a pointer that we got from lang_tag_new().
+ let lang = unsafe { *(attribute as *const LanguageTag<&str>) };
+
+ // Our callers guarantee that the selector string is valid UTF-8.
+ let range_str = unsafe { selector.as_str_unchecked() };
+
+ if lang.is_empty() || range_str.is_empty() {
+ return false;
+ }
+
+ // RFC 4647 Extended Filtering:
+ // https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.2
+
+ // 1. Split both the extended language range and the language tag being
+ // compared into a list of subtags by dividing on the hyphen (%x2D)
+ // character. Two subtags match if either they are the same when
+ // compared case-insensitively or the language range's subtag is the
+ // wildcard '*'.
+
+ let mut range_subtags = range_str.split('-');
+ let mut lang_subtags = lang.as_str().split('-');
+
+ // 2. Begin with the first subtag in each list. If the first subtag in
+ // the range does not match the first subtag in the tag, the overall
+ // match fails. Otherwise, move to the next subtag in both the
+ // range and the tag.
+
+ let mut range_subtag = range_subtags.next();
+ let mut lang_subtag = lang_subtags.next();
+ // Cannot be None, because we checked that both args were non-empty.
+ assert!(range_subtag.is_some() && lang_subtag.is_some());
+ if !(range_subtag.unwrap() == "*"
+ || range_subtag
+ .unwrap()
+ .eq_ignore_ascii_case(lang_subtag.unwrap()))
+ {
+ return false;
+ }
+
+ range_subtag = range_subtags.next();
+ lang_subtag = lang_subtags.next();
+
+ // 3. While there are more subtags left in the language range's list:
+ loop {
+ // 4. When the language range's list has no more subtags, the match
+ // succeeds.
+ let Some(range_subtag_str) = range_subtag else {
+ return true;
+ };
+
+ // A. If the subtag currently being examined in the range is the
+ // wildcard ('*'), move to the next subtag in the range and
+ // continue with the loop.
+ if range_subtag_str == "*" {
+ range_subtag = range_subtags.next();
+ continue;
+ }
+
+ // B. Else, if there are no more subtags in the language tag's
+ // list, the match fails.
+ let Some(lang_subtag_str) = lang_subtag else {
+ return false;
+ };
+
+ // C. Else, if the current subtag in the range's list matches the
+ // current subtag in the language tag's list, move to the next
+ // subtag in both lists and continue with the loop.
+ if range_subtag_str.eq_ignore_ascii_case(lang_subtag_str) {
+ range_subtag = range_subtags.next();
+ lang_subtag = lang_subtags.next();
+ continue;
+ }
+
+ // D. Else, if the language tag's subtag is a "singleton" (a single
+ // letter or digit, which includes the private-use subtag 'x')
+ // the match fails.
+ if lang_subtag_str.len() == 1 {
+ return false;
+ }
+
+ // E. Else, move to the next subtag in the language tag's list and
+ // continue with the loop.
+ lang_subtag = lang_subtags.next();
+ }
+}
diff --git a/intl/locale/rust/unic-langid-ffi/Cargo.toml b/intl/locale/rust/unic-langid-ffi/Cargo.toml
new file mode 100644
index 0000000000..bd969437a6
--- /dev/null
+++ b/intl/locale/rust/unic-langid-ffi/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "unic-langid-ffi"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["Zibi Braniecki <zibi@braniecki.net>"]
+edition = "2018"
+
+[dependencies]
+nsstring = { path = "../../../../xpcom/rust/nsstring" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+unic-langid = { version = "0.9", features = ["likelysubtags"] }
diff --git a/intl/locale/rust/unic-langid-ffi/cbindgen.toml b/intl/locale/rust/unic-langid-ffi/cbindgen.toml
new file mode 100644
index 0000000000..3842e5183b
--- /dev/null
+++ b/intl/locale/rust/unic-langid-ffi/cbindgen.toml
@@ -0,0 +1,22 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+#ifndef mozilla_intl_locale_MozLocaleBindings_h
+#error "Don't include this file directly, instead include MozLocaleBindings.h"
+#endif
+"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla", "intl", "ffi"]
+
+[parse]
+parse_deps = true
+include = ["unic-langid", "unic-langid-impl"]
+
+[export.rename]
+"ThinVec" = "nsTArray"
+"nsCStr" = "nsDependentCSubstring"
diff --git a/intl/locale/rust/unic-langid-ffi/src/lib.rs b/intl/locale/rust/unic-langid-ffi/src/lib.rs
new file mode 100644
index 0000000000..804a4341e1
--- /dev/null
+++ b/intl/locale/rust/unic-langid-ffi/src/lib.rs
@@ -0,0 +1,168 @@
+/* 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/. */
+
+use nsstring::{nsACString, nsCStr, nsCString};
+use thin_vec::ThinVec;
+pub use unic_langid::{subtags, CharacterDirection, LanguageIdentifier, LanguageIdentifierError};
+
+pub fn new_langid_for_mozilla(
+ name: &nsACString,
+) -> Result<LanguageIdentifier, LanguageIdentifierError> {
+ if name.eq_ignore_ascii_case(b"ja-jp-mac") {
+ "ja-JP-macos".parse()
+ } else {
+ // Cut out any `.FOO` like `en-US.POSIX`.
+ let mut name: &[u8] = name.as_ref();
+ if let Some(ptr) = name.iter().position(|b| b == &b'.') {
+ name = &name[..ptr];
+ }
+ LanguageIdentifier::from_bytes(name)
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_canonicalize(name: &mut nsACString) -> bool {
+ let langid = new_langid_for_mozilla(name);
+
+ let result = langid.is_ok();
+
+ name.assign(&langid.unwrap_or_default().to_string());
+
+ result
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_new(
+ name: &nsACString,
+ ret_val: &mut bool,
+) -> *mut LanguageIdentifier {
+ let langid = new_langid_for_mozilla(name);
+
+ *ret_val = langid.is_ok();
+ Box::into_raw(Box::new(langid.unwrap_or_default()))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn unic_langid_destroy(langid: *mut LanguageIdentifier) {
+ let _ = Box::from_raw(langid);
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_as_string(langid: &mut LanguageIdentifier, ret_val: &mut nsACString) {
+ ret_val.assign(&langid.to_string());
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_get_language<'a>(
+ langid: &'a LanguageIdentifier,
+ out: &mut nsCStr<'a>,
+) {
+ *out = nsCStr::from(langid.language.as_str());
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_set_language(
+ langid: &mut LanguageIdentifier,
+ string: &nsACString,
+) -> bool {
+ subtags::Language::from_bytes(string)
+ .map(|lang| langid.language = lang)
+ .is_ok()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_clear_language(langid: &mut LanguageIdentifier) {
+ langid.language.clear()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_get_script<'a>(langid: &'a LanguageIdentifier, out: &mut nsCStr<'a>) {
+ *out = nsCStr::from(langid.script.as_ref().map_or("", |s| s.as_str()));
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_set_script(
+ langid: &mut LanguageIdentifier,
+ string: &nsACString,
+) -> bool {
+ subtags::Script::from_bytes(string)
+ .map(|script| langid.script = Some(script))
+ .is_ok()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_clear_script(langid: &mut LanguageIdentifier) {
+ langid.script = None;
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_get_region<'a>(langid: &'a LanguageIdentifier, out: &mut nsCStr<'a>) {
+ *out = nsCStr::from(langid.region.as_ref().map_or("", |s| s.as_str()));
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_set_region(
+ langid: &mut LanguageIdentifier,
+ string: &nsACString,
+) -> bool {
+ subtags::Region::from_bytes(string)
+ .map(|region| langid.region = Some(region))
+ .is_ok()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_clear_region(langid: &mut LanguageIdentifier) {
+ langid.region = None;
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_get_variants(
+ langid: &LanguageIdentifier,
+ variants: &mut ThinVec<nsCString>,
+) {
+ for v in langid.variants() {
+ variants.push(v.as_str().into());
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_set_variants(
+ langid: &mut LanguageIdentifier,
+ variants: &ThinVec<nsCString>,
+) -> bool {
+ variants
+ .iter()
+ .map(|v| subtags::Variant::from_bytes(v))
+ .collect::<Result<Vec<_>, _>>()
+ .map(|variants| langid.set_variants(&variants))
+ .is_ok()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_clear_variants(langid: &mut LanguageIdentifier) {
+ langid.clear_variants()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_matches(
+ langid: &LanguageIdentifier,
+ other: &LanguageIdentifier,
+ self_as_range: bool,
+ other_as_range: bool,
+) -> bool {
+ langid.matches(other, self_as_range, other_as_range)
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_maximize(langid: &mut LanguageIdentifier) -> bool {
+ langid.maximize()
+}
+
+#[no_mangle]
+pub extern "C" fn unic_langid_is_rtl(name: &nsACString) -> bool {
+ match new_langid_for_mozilla(name) {
+ Ok(langid) => langid.character_direction() == CharacterDirection::RTL,
+ Err(_) => false,
+ }
+}
diff --git a/intl/locale/tests/LangPackMatcherTestUtils.sys.mjs b/intl/locale/tests/LangPackMatcherTestUtils.sys.mjs
new file mode 100644
index 0000000000..4b18f1be13
--- /dev/null
+++ b/intl/locale/tests/LangPackMatcherTestUtils.sys.mjs
@@ -0,0 +1,124 @@
+/* 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/. */
+
+import { LangPackMatcher } from "resource://gre/modules/LangPackMatcher.sys.mjs";
+
+/**
+ * LangPackMatcher.jsm calls out to to the addons store, which involves network requests.
+ * Other tests create a fake addons server, and install mock XPIs. At the time of this
+ * writing that infrastructure is not available for mochitests.
+ *
+ * Instead, this test mocks out APIs that have a side-effect, so the addons of the browser
+ * are never modified.
+ *
+ * The calls to get the app's locale and system's locale are also mocked so that the
+ * different language mismatch scenarios can be run through.
+ *
+ * The locales are BCP 47 identifiers:
+ *
+ * @param {{
+ * sandbox: SinonSandbox,
+ * systemLocale: string,
+ * appLocale, string,
+ * }}
+ */
+export function getAddonAndLocalAPIsMocker(testScope, sandbox) {
+ const { info } = testScope;
+ return function mockAddonAndLocaleAPIs({ systemLocale, appLocale }) {
+ info("Mocking LangPackMatcher.jsm APIs");
+
+ let resolveLangPacks;
+ const langPackPromise = new Promise(resolve => {
+ resolveLangPacks = availableLangpacks => {
+ info(
+ `Resolving which langpacks are available for download: ${JSON.stringify(
+ availableLangpacks
+ )}`
+ );
+ resolve(
+ availableLangpacks.map(locale => ({
+ guid: `langpack-${locale}@firefox.mozilla.org`,
+ type: "language",
+ hash: locale,
+ target_locale: locale,
+ current_compatible_version: {
+ files: [
+ {
+ platform: "all",
+ url: `http://example.com/${locale}.langpack.xpi`,
+ },
+ ],
+ },
+ }))
+ );
+ };
+ });
+
+ let resolveInstaller;
+ const installerPromise = new Promise(resolve => {
+ resolveInstaller = () => {
+ info("LangPack install finished.");
+ resolve();
+ };
+ });
+
+ const { mockable } = LangPackMatcher;
+ if (appLocale) {
+ const availableLocales = [appLocale];
+ if (appLocale !== "en-US") {
+ // Ensure the fallback behavior is accurately simulated for Firefox.
+ availableLocales.push("en-US");
+ }
+ sandbox
+ .stub(mockable, "getAvailableLocalesIncludingFallback")
+ .returns(availableLocales);
+ sandbox.stub(mockable, "getDefaultLocale").returns(appLocale);
+ sandbox.stub(mockable, "getAppLocaleAsBCP47").returns(appLocale);
+ sandbox.stub(mockable, "getLastFallbackLocale").returns("en-US");
+ }
+ if (systemLocale) {
+ sandbox.stub(mockable, "getSystemLocale").returns(systemLocale);
+ }
+
+ sandbox.stub(mockable, "getAvailableLangpacks").callsFake(() => {
+ info("Requesting which langpacks are available for download");
+ return langPackPromise;
+ });
+
+ sandbox.stub(mockable, "installLangPack").callsFake(langPack => {
+ info(`LangPack install started, but pending: ${langPack.target_locale}`);
+ return installerPromise;
+ });
+
+ sandbox.stub(mockable, "setRequestedAppLocales").callsFake(locales => {
+ info(
+ `Changing the browser's requested locales to: ${JSON.stringify(
+ locales
+ )}`
+ );
+ });
+
+ return {
+ /**
+ * Resolves the addons API call with available langpacks. Call with a list
+ * of BCP 47 identifiers.
+ *
+ * @type {(availableLangpacks: string[]) => {}}
+ */
+ resolveLangPacks,
+
+ /**
+ * Resolves the pending call to install a langpack.
+ *
+ * @type {() => {}}
+ */
+ resolveInstaller,
+
+ /**
+ * The mocked APIs.
+ */
+ mockable,
+ };
+ };
+}
diff --git a/intl/locale/tests/gtest/TestAppDateTimeFormat.cpp b/intl/locale/tests/gtest/TestAppDateTimeFormat.cpp
new file mode 100644
index 0000000000..a83b373428
--- /dev/null
+++ b/intl/locale/tests/gtest/TestAppDateTimeFormat.cpp
@@ -0,0 +1,310 @@
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+#include "mozilla/intl/DateTimeFormat.h"
+
+namespace mozilla::intl {
+using Style = DateTimeFormat::Style;
+using StyleBag = DateTimeFormat::StyleBag;
+using ComponentsBag = DateTimeFormat::ComponentsBag;
+
+static DateTimeFormat::StyleBag ToStyleBag(Maybe<DateTimeFormat::Style> date,
+ Maybe<DateTimeFormat::Style> time) {
+ DateTimeFormat::StyleBag style;
+ style.date = date;
+ style.time = time;
+ return style;
+}
+
+TEST(AppDateTimeFormat, FormatPRExplodedTime)
+{
+ PRTime prTime = 0;
+ PRExplodedTime prExplodedTime;
+ PR_ExplodeTime(prTime, PR_GMTParameters, &prExplodedTime);
+
+ AppDateTimeFormat::sLocale = new nsCString("en-US");
+ AppDateTimeFormat::DeleteCache();
+ StyleBag style = ToStyleBag(Some(Style::Long), Some(Style::Long));
+
+ nsAutoString formattedTime;
+ nsresult rv =
+ AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"January") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"12:00:00 AM") != kNotFound ||
+ formattedTime.Find(u"12:00:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"00:00:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 19, 0, 1, 0, 1970, 4, 0, {(19 * 60), 0}};
+
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"January") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"12:19:00 AM") != kNotFound ||
+ formattedTime.Find(u"12:19:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"00:19:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 0, 7, 1,
+ 0, 1970, 4, 0, {(6 * 60 * 60), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"January") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"7:00:00 AM") != kNotFound ||
+ formattedTime.Find(u"7:00:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"07:00:00") != kNotFound);
+
+ prExplodedTime = {
+ 0, 0, 29, 11, 1,
+ 0, 1970, 4, 0, {(10 * 60 * 60) + (29 * 60), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"January") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"11:29:00 AM") != kNotFound ||
+ formattedTime.Find(u"11:29:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"11:29:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 37, 23, 31, 11, 1969, 3, 364, {-(23 * 60), 0}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"December") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"31") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"11:37:00 PM") != kNotFound ||
+ formattedTime.Find(u"11:37:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"23:37:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 0, 17, 31, 11, 1969, 3, 364, {-(7 * 60 * 60), 0}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"December") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"31") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"5:00:00 PM") != kNotFound ||
+ formattedTime.Find(u"5:00:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"17:00:00") != kNotFound);
+
+ prExplodedTime = {
+ 0, 0, 47, 14, 31,
+ 11, 1969, 3, 364, {-((10 * 60 * 60) + (13 * 60)), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"December") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"31") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"2:47:00 PM") != kNotFound ||
+ formattedTime.Find(u"2:47:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"14:47:00") != kNotFound);
+}
+
+TEST(AppDateTimeFormat, DateFormatSelectors)
+{
+ PRTime prTime = 0;
+ PRExplodedTime prExplodedTime;
+ PR_ExplodeTime(prTime, PR_GMTParameters, &prExplodedTime);
+
+ AppDateTimeFormat::sLocale = new nsCString("en-US");
+ AppDateTimeFormat::DeleteCache();
+
+ nsAutoString formattedTime;
+
+ {
+ ComponentsBag components{};
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::TwoDigit);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("01/1970", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::Long);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("January 1970", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.month = Some(DateTimeFormat::Month::Long);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("January", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.weekday = Some(DateTimeFormat::Text::Short);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Thu", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+}
+
+TEST(AppDateTimeFormat, FormatPRExplodedTimeForeign)
+{
+ PRTime prTime = 0;
+ PRExplodedTime prExplodedTime;
+ PR_ExplodeTime(prTime, PR_GMTParameters, &prExplodedTime);
+
+ AppDateTimeFormat::sLocale = new nsCString("de-DE");
+ AppDateTimeFormat::DeleteCache();
+ StyleBag style = ToStyleBag(Some(Style::Long), Some(Style::Long));
+
+ nsAutoString formattedTime;
+ nsresult rv =
+ AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"1.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Januar") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"12:00:00 AM") != kNotFound ||
+ formattedTime.Find(u"12:00:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"00:00:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 19, 0, 1, 0, 1970, 4, 0, {(19 * 60), 0}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"1.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Januar") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"12:19:00 AM") != kNotFound ||
+ formattedTime.Find(u"12:19:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"00:19:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 0, 7, 1,
+ 0, 1970, 4, 0, {(6 * 60 * 60), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"1.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Januar") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"7:00:00 AM") != kNotFound ||
+ formattedTime.Find(u"7:00:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"07:00:00") != kNotFound);
+
+ prExplodedTime = {
+ 0, 0, 29, 11, 1,
+ 0, 1970, 4, 0, {(10 * 60 * 60) + (29 * 60), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"1.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Januar") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1970") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"11:29:00 AM") != kNotFound ||
+ formattedTime.Find(u"11:29:00\u202FAM") != kNotFound ||
+ formattedTime.Find(u"11:29:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 37, 23, 31, 11, 1969, 3, 364, {-(23 * 60), 0}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"31.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Dezember") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"11:37:00 PM") != kNotFound ||
+ formattedTime.Find(u"11:37:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"23:37:00") != kNotFound);
+
+ prExplodedTime = {0, 0, 0, 17, 31, 11, 1969, 3, 364, {-(7 * 60 * 60), 0}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"31.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Dezember") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"5:00:00 PM") != kNotFound ||
+ formattedTime.Find(u"5:00:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"17:00:00") != kNotFound);
+
+ prExplodedTime = {
+ 0, 0, 47, 14, 31,
+ 11, 1969, 3, 364, {-((10 * 60 * 60) + (13 * 60)), (1 * 60 * 60)}};
+ rv = AppDateTimeFormat::Format(style, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(formattedTime.Find(u"31.") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"Dezember") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"1969") != kNotFound);
+ ASSERT_TRUE(formattedTime.Find(u"2:47:00 PM") != kNotFound ||
+ formattedTime.Find(u"2:47:00\u202FPM") != kNotFound ||
+ formattedTime.Find(u"14:47:00") != kNotFound);
+}
+
+TEST(AppDateTimeFormat, DateFormatSelectorsForeign)
+{
+ PRTime prTime = 0;
+ PRExplodedTime prExplodedTime;
+ PR_ExplodeTime(prTime, PR_GMTParameters, &prExplodedTime);
+
+ AppDateTimeFormat::sLocale = new nsCString("de-DE");
+ AppDateTimeFormat::DeleteCache();
+
+ nsAutoString formattedTime;
+ {
+ ComponentsBag components{};
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::TwoDigit);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("01.1970", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::Long);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Januar 1970", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.weekday = Some(DateTimeFormat::Text::Short);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Do", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.weekday = Some(DateTimeFormat::Text::Long);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Donnerstag", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.month = Some(DateTimeFormat::Month::Long);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Januar", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+ {
+ ComponentsBag components{};
+ components.weekday = Some(DateTimeFormat::Text::Short);
+
+ nsresult rv =
+ AppDateTimeFormat::Format(components, &prExplodedTime, formattedTime);
+ ASSERT_NS_SUCCEEDED(rv);
+ ASSERT_STREQ("Do", NS_ConvertUTF16toUTF8(formattedTime).get());
+ }
+}
+
+} // namespace mozilla::intl
diff --git a/intl/locale/tests/gtest/TestLocaleService.cpp b/intl/locale/tests/gtest/TestLocaleService.cpp
new file mode 100644
index 0000000000..2cf19727d6
--- /dev/null
+++ b/intl/locale/tests/gtest/TestLocaleService.cpp
@@ -0,0 +1,173 @@
+/* -*- 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/Preferences.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/intl/Collator.h"
+
+using namespace mozilla::intl;
+
+TEST(Intl_Locale_LocaleService, CanonicalizeLanguageId)
+{
+ nsCString locale("en-US.POSIX");
+ ASSERT_TRUE(LocaleService::CanonicalizeLanguageId(locale));
+ ASSERT_TRUE(locale.EqualsLiteral("en-US"));
+
+ locale.AssignLiteral("en-US_POSIX");
+ ASSERT_TRUE(LocaleService::CanonicalizeLanguageId(locale));
+ ASSERT_TRUE(locale.EqualsLiteral("en-US-posix"));
+
+ locale.AssignLiteral("en-US-POSIX");
+ ASSERT_TRUE(LocaleService::CanonicalizeLanguageId(locale));
+ ASSERT_TRUE(locale.EqualsLiteral("en-US-posix"));
+
+ locale.AssignLiteral("C");
+ ASSERT_FALSE(LocaleService::CanonicalizeLanguageId(locale));
+ ASSERT_TRUE(locale.EqualsLiteral("und"));
+
+ locale.AssignLiteral("");
+ ASSERT_FALSE(LocaleService::CanonicalizeLanguageId(locale));
+ ASSERT_TRUE(locale.EqualsLiteral("und"));
+}
+
+TEST(Intl_Locale_LocaleService, GetAppLocalesAsBCP47)
+{
+ nsTArray<nsCString> appLocales;
+ LocaleService::GetInstance()->GetAppLocalesAsBCP47(appLocales);
+
+ ASSERT_FALSE(appLocales.IsEmpty());
+}
+
+TEST(Intl_Locale_LocaleService, GetAppLocalesAsLangTags)
+{
+ nsTArray<nsCString> appLocales;
+ LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
+
+ ASSERT_FALSE(appLocales.IsEmpty());
+}
+
+TEST(Intl_Locale_LocaleService, GetAppLocalesAsLangTags_lastIsPresent)
+{
+ nsAutoCString lastFallbackLocale;
+ LocaleService::GetInstance()->GetLastFallbackLocale(lastFallbackLocale);
+
+ nsTArray<nsCString> appLocales;
+ LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
+
+ ASSERT_TRUE(appLocales.Contains(lastFallbackLocale));
+}
+
+TEST(Intl_Locale_LocaleService, GetAppLocaleAsLangTag)
+{
+ nsTArray<nsCString> appLocales;
+ LocaleService::GetInstance()->GetAppLocalesAsLangTags(appLocales);
+
+ nsAutoCString locale;
+ LocaleService::GetInstance()->GetAppLocaleAsLangTag(locale);
+
+ ASSERT_TRUE(appLocales[0] == locale);
+}
+
+TEST(Intl_Locale_LocaleService, GetRegionalPrefsLocales)
+{
+ nsTArray<nsCString> rpLocales;
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
+
+ int32_t len = rpLocales.Length();
+ ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetWebExposedLocales)
+{
+ const nsTArray<nsCString> spoofLocale{"de"_ns};
+ LocaleService::GetInstance()->SetAvailableLocales(spoofLocale);
+ LocaleService::GetInstance()->SetRequestedLocales(spoofLocale);
+
+ nsTArray<nsCString> pvLocales;
+
+ mozilla::Preferences::SetInt("privacy.spoof_english", 0);
+ LocaleService::GetInstance()->GetWebExposedLocales(pvLocales);
+ ASSERT_TRUE(pvLocales.Length() > 0);
+ ASSERT_TRUE(pvLocales[0].Equals("de"_ns));
+
+ mozilla::Preferences::SetCString("intl.locale.privacy.web_exposed", "zh-TW");
+ LocaleService::GetInstance()->GetWebExposedLocales(pvLocales);
+ ASSERT_TRUE(pvLocales.Length() > 0);
+ ASSERT_TRUE(pvLocales[0].Equals("zh-TW"_ns));
+
+ mozilla::Preferences::SetInt("privacy.spoof_english", 2);
+ LocaleService::GetInstance()->GetWebExposedLocales(pvLocales);
+ ASSERT_EQ(1u, pvLocales.Length());
+ ASSERT_TRUE(pvLocales[0].Equals("en-US"_ns));
+}
+
+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, GetAvailableLocales)
+{
+ nsTArray<nsCString> availableLocales;
+ LocaleService::GetInstance()->GetAvailableLocales(availableLocales);
+
+ int32_t len = availableLocales.Length();
+ ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetPackagedLocales)
+{
+ nsTArray<nsCString> packagedLocales;
+ LocaleService::GetInstance()->GetPackagedLocales(packagedLocales);
+
+ int32_t len = packagedLocales.Length();
+ ASSERT_TRUE(len > 0);
+}
+
+TEST(Intl_Locale_LocaleService, GetDefaultLocale)
+{
+ nsAutoCString locStr;
+ LocaleService::GetInstance()->GetDefaultLocale(locStr);
+
+ ASSERT_FALSE(locStr.IsEmpty());
+ Locale loc;
+ ASSERT_TRUE(LocaleParser::TryParse(locStr, loc).isOk());
+}
+
+TEST(Intl_Locale_LocaleService, IsAppLocaleRTL)
+{
+ mozilla::Preferences::SetCString("intl.l10n.pseudo", "bidi");
+ ASSERT_TRUE(LocaleService::GetInstance()->IsAppLocaleRTL());
+ mozilla::Preferences::ClearUser("intl.l10n.pseudo");
+}
+
+TEST(Intl_Locale_LocaleService, TryCreateComponent)
+{
+ {
+ // Create a Collator with the app locale.
+ auto result = LocaleService::GetInstance()->TryCreateComponent<Collator>();
+ ASSERT_TRUE(result.isOk());
+ }
+ {
+ // Create a Collator with the "en" locale.
+ auto result =
+ LocaleService::GetInstance()->TryCreateComponentWithLocale<Collator>(
+ "en");
+ ASSERT_TRUE(result.isOk());
+ }
+ {
+ // Fallback to the app locale when an invalid one is used.
+ auto result =
+ LocaleService::GetInstance()->TryCreateComponentWithLocale<Collator>(
+ "$invalidName");
+ ASSERT_TRUE(result.isOk());
+ }
+}
diff --git a/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp b/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp
new file mode 100644
index 0000000000..c428e81c8d
--- /dev/null
+++ b/intl/locale/tests/gtest/TestLocaleServiceNegotiate.cpp
@@ -0,0 +1,53 @@
+/* -*- 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"
+
+using namespace mozilla::intl;
+
+TEST(Intl_Locale_LocaleService, Negotiate)
+{
+ nsTArray<nsCString> requestedLocales;
+ nsTArray<nsCString> availableLocales;
+ nsTArray<nsCString> supportedLocales;
+ nsAutoCString defaultLocale("en-US");
+ int32_t strategy = LocaleService::kLangNegStrategyFiltering;
+
+ requestedLocales.AppendElement("sr"_ns);
+
+ availableLocales.AppendElement("sr-Cyrl"_ns);
+ availableLocales.AppendElement("sr-Latn"_ns);
+
+ LocaleService::GetInstance()->NegotiateLanguages(
+ requestedLocales, availableLocales, defaultLocale, strategy,
+ supportedLocales);
+
+ ASSERT_TRUE(supportedLocales.Length() == 2);
+ ASSERT_TRUE(supportedLocales[0].EqualsLiteral("sr-Cyrl"));
+ ASSERT_TRUE(supportedLocales[1].EqualsLiteral("en-US"));
+}
+
+TEST(Intl_Locale_LocaleService, UseLSDefaultLocale)
+{
+ nsTArray<nsCString> requestedLocales;
+ nsTArray<nsCString> availableLocales;
+ nsTArray<nsCString> supportedLocales;
+ nsAutoCString defaultLocale("en-US");
+ int32_t strategy = LocaleService::kLangNegStrategyLookup;
+
+ requestedLocales.AppendElement("sr"_ns);
+
+ availableLocales.AppendElement("de"_ns);
+
+ LocaleService::GetInstance()->NegotiateLanguages(
+ requestedLocales, availableLocales, defaultLocale, strategy,
+ supportedLocales);
+
+ nsAutoCString lsDefaultLocale;
+ LocaleService::GetInstance()->GetDefaultLocale(lsDefaultLocale);
+ ASSERT_TRUE(supportedLocales.Length() == 1);
+ ASSERT_TRUE(supportedLocales[0].Equals(lsDefaultLocale));
+}
diff --git a/intl/locale/tests/gtest/TestOSPreferences.cpp b/intl/locale/tests/gtest/TestOSPreferences.cpp
new file mode 100644
index 0000000000..7e3b71582b
--- /dev/null
+++ b/intl/locale/tests/gtest/TestOSPreferences.cpp
@@ -0,0 +1,205 @@
+/* -*- 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/ArrayUtils.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/intl/OSPreferences.h"
+
+using namespace mozilla::intl;
+
+/**
+ * We test that on all platforms we test against (irrelevant of the tier),
+ * we will be able to retrieve at least a single locale out of the system.
+ *
+ * In theory, that may not be true, but if we encounter such platform we should
+ * decide how to handle this and special case and this test should make
+ * it not happen without us noticing.
+ */
+TEST(Intl_Locale_OSPreferences, GetSystemLocales)
+{
+ nsTArray<nsCString> systemLocales;
+ ASSERT_TRUE(NS_SUCCEEDED(
+ OSPreferences::GetInstance()->GetSystemLocales(systemLocales)));
+
+ ASSERT_FALSE(systemLocales.IsEmpty());
+}
+
+/**
+ * We test that on all platforms we test against (irrelevant of the tier),
+ * we will be able to retrieve at least a single locale out of the system.
+ *
+ * In theory, that may not be true, but if we encounter such platform we should
+ * decide how to handle this and special case and this test should make
+ * it not happen without us noticing.
+ */
+TEST(Intl_Locale_OSPreferences, GetRegionalPrefsLocales)
+{
+ nsTArray<nsCString> rgLocales;
+ ASSERT_TRUE(NS_SUCCEEDED(
+ OSPreferences::GetInstance()->GetRegionalPrefsLocales(rgLocales)));
+
+ ASSERT_FALSE(rgLocales.IsEmpty());
+}
+
+/**
+ * We test that on all platforms we test against,
+ * we will be able to retrieve a date and time pattern.
+ *
+ * This may come back empty on platforms where we don't have platforms
+ * bindings for, so effectively, we're testing for crashes. We should
+ * never crash.
+ */
+TEST(Intl_Locale_OSPreferences, GetDateTimePattern)
+{
+ nsAutoCString pattern;
+ OSPreferences* osprefs = OSPreferences::GetInstance();
+
+ struct Test {
+ int dateStyle;
+ int timeStyle;
+ const char* locale;
+ };
+ Test tests[] = {{0, 0, ""}, {1, 0, "pl"}, {2, 0, "de-DE"}, {3, 0, "fr"},
+ {4, 0, "ar"},
+
+ {0, 1, ""}, {0, 2, "it"}, {0, 3, ""}, {0, 4, "ru"},
+
+ {4, 1, ""}, {3, 2, "cs"}, {2, 3, ""}, {1, 4, "ja"}};
+
+ for (unsigned i = 0; i < mozilla::ArrayLength(tests); i++) {
+ const Test& t = tests[i];
+ if (NS_SUCCEEDED(osprefs->GetDateTimePattern(
+ t.dateStyle, t.timeStyle, nsDependentCString(t.locale), pattern))) {
+ ASSERT_TRUE((t.dateStyle == 0 && t.timeStyle == 0) || !pattern.IsEmpty());
+ }
+ }
+
+ // If the locale is not specified, we should get the pattern corresponding to
+ // the first regional prefs locale.
+ AutoTArray<nsCString, 10> rpLocales;
+ LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
+ ASSERT_TRUE(rpLocales.Length() > 0);
+
+ nsAutoCString rpLocalePattern;
+ ASSERT_TRUE(NS_SUCCEEDED(
+ osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleLong,
+ mozIOSPreferences::dateTimeFormatStyleLong,
+ rpLocales[0], rpLocalePattern)));
+ ASSERT_TRUE(NS_SUCCEEDED(
+ osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleLong,
+ mozIOSPreferences::dateTimeFormatStyleLong,
+ nsDependentCString(""), pattern)));
+ ASSERT_EQ(rpLocalePattern, pattern);
+}
+
+/**
+ * Test that is possible to override the OS defaults through a pref.
+ */
+TEST(Intl_Locale_OSPreferences, GetDateTimePatternPrefOverrides)
+{
+ nsresult nr;
+ nsAutoCString default_pattern, pattern;
+ OSPreferences* osprefs = OSPreferences::GetInstance();
+
+ struct {
+ const char* DatePref;
+ const char* TimePref;
+ int32_t DateTimeFormatStyle;
+ } configs[] = {{"intl.date_time.pattern_override.date_short",
+ "intl.date_time.pattern_override.time_short",
+ mozIOSPreferences::dateTimeFormatStyleShort},
+ {"intl.date_time.pattern_override.date_medium",
+ "intl.date_time.pattern_override.time_medium",
+ mozIOSPreferences::dateTimeFormatStyleMedium},
+ {"intl.date_time.pattern_override.date_long",
+ "intl.date_time.pattern_override.time_long",
+ mozIOSPreferences::dateTimeFormatStyleLong},
+ {"intl.date_time.pattern_override.date_full",
+ "intl.date_time.pattern_override.time_full",
+ mozIOSPreferences::dateTimeFormatStyleFull}};
+
+ for (const auto& config : configs) {
+ // Get default value for the OS
+ nr = osprefs->GetDateTimePattern(config.DateTimeFormatStyle,
+ mozIOSPreferences::dateTimeFormatStyleNone,
+ nsDependentCString(""), default_pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+
+ // Override date format
+ mozilla::Preferences::SetCString(config.DatePref, "yy-MM");
+ nr = osprefs->GetDateTimePattern(config.DateTimeFormatStyle,
+ mozIOSPreferences::dateTimeFormatStyleNone,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_TRUE(pattern.EqualsASCII("yy-MM"));
+
+ // Override time format
+ mozilla::Preferences::SetCString(config.TimePref, "HH:mm");
+ nr = osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleNone,
+ config.DateTimeFormatStyle,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_TRUE(pattern.EqualsASCII("HH:mm"));
+
+ // Override both
+ nr = osprefs->GetDateTimePattern(config.DateTimeFormatStyle,
+ config.DateTimeFormatStyle,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_TRUE(pattern.Find("yy-MM") != kNotFound);
+ ASSERT_TRUE(pattern.Find("HH:mm") != kNotFound);
+
+ // Clear overrides, we should get the default value back.
+ mozilla::Preferences::ClearUser(config.DatePref);
+ mozilla::Preferences::ClearUser(config.TimePref);
+ nr = osprefs->GetDateTimePattern(config.DateTimeFormatStyle,
+ mozIOSPreferences::dateTimeFormatStyleNone,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_EQ(default_pattern, pattern);
+ }
+
+ // Test overriding connector
+ nr = osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleShort,
+ mozIOSPreferences::dateTimeFormatStyleShort,
+ nsDependentCString(""), default_pattern);
+
+ mozilla::Preferences::SetCString("intl.date_time.pattern_override.date_short",
+ "yyyy-MM-dd");
+ mozilla::Preferences::SetCString("intl.date_time.pattern_override.time_short",
+ "HH:mm:ss");
+ mozilla::Preferences::SetCString(
+ "intl.date_time.pattern_override.connector_short", "{1} {0}");
+ nr = osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleShort,
+ mozIOSPreferences::dateTimeFormatStyleShort,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_TRUE(pattern.EqualsASCII("yyyy-MM-dd HH:mm:ss"));
+
+ // Reset to date and time to defaults
+ mozilla::Preferences::ClearUser("intl.date_time.pattern_override.date_short");
+ mozilla::Preferences::ClearUser("intl.date_time.pattern_override.time_short");
+
+ // Invalid patterns are ignored
+ mozilla::Preferences::SetCString(
+ "intl.date_time.pattern_override.connector_short", "hello, world!");
+ nr = osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleShort,
+ mozIOSPreferences::dateTimeFormatStyleShort,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_EQ(default_pattern, pattern);
+
+ // Clearing the override results in getting the default pattern back.
+ mozilla::Preferences::ClearUser(
+ "intl.date_time.pattern_override.connector_short");
+ nr = osprefs->GetDateTimePattern(mozIOSPreferences::dateTimeFormatStyleShort,
+ mozIOSPreferences::dateTimeFormatStyleShort,
+ nsDependentCString(""), pattern);
+ ASSERT_NS_SUCCEEDED(nr);
+ ASSERT_EQ(default_pattern, pattern);
+}
diff --git a/intl/locale/tests/gtest/moz.build b/intl/locale/tests/gtest/moz.build
new file mode 100644
index 0000000000..bd2a2b8f86
--- /dev/null
+++ b/intl/locale/tests/gtest/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestAppDateTimeFormat.cpp",
+ "TestLocaleService.cpp",
+ "TestLocaleServiceNegotiate.cpp",
+ "TestOSPreferences.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/intl/locale/tests/sort/us-ascii_base.txt b/intl/locale/tests/sort/us-ascii_base.txt
new file mode 100644
index 0000000000..3bc7fbc7be
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_base.txt
@@ -0,0 +1,95 @@
+
+!
+"
+#
+$
+%
+&
+'
+(
+)
+*
++
+,
+-
+.
+/
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+:
+;
+<
+=
+>
+?
+@
+A
+B
+C
+D
+E
+F
+G
+H
+I
+J
+K
+L
+M
+N
+O
+P
+Q
+R
+S
+T
+U
+V
+W
+X
+Y
+Z
+[
+\
+]
+^
+_
+`
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x
+y
+z
+{
+|
+}
+~
diff --git a/intl/locale/tests/sort/us-ascii_base_case_res.txt b/intl/locale/tests/sort/us-ascii_base_case_res.txt
new file mode 100644
index 0000000000..3e526d226b
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_base_case_res.txt
@@ -0,0 +1,96 @@
+'
+-
+
+!
+"
+#
+$
+%
+&
+(
+)
+*
+,
+.
+/
+:
+;
+?
+@
+[
+\
+]
+^
+_
+`
+{
+|
+}
+~
++
+<
+=
+>
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+a
+A
+b
+B
+c
+C
+d
+D
+e
+E
+f
+F
+g
+G
+h
+H
+i
+I
+j
+J
+k
+K
+l
+L
+m
+M
+n
+N
+o
+O
+p
+P
+q
+Q
+r
+R
+s
+S
+t
+T
+u
+U
+v
+V
+w
+W
+x
+X
+y
+Y
+z
+Z
+
diff --git a/intl/locale/tests/sort/us-ascii_base_nocase_res.txt b/intl/locale/tests/sort/us-ascii_base_nocase_res.txt
new file mode 100644
index 0000000000..2d18096db5
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_base_nocase_res.txt
@@ -0,0 +1,96 @@
+'
+-
+
+!
+"
+#
+$
+%
+&
+(
+)
+*
+,
+.
+/
+:
+;
+?
+@
+[
+\
+]
+^
+_
+`
+{
+|
+}
+~
++
+<
+=
+>
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+A
+a
+B
+b
+C
+c
+d
+D
+e
+E
+f
+F
+g
+G
+h
+H
+i
+I
+j
+J
+k
+K
+L
+l
+m
+M
+n
+N
+O
+o
+p
+P
+q
+Q
+r
+R
+s
+S
+t
+T
+u
+U
+v
+V
+w
+W
+X
+x
+Y
+y
+Z
+z
+
diff --git a/intl/locale/tests/sort/us-ascii_sort.txt b/intl/locale/tests/sort/us-ascii_sort.txt
new file mode 100644
index 0000000000..b6962f8de8
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_sort.txt
@@ -0,0 +1,78 @@
+ludwig van beethoven
+Ludwig van Beethoven
+Ludwig van beethoven
+Jane
+jane
+JANE
+jAne
+jaNe
+janE
+JAne
+JaNe
+JanE
+JANe
+JaNE
+JAnE
+jANE
+Umberto Eco
+Umberto eco
+umberto eco
+umberto Eco
+UMBERTO ECO
+ace
+bash
+*ace
+!ace
+%ace
+~ace
+#ace
+cork
+denizen
+[denizen]
+(denizen)
+{denizen}
+/denizen/
+#denizen#
+$denizen$
+@denizen
+elf
+full
+gnome
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese
+hint
+Internationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio
+n
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion
+jostle
+kin
+Laymen
+lumens
+Lumens
+motleycrew
+motley crew
+niven's creative talents
+nivens creative talents
+opie
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies
+quilt's
+quilts
+quilt
+Rondo
+street
+tamale oxidization and iodization in rapid progress
+tamale oxidization and iodization in rapid Progress
+until
+vera
+Wobble
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname
+yearn
+zodiac
+
diff --git a/intl/locale/tests/sort/us-ascii_sort_case_res.txt b/intl/locale/tests/sort/us-ascii_sort_case_res.txt
new file mode 100644
index 0000000000..2720a83426
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_sort_case_res.txt
@@ -0,0 +1,79 @@
+!ace
+#ace
+#denizen#
+$denizen$
+%ace
+(denizen)
+*ace
+/denizen/
+@denizen
+[denizen]
+{denizen}
+~ace
+ace
+bash
+cork
+denizen
+elf
+full
+gnome
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese
+hint
+Internationalization
+jane
+janE
+jaNe
+jAne
+jANE
+Jane
+JanE
+JaNe
+JaNE
+JAne
+JAnE
+JANe
+JANE
+jostle
+kin
+Laymen
+ludwig van beethoven
+Ludwig van beethoven
+Ludwig van Beethoven
+lumens
+Lumens
+motley crew
+motleycrew
+nivens creative talents
+niven's creative talents
+opie
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies
+quilt
+quilts
+quilt's
+Rondo
+street
+tamale oxidization and iodization in rapid progress
+tamale oxidization and iodization in rapid Progress
+umberto eco
+umberto Eco
+Umberto eco
+Umberto Eco
+UMBERTO ECO
+until
+veda
+Veda
+vera
+Vera
+Wobble
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme
+yearn
+Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization
+zodiac
diff --git a/intl/locale/tests/sort/us-ascii_sort_nocase_res.txt b/intl/locale/tests/sort/us-ascii_sort_nocase_res.txt
new file mode 100644
index 0000000000..fa3426d4e9
--- /dev/null
+++ b/intl/locale/tests/sort/us-ascii_sort_nocase_res.txt
@@ -0,0 +1,79 @@
+!ace
+#ace
+#denizen#
+$denizen$
+%ace
+(denizen)
+*ace
+/denizen/
+@denizen
+[denizen]
+{denizen}
+~ace
+ace
+bash
+cork
+denizen
+elf
+full
+gnome
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese
+hint
+Internationalization
+JANe
+jANE
+JAnE
+JaNE
+Jane
+JanE
+JaNe
+JAne
+janE
+jaNe
+jAne
+JANE
+jane
+jostle
+kin
+Laymen
+Ludwig van beethoven
+Ludwig van Beethoven
+ludwig van beethoven
+Lumens
+lumens
+motley crew
+motleycrew
+nivens creative talents
+niven's creative talents
+opie
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies
+quilt
+quilts
+quilt's
+Rondo
+street
+tamale oxidization and iodization in rapid Progress
+tamale oxidization and iodization in rapid progress
+Umberto eco
+Umberto Eco
+umberto eco
+UMBERTO ECO
+umberto Eco
+until
+Veda
+veda
+Vera
+vera
+Wobble
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme
+yearn
+Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+zodiac
diff --git a/intl/locale/tests/unit/data/chrome.manifest b/intl/locale/tests/unit/data/chrome.manifest
new file mode 100644
index 0000000000..a8678deef3
--- /dev/null
+++ b/intl/locale/tests/unit/data/chrome.manifest
@@ -0,0 +1 @@
+content locale ./
diff --git a/intl/locale/tests/unit/data/intl_on_workers_worker.js b/intl/locale/tests/unit/data/intl_on_workers_worker.js
new file mode 100644
index 0000000000..e6f01e71a8
--- /dev/null
+++ b/intl/locale/tests/unit/data/intl_on_workers_worker.js
@@ -0,0 +1,6 @@
+/* eslint-env worker */
+
+self.onmessage = function (data) {
+ let myLocale = Intl.NumberFormat().resolvedOptions().locale;
+ self.postMessage(myLocale);
+};
diff --git a/intl/locale/tests/unit/test_bug1086527.js b/intl/locale/tests/unit/test_bug1086527.js
new file mode 100644
index 0000000000..5e33ce060a
--- /dev/null
+++ b/intl/locale/tests/unit/test_bug1086527.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+/**
+ * This unit test makes sure that PluralForm.get can be called from strict mode
+ */
+
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+delete PluralForm.numForms;
+delete PluralForm.get;
+[PluralForm.get, PluralForm.numForms] = PluralForm.makeGetter(9);
+
+function run_test() {
+ "use strict";
+
+ Assert.equal(3, PluralForm.numForms());
+ Assert.equal("one", PluralForm.get(5, "one;many"));
+}
diff --git a/intl/locale/tests/unit/test_bug22310.js b/intl/locale/tests/unit/test_bug22310.js
new file mode 100644
index 0000000000..c14296d931
--- /dev/null
+++ b/intl/locale/tests/unit/test_bug22310.js
@@ -0,0 +1,65 @@
+String.prototype.has = function (s) {
+ return this.includes(s);
+};
+
+function dt(locale) {
+ var date = new Date("2008-06-30T13:56:34");
+ const dtOptions = {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ };
+ return date.toLocaleString(locale, dtOptions);
+}
+
+var all_passed = true;
+const tests = [
+ [dt("en-US").has("June"), "month name in en-US"],
+ [dt("en-US").has("2008"), "year in en-US"],
+ [dt("da").has("jun"), "month name in da"],
+ [dt("da-DK") == dt("da"), "da same as da-DK"],
+ [
+ dt("en-GB").has("30") &&
+ dt("en-GB").has("June") &&
+ dt("en-GB").indexOf("30") < dt("en-GB").indexOf("June"),
+ "day before month in en-GB",
+ ],
+ [
+ dt("en-US").has("30") &&
+ dt("en-US").has("June") &&
+ dt("en-US").indexOf("30") > dt("en-US").indexOf("June"),
+ "month before day in en-US",
+ ],
+ [dt("ja-JP").has("\u5E746\u670830\u65E5"), "year month and day in ja-JP"],
+ // The Firefox locale code ja-JP-mac will be resolved to a BCP47-compliant
+ // tag ja-JP-x-lvariant-mac by uloc_toLanguageTag
+ [
+ dt("ja-JP") == dt("ja-JP-x-lvariant-mac"),
+ "ja-JP-x-lvariant-mac same as ja-JP",
+ ],
+ [dt("nn-NO").has("juni"), "month name in nn-NO"],
+ [dt("nb-NO").has("juni"), "month name in nb-NO"],
+ // Bug 1261775 - failures on win10
+ //[dt("no-NO").has("30. juni"), "month name in no-NO"],
+ [dt("sv-SE").has("30 jun"), "month name in sv-SE"],
+ [dt("kok").has("\u091C\u0942\u0928"), "month name in kok"],
+ [dt("ta-IN").has("\u0B9C\u0BC2\u0BA9\u0BCD"), "month name in ta-IN"],
+ [!!dt("ab-CD").length, "fallback for ab-CD"],
+];
+
+function one_test(testcase, msg) {
+ if (!testcase) {
+ all_passed = false;
+ dump("Unexpected date format: " + msg + "\n");
+ }
+}
+
+function run_test() {
+ for (var i = 0; i < tests.length; ++i) {
+ one_test(tests[i][0], tests[i][1]);
+ }
+ Assert.ok(all_passed);
+}
diff --git a/intl/locale/tests/unit/test_intl_on_workers.js b/intl/locale/tests/unit/test_intl_on_workers.js
new file mode 100644
index 0000000000..b5e05c4678
--- /dev/null
+++ b/intl/locale/tests/unit/test_intl_on_workers.js
@@ -0,0 +1,29 @@
+function run_test() {
+ do_load_manifest("data/chrome.manifest");
+
+ if (typeof Intl !== "object") {
+ dump("Intl not enabled, skipping test\n");
+ equal(true, true);
+ return;
+ }
+
+ let mainThreadLocale = Intl.NumberFormat().resolvedOptions().locale;
+ let testWorker = new Worker(
+ "chrome://locale/content/intl_on_workers_worker.js"
+ );
+ testWorker.onmessage = function (e) {
+ try {
+ let workerLocale = e.data;
+ equal(
+ mainThreadLocale,
+ workerLocale,
+ "Worker should inherit Intl locale from main thread."
+ );
+ } finally {
+ do_test_finished();
+ }
+ };
+
+ do_test_pending();
+ testWorker.postMessage("go!");
+}
diff --git a/intl/locale/tests/unit/test_langPackMatcher.js b/intl/locale/tests/unit/test_langPackMatcher.js
new file mode 100644
index 0000000000..03adec7bff
--- /dev/null
+++ b/intl/locale/tests/unit/test_langPackMatcher.js
@@ -0,0 +1,287 @@
+/* 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 { getAddonAndLocalAPIsMocker } = ChromeUtils.importESModule(
+ "resource://testing-common/LangPackMatcherTestUtils.sys.mjs"
+);
+const { LangPackMatcher } = ChromeUtils.importESModule(
+ "resource://gre/modules/LangPackMatcher.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const sandbox = sinon.createSandbox();
+const mockAddonAndLocaleAPIs = getAddonAndLocalAPIsMocker(this, sandbox);
+
+add_task(function initSandbox() {
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(function test_appLocaleLanguageMismatch() {
+ sandbox.restore();
+ mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "es-ES",
+ systemLocale: { baseName: "es-ES", language: "es", region: "ES" },
+ appLocaleRaw: "en-US",
+ appLocale: { baseName: "en-US", language: "en", region: "US" },
+ matchType: "language-mismatch",
+ canLiveReload: true,
+ displayNames: {
+ systemLanguage: "Español (ES)",
+ appLanguage: "English (US)",
+ },
+ });
+});
+
+add_task(function test_appLocaleRegionMismatch() {
+ sandbox.restore();
+ mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "en-CA",
+ appLocale: "en-US",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "en-CA",
+ systemLocale: { baseName: "en-CA", language: "en", region: "CA" },
+ appLocaleRaw: "en-US",
+ appLocale: { baseName: "en-US", language: "en", region: "US" },
+ matchType: "region-mismatch",
+ canLiveReload: true,
+ displayNames: {
+ systemLanguage: "English (CA)",
+ appLanguage: "English (US)",
+ },
+ });
+});
+
+add_task(function test_appLocaleScriptMismatch() {
+ sandbox.restore();
+ // Script mismatch:
+ mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "zh-Hans-CN",
+ appLocale: "zh-CN",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "zh-Hans-CN",
+ systemLocale: { baseName: "zh-Hans-CN", language: "zh", region: "CN" },
+ appLocaleRaw: "zh-CN",
+ appLocale: { baseName: "zh-CN", language: "zh", region: "CN" },
+ matchType: "match",
+ canLiveReload: true,
+ displayNames: {
+ systemLanguage: "Chinese (Hans, China)",
+ appLanguage: "简体中文",
+ },
+ });
+});
+
+add_task(function test_appLocaleInvalidSystem() {
+ sandbox.restore();
+ // Script mismatch:
+ mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "Not valid",
+ appLocale: "en-US",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "Not valid",
+ systemLocale: null,
+ appLocaleRaw: "en-US",
+ appLocale: { baseName: "en-US", language: "en", region: "US" },
+ matchType: "unknown",
+ canLiveReload: null,
+ displayNames: { systemLanguage: null, appLanguage: "English (US)" },
+ });
+});
+
+add_task(function test_bidiSwitchDisabled() {
+ Services.prefs.setBoolPref(
+ "intl.multilingual.liveReloadBidirectional",
+ false
+ );
+ sandbox.restore();
+ // Script mismatch:
+ mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "ar-EG",
+ appLocale: "en-US",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "ar-EG",
+ systemLocale: { baseName: "ar-EG", language: "ar", region: "EG" },
+ appLocaleRaw: "en-US",
+ appLocale: { baseName: "en-US", language: "en", region: "US" },
+ matchType: "language-mismatch",
+ canLiveReload: false,
+ displayNames: {
+ systemLanguage: "Arabic (Egypt)",
+ appLanguage: "English (US)",
+ },
+ });
+ Services.prefs.clearUserPref("intl.multilingual.liveReloadBidirectional");
+});
+
+add_task(async function test_bidiSwitchEnabled() {
+ Services.prefs.setBoolPref("intl.multilingual.liveReloadBidirectional", true);
+ sandbox.restore();
+ // Script mismatch:
+ mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "ar-EG",
+ appLocale: "en-US",
+ });
+
+ deepEqual(LangPackMatcher.getAppAndSystemLocaleInfo(), {
+ systemLocaleRaw: "ar-EG",
+ systemLocale: { baseName: "ar-EG", language: "ar", region: "EG" },
+ appLocaleRaw: "en-US",
+ appLocale: { baseName: "en-US", language: "en", region: "US" },
+ matchType: "language-mismatch",
+ canLiveReload: true,
+ displayNames: {
+ systemLanguage: "Arabic (Egypt)",
+ appLanguage: "English (US)",
+ },
+ });
+
+ Services.prefs.clearUserPref("intl.multilingual.liveReloadBidirectional");
+});
+
+function shuffle(array) {
+ return array
+ .map(value => ({ value, sort: Math.random() }))
+ .sort((a, b) => a.sort - b.sort)
+ .map(({ value }) => value);
+}
+
+add_task(async function test_negotiateLangPacks() {
+ const negotiations = [
+ {
+ // Exact match found.
+ systemLocale: "en-US",
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: "en-US",
+ expectedDisplayName: "English (US)",
+ },
+ {
+ // Region-less match.
+ systemLocale: "en-CA",
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: "en",
+ expectedDisplayName: "English",
+ },
+ {
+ // Fallback to a different region.
+ systemLocale: "en-CA",
+ availableLangPacks: ["en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: "en-US",
+ expectedDisplayName: "English (US)",
+ },
+ {
+ // Match with a script. zh-Hans-CN is the locale used with simplified
+ // Chinese scripts, while zh-CN uses the Latin script.
+ systemLocale: "zh-Hans-CN",
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-TW", "zh-Hans-CN"],
+ expectedLangPack: "zh-Hans-CN",
+ expectedDisplayName: "Chinese (Hans, China)",
+ },
+ {
+ // Match excluding script but matching region. zh-Hant-TW should
+ // match zh-TW before zh-CN.
+ systemLocale: "zh-Hant-TW",
+ availableLangPacks: ["en", "zh", "zh-CN", "zh-TW"],
+ expectedLangPack: "zh-TW",
+ expectedDisplayName: "正體中文",
+ },
+ {
+ // No reasonable match could be found.
+ systemLocale: "tlh", // Klingon
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: null,
+ expectedDisplayName: null,
+ },
+ {
+ // Weird, but valid locale identifiers.
+ systemLocale: "en-US-u-hc-h23-ca-islamic-civil-ss-true",
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: "en-US",
+ expectedDisplayName: "English (US)",
+ },
+ {
+ // Invalid system locale
+ systemLocale: "Not valid",
+ availableLangPacks: ["en", "en-US", "zh", "zh-CN", "zh-Hans-CN"],
+ expectedLangPack: null,
+ expectedDisplayName: null,
+ },
+ ];
+
+ for (const {
+ systemLocale,
+ availableLangPacks,
+ expectedLangPack,
+ expectedDisplayName,
+ } of negotiations) {
+ sandbox.restore();
+ const { resolveLangPacks } = mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale,
+ });
+
+ const promise = LangPackMatcher.negotiateLangPackForLanguageMismatch();
+ // Shuffle the order to ensure that this test doesn't require on ordering of the
+ // langpack responses.
+ resolveLangPacks(shuffle(availableLangPacks));
+ const { langPack, langPackDisplayName } = await promise;
+ equal(
+ langPack?.target_locale,
+ expectedLangPack,
+ `Resolve the systemLocale "${systemLocale}" with available langpacks: ${JSON.stringify(
+ availableLangPacks
+ )}`
+ );
+ equal(
+ langPackDisplayName,
+ expectedDisplayName,
+ "The display name matches."
+ );
+ }
+});
+
+add_task(async function test_ensureLangPackInstalled() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
+ sandbox,
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ const negotiatePromise =
+ LangPackMatcher.negotiateLangPackForLanguageMismatch();
+ resolveLangPacks(["es-ES"]);
+ const { langPack } = await negotiatePromise;
+
+ const installPromise1 = LangPackMatcher.ensureLangPackInstalled(langPack);
+ const installPromise2 = LangPackMatcher.ensureLangPackInstalled(langPack);
+
+ resolveInstaller(["fake langpack"]);
+
+ info("Ensure both installers resolve when called twice in a row.");
+ await installPromise1;
+ await installPromise2;
+ ok(true, "Both were called.");
+});
diff --git a/intl/locale/tests/unit/test_localeService.js b/intl/locale/tests/unit/test_localeService.js
new file mode 100644
index 0000000000..ae2d949e80
--- /dev/null
+++ b/intl/locale/tests/unit/test_localeService.js
@@ -0,0 +1,240 @@
+/* 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 osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+);
+
+const localeService = Services.locale;
+
+/**
+ * Make sure the locale service can be instantiated.
+ */
+
+add_test(function test_defaultLocale() {
+ const defaultLocale = localeService.defaultLocale;
+ Assert.ok(defaultLocale.length !== 0, "Default locale is not empty");
+ run_next_test();
+});
+
+add_test(function test_lastFallbackLocale() {
+ const lastFallbackLocale = localeService.lastFallbackLocale;
+ Assert.ok(lastFallbackLocale === "en-US", "Last fallback locale is en-US");
+ run_next_test();
+});
+
+add_test(function test_appLocalesAsLangTags() {
+ const appLocale = localeService.appLocaleAsLangTag;
+ Assert.ok(appLocale != "", "appLocale is non-empty");
+
+ const appLocales = localeService.appLocalesAsLangTags;
+ Assert.ok(Array.isArray(appLocales), "appLocales returns an array");
+
+ Assert.ok(
+ appLocale == appLocales[0],
+ "appLocale matches first entry in appLocales"
+ );
+
+ const enUSLocales = appLocales.filter(loc => loc === "en-US");
+ Assert.ok(enUSLocales.length == 1, "en-US is present exactly one time");
+
+ run_next_test();
+});
+
+const PREF_REQUESTED_LOCALES = "intl.locale.requested";
+const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed";
+
+add_test(function test_requestedLocales() {
+ const requestedLocales = localeService.requestedLocales;
+ Assert.ok(
+ 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 requestedLocales.
+ */
+add_test(function test_requestedLocales_matchOS() {
+ do_test_pending();
+
+ Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "ar-IR");
+
+ const observer = {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case REQ_LOC_CHANGE_EVENT:
+ const reqLocs = localeService.requestedLocales;
+ Assert.ok(reqLocs[0] === osPrefs.systemLocale);
+ Services.obs.removeObserver(observer, REQ_LOC_CHANGE_EVENT);
+ do_test_finished();
+ }
+ },
+ };
+
+ Services.obs.addObserver(observer, REQ_LOC_CHANGE_EVENT);
+ Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "");
+
+ 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_requestedLocales_onChange() {
+ do_test_pending();
+
+ Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "ar-IR");
+
+ const observer = {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case REQ_LOC_CHANGE_EVENT:
+ const reqLocs = localeService.requestedLocales;
+ Assert.ok(reqLocs[0] === "sr-RU");
+ Services.obs.removeObserver(observer, REQ_LOC_CHANGE_EVENT);
+ do_test_finished();
+ }
+ },
+ };
+
+ Services.obs.addObserver(observer, REQ_LOC_CHANGE_EVENT);
+ Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "sr-RU");
+
+ run_next_test();
+});
+
+add_test(function test_requestedLocale() {
+ Services.prefs.setCharPref(PREF_REQUESTED_LOCALES, "tlh");
+
+ let requestedLocale = localeService.requestedLocale;
+ Assert.ok(
+ requestedLocale === "tlh",
+ "requestedLocale returns the right value"
+ );
+
+ Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
+
+ run_next_test();
+});
+
+add_test(function test_requestedLocales() {
+ localeService.requestedLocales = ["de-AT", "de-DE", "de-CH"];
+
+ let locales = localeService.requestedLocales;
+ Assert.ok(locales[0] === "de-AT");
+ Assert.ok(locales[1] === "de-DE");
+ Assert.ok(locales[2] === "de-CH");
+
+ run_next_test();
+});
+
+add_test(function test_isAppLocaleRTL() {
+ Assert.ok(typeof localeService.isAppLocaleRTL === "boolean");
+
+ run_next_test();
+});
+
+add_test(function test_isAppLocaleRTL_pseudo() {
+ let avLocales = localeService.availableLocales;
+ let reqLocales = localeService.requestedLocales;
+
+ localeService.availableLocales = ["en-US"];
+ localeService.requestedLocales = ["en-US"];
+ Services.prefs.setCharPref("intl.l10n.pseudo", "");
+
+ Assert.ok(localeService.isAppLocaleRTL === false);
+
+ Services.prefs.setCharPref("intl.l10n.pseudo", "bidi");
+ Assert.ok(localeService.isAppLocaleRTL === true);
+
+ Services.prefs.setCharPref("intl.l10n.pseudo", "accented");
+ Assert.ok(localeService.isAppLocaleRTL === false);
+
+ // Clean up
+ localeService.availableLocales = avLocales;
+ localeService.requestedLocales = reqLocales;
+ Services.prefs.clearUserPref("intl.l10n.pseudo");
+
+ run_next_test();
+});
+
+add_test(function test_packagedLocales() {
+ const locales = localeService.packagedLocales;
+ Assert.ok(locales.length !== 0, "Packaged locales are empty");
+ run_next_test();
+});
+
+add_test(function test_availableLocales() {
+ const avLocales = localeService.availableLocales;
+ localeService.availableLocales = ["und", "ar-IR"];
+
+ let locales = localeService.availableLocales;
+ Assert.ok(locales.length == 2);
+ Assert.ok(locales[0] === "und");
+ Assert.ok(locales[1] === "ar-IR");
+
+ localeService.availableLocales = avLocales;
+
+ run_next_test();
+});
+
+/**
+ * This test verifies that all values coming from the pref are sanitized.
+ */
+add_test(function test_requestedLocales_sanitize() {
+ Services.prefs.setStringPref(
+ PREF_REQUESTED_LOCALES,
+ "de,2,#$@#,pl,ąó,!a2,DE-at,,;"
+ );
+
+ let locales = localeService.requestedLocales;
+ Assert.equal(locales[0], "de");
+ Assert.equal(locales[1], "pl");
+ Assert.equal(locales[2], "de-AT");
+ Assert.equal(locales.length, 3);
+
+ Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
+
+ run_next_test();
+});
+
+add_test(function test_handle_ja_JP_mac() {
+ const bkpAvLocales = localeService.availableLocales;
+
+ localeService.availableLocales = ["ja-JP-mac", "en-US"];
+
+ localeService.requestedLocales = ["ja-JP-mac"];
+
+ let reqLocales = localeService.requestedLocales;
+ Assert.equal(reqLocales[0], "ja-JP-macos");
+
+ let avLocales = localeService.availableLocales;
+ Assert.equal(avLocales[0], "ja-JP-macos");
+
+ let appLocales = localeService.appLocalesAsBCP47;
+ Assert.equal(appLocales[0], "ja-JP-macos");
+
+ let appLocalesAsLT = localeService.appLocalesAsLangTags;
+ Assert.equal(appLocalesAsLT[0], "ja-JP-mac");
+
+ Assert.equal(localeService.appLocaleAsLangTag, "ja-JP-mac");
+
+ localeService.availableLocales = bkpAvLocales;
+
+ run_next_test();
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_REQUESTED_LOCALES);
+});
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 0000000000..f929bc3a43
--- /dev/null
+++ b/intl/locale/tests/unit/test_localeService_negotiateLanguages.js
@@ -0,0 +1,205 @@
+/* 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 = Services.locale;
+
+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-windows"], ["en-Latn-US-windows"], ["en-Latn-US-windows"]],
+ [["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-windows"], ["en-US"], ["en-US"]],
+ [
+ ["fr-CA", "de-DE"],
+ ["fr", "it", "de"],
+ ["fr", "de"],
+ ],
+ [["ja-JP-windows"], ["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 likelySubtag region over other regions": [
+ [["en-CA"], ["en-ZA", "en-GB", "en-US"], ["en-US", "en-ZA", "en-GB"]],
+ ],
+ "should match cross-region": [
+ [["en"], ["en-US"], ["en-US"]],
+ [["en-US"], ["en-GB"], ["en-GB"]],
+ [["en-Latn-US"], ["en-Latn-GB"], ["en-Latn-GB"]],
+ ],
+ "should match cross-variant": [
+ [["en-US-linux"], ["en-US-windows"], ["en-US-windows"]],
+ ],
+ "should prioritize properly": [
+ // exact match first
+ [
+ ["en-US"],
+ ["en-US-windows", "en", "en-US"],
+ ["en-US", "en", "en-US-windows"],
+ ],
+ // 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-macos"],
+ ["en-US-windows", "en-GB-macos"],
+ ["en-US-windows", "en-GB-macos"],
+ ],
+ // regional range fifth
+ [["en-US-macos"], ["en-GB-windows"], ["en-GB-windows"]],
+ [["en-US"], ["en-GB", "en"], ["en", "en-GB"]],
+ [
+ ["fr-CA-macos", "de-DE"],
+ ["de-DE", "fr-FR-windows"],
+ ["fr-FR-windows", "de-DE"],
+ ],
+ ],
+ "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-macos", "de-DE"],
+ ["de-DE", "fr-FR-windows"],
+ ["fr-FR-windows", "de-DE"],
+ ],
+ ],
+ "should handle cases and underscores, returning the form given in the 'available' list":
+ [
+ [["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_macos"], ["fr_Cyrl_fr-macos"], ["fr_Cyrl_fr-macos"]],
+ ],
+ "should handle mozilla specific 3-letter variants": [
+ [
+ ["ja-JP-mac", "de-DE"],
+ ["ja-JP-mac", "de-DE"],
+ ["ja-JP-mac", "de-DE"],
+ ],
+ ],
+ "should not crash on invalid input": [
+ [["fą-FŻ"], ["ór_Fń"], []],
+ [["2"], ["ąóżł"], []],
+ [[[""]], ["fr-FR"], []],
+ ],
+ "should not match on invalid input": [[["en"], ["ťŮ"], []]],
+ },
+ 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 strategyInner = test.length > 4 ? test[3] : undefined;
+ const supported = test[test.length - 1];
+
+ const result = nl(test[0], test[1], defaultLocale, strategyInner);
+ deepEqual(
+ result,
+ supported,
+ `\nExpected ${json(requested)} * ${json(available)} = ${json(
+ supported
+ )}.\n`
+ );
+ }
+ }
+ }
+
+ // Verify that we error out when requested or available is not an array.
+ for (const [req, avail] in [
+ [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"],
+ [[null], []],
+ [[undefined], []],
+ [[undefined], [null]],
+ [[undefined], [undefined]],
+ [[null], [null]],
+ [[[]], [[2]]],
+ ]) {
+ Assert.throws(
+ () => {
+ nl(req, avail);
+ },
+ err => err.result == Cr.NS_ERROR_XPC_CANT_CONVERT_PRIMITIVE_TO_ARRAY
+ );
+ }
+}
diff --git a/intl/locale/tests/unit/test_osPreferences.js b/intl/locale/tests/unit/test_osPreferences.js
new file mode 100644
index 0000000000..6d1bb637ff
--- /dev/null
+++ b/intl/locale/tests/unit/test_osPreferences.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+function run_test() {
+ const osprefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ );
+
+ const systemLocale = osprefs.systemLocale;
+ Assert.ok(systemLocale != "", "systemLocale is non-empty");
+
+ const systemLocales = osprefs.systemLocales;
+ Assert.ok(Array.isArray(systemLocales), "systemLocales returns an array");
+
+ Assert.ok(
+ systemLocale == systemLocales[0],
+ "systemLocale matches first entry in systemLocales"
+ );
+
+ const rgLocales = osprefs.regionalPrefsLocales;
+ Assert.ok(Array.isArray(rgLocales), "regionalPrefsLocales returns an array");
+
+ const getDateTimePatternTests = [
+ [osprefs.dateTimeFormatStyleNone, osprefs.dateTimeFormatStyleNone, ""],
+ [osprefs.dateTimeFormatStyleShort, osprefs.dateTimeFormatStyleNone, ""],
+ [osprefs.dateTimeFormatStyleNone, osprefs.dateTimeFormatStyleLong, "ar"],
+ [osprefs.dateTimeFormatStyleFull, osprefs.dateTimeFormatStyleMedium, "ru"],
+ ];
+
+ for (let i = 0; i < getDateTimePatternTests.length; i++) {
+ const test = getDateTimePatternTests[i];
+
+ const pattern = osprefs.getDateTimePattern(...test);
+ if (
+ test[0] !== osprefs.dateTimeFormatStyleNone &&
+ test[1] !== osprefs.dateTImeFormatStyleNone
+ ) {
+ Assert.greater(pattern.length, 0, "pattern is not empty.");
+ }
+ }
+
+ Assert.ok(1, "osprefs didn't crash");
+}
diff --git a/intl/locale/tests/unit/test_pluralForm.js b/intl/locale/tests/unit/test_pluralForm.js
new file mode 100644
index 0000000000..e7717d20c1
--- /dev/null
+++ b/intl/locale/tests/unit/test_pluralForm.js
@@ -0,0 +1,390 @@
+/* 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/. */
+
+/**
+ * Make sure each of the plural forms have the correct number of forms and
+ * match up in functionality.
+ */
+
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+function run_test() {
+ let allExpect = [
+ [
+ // 0: Chinese 0-9, 10-19, ..., 90-99
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // 100-109, 110-119, ..., 190-199
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ // 200-209, 210-219, ..., 290-299
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ ],
+ [
+ // 1: English 0-9, 10-19, ..., 90-99
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 100-109, 110-119, ..., 190-199
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 200-209, 210-219, ..., 290-299
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ ],
+ [
+ // 2: French 0-9, 10-19, ..., 90-99
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 100-109, 110-119, ..., 190-199
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 200-209, 210-219, ..., 290-299
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ ],
+ [
+ // 3: Latvian 0-9, 10-19, ..., 90-99
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3,
+ 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 4: Scottish Gaelic 0-9, 10-19, ..., 90-99
+ 4, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 100-109, 110-119, ..., 190-199
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 200-209, 210-219, ..., 290-299
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ ],
+ [
+ // 5: Romanian 0-9, 10-19, ..., 90-99
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 6: Lithuanian 0-9, 10-19, ..., 90-99
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3,
+ 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 7: Russian 0-9, 10-19, ..., 90-99
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 8: Slovak 0-9, 10-19, ..., 90-99
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 9: Polish 0-9, 10-19, ..., 90-99
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 10: Slovenian 0-9, 10-19, ..., 90-99
+ 4, 1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 100-109, 110-119, ..., 190-199
+ 4, 1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 200-209, 210-219, ..., 290-299
+ 4, 1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ ],
+ [
+ // 11: Irish Gaeilge 0-9, 10-19, ..., 90-99
+ 5, 1, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ // 100-109, 110-119, ..., 190-199
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ // 200-209, 210-219, ..., 290-299
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ ],
+ [
+ // 12: Arabic 0-9, 10-19, ..., 90-99
+ 6, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 100-109, 110-119, ..., 190-199
+ 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 200-209, 210-219, ..., 290-299
+ 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ ],
+ [
+ // 13: Maltese 0-9, 10-19, ..., 90-99
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 100-109, 110-119, ..., 190-199
+ 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ // 200-209, 210-219, ..., 290-299
+ 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ ],
+ [
+ // 14: Unused 0-9, 10-19, ..., 90-99
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3,
+ 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 3, 3, 3, 3, 3, 3,
+ ],
+ [
+ // 15: Icelandic, Macedonian 0-9, 10-19, ..., 90-99
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 100-109, 110-119, ..., 190-199
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 200-209, 210-219, ..., 290-299
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2,
+ ],
+ [
+ // 16: Breton 0-9, 10-19, ..., 90-99
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 2, 3, 3,
+ 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3,
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ // 100-109, 110-119, ..., 190-199
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 2, 3, 3,
+ 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3,
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ // 200-209, 210-219, ..., 290-299
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 2, 3, 3,
+ 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3,
+ 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 1, 2, 3, 3, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ ],
+ [
+ // 17: Shuar 0-9, 10-19, ..., 90-99
+ 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 100-109, 110-119, ..., 190-199
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ // 200-209, 210-219, ..., 290-299
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ ],
+ [
+ // 18: Welsh 0-9, 10-19, ..., 90-99
+ 1, 2, 3, 4, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ // 100-109, 110-119, ..., 190-199
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ // 200-209, 210-219, ..., 290-299
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ ],
+ [
+ // 19: Slavic languages (bs, hr, sr) 0-9, 10-19, ..., 90-99
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 100-109, 110-119, ..., 190-199
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ // 200-209, 210-219, ..., 290-299
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 1, 2, 2, 2, 3, 3, 3, 3, 3,
+ ],
+ ];
+
+ for (let [rule, expect] of allExpect.entries()) {
+ print("\nTesting rule #" + rule);
+
+ let [get, numForms] = PluralForm.makeGetter(rule);
+
+ // Make sure the largest value expected matches the number of plural forms
+ let maxExpect = Math.max.apply(this, expect);
+ Assert.equal(maxExpect, numForms());
+
+ // Make a string of numbers, e.g., 1;2;3;4;5
+ let words = [];
+ for (let i = 1; i <= maxExpect; i++) {
+ words.push(i);
+ }
+ words = words.join(";");
+
+ // Make sure we get the expected number
+ for (let [index, number] of expect.entries()) {
+ print(
+ [
+ "Plural form of ",
+ index,
+ " should be ",
+ number,
+ " (",
+ words,
+ ")",
+ ].join("")
+ );
+ Assert.equal(get(index, words), number);
+ }
+ }
+}
diff --git a/intl/locale/tests/unit/test_pluralForm_english.js b/intl/locale/tests/unit/test_pluralForm_english.js
new file mode 100644
index 0000000000..7ad221e885
--- /dev/null
+++ b/intl/locale/tests/unit/test_pluralForm_english.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+/**
+ * This unit test makes sure the plural form for the default language (by
+ * development), English, is working for the PluralForm javascript module.
+ */
+
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+function run_test() {
+ // English has 2 plural forms
+ Assert.equal(2, PluralForm.numForms());
+
+ // Make sure for good inputs, things work as expected
+ for (var num = 0; num <= 200; num++) {
+ Assert.equal(
+ num == 1 ? "word" : "words",
+ PluralForm.get(num, "word;words")
+ );
+ }
+
+ // Not having enough plural forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word"));
+
+ // Empty forms defaults to the first form
+ Assert.equal("word", PluralForm.get(2, "word;"));
+}
diff --git a/intl/locale/tests/unit/test_pluralForm_makeGetter.js b/intl/locale/tests/unit/test_pluralForm_makeGetter.js
new file mode 100644
index 0000000000..0a5ec28f69
--- /dev/null
+++ b/intl/locale/tests/unit/test_pluralForm_makeGetter.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+/**
+ * This unit test makes sure the plural form for Irish Gaeilge is working by
+ * using the makeGetter method instead of using the default language (by
+ * development), English.
+ */
+
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+function run_test() {
+ // Irish is plural rule #11
+ let [get, numForms] = PluralForm.makeGetter(11);
+
+ // Irish has 5 plural forms
+ Assert.equal(5, numForms());
+
+ // I don't really know Irish, so I'll stick in some dummy text
+ let words = "is 1;is 2;is 3-6;is 7-10;everything else";
+
+ let test = function (text, low, high) {
+ for (let num = low; num <= high; num++) {
+ Assert.equal(text, get(num, words));
+ }
+ };
+
+ // Make sure for good inputs, things work as expected
+ test("everything else", 0, 0);
+ test("is 1", 1, 1);
+ test("is 2", 2, 2);
+ test("is 3-6", 3, 6);
+ test("is 7-10", 7, 10);
+ test("everything else", 11, 200);
+}
diff --git a/intl/locale/tests/unit/xpcshell.ini b/intl/locale/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..1b213949a1
--- /dev/null
+++ b/intl/locale/tests/unit/xpcshell.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+head =
+support-files =
+ data/intl_on_workers_worker.js
+ data/chrome.manifest
+
+[test_bug22310.js]
+skip-if = toolkit != "windows" && toolkit != "cocoa"
+
+[test_bug1086527.js]
+[test_intl_on_workers.js]
+skip-if = toolkit == "android" # bug 1309447
+[test_langPackMatcher.js]
+[test_pluralForm.js]
+[test_pluralForm_english.js]
+[test_pluralForm_makeGetter.js]
+
+[test_osPreferences.js]
+skip-if = toolkit == "android" # bug 1344596
+[test_localeService.js]
+[test_localeService_negotiateLanguages.js]
diff --git a/intl/locale/windows/OSPreferences_win.cpp b/intl/locale/windows/OSPreferences_win.cpp
new file mode 100644
index 0000000000..e1093f3a3a
--- /dev/null
+++ b/intl/locale/windows/OSPreferences_win.cpp
@@ -0,0 +1,321 @@
+/* -*- 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 "OSPreferences.h"
+#include "mozilla/intl/Locale.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsReadableUtils.h"
+
+#include <windows.h>
+
+#ifndef __MINGW32__ // WinRT headers not yet supported by MinGW
+# include <roapi.h>
+# include <wrl.h>
+# include <Windows.System.UserProfile.h>
+
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace ABI::Windows::Foundation::Collections;
+using namespace ABI::Windows::System::UserProfile;
+#endif
+
+using namespace mozilla::intl;
+
+OSPreferences::OSPreferences() {}
+
+bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
+ MOZ_ASSERT(aLocaleList.IsEmpty());
+
+#ifndef __MINGW32__
+ if (IsWin8OrLater()) {
+ // Try to get language list from GlobalizationPreferences; if this fails,
+ // we'll fall back to GetUserPreferredUILanguages.
+ // Per MSDN, these APIs are not available prior to Win8.
+ ComPtr<IGlobalizationPreferencesStatics> globalizationPrefs;
+ ComPtr<IVectorView<HSTRING>> languages;
+ uint32_t count;
+ if (SUCCEEDED(RoGetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_System_UserProfile_GlobalizationPreferences)
+ .Get(),
+ IID_PPV_ARGS(&globalizationPrefs))) &&
+ SUCCEEDED(globalizationPrefs->get_Languages(&languages)) &&
+ SUCCEEDED(languages->get_Size(&count))) {
+ for (uint32_t i = 0; i < count; ++i) {
+ HString lang;
+ if (SUCCEEDED(languages->GetAt(i, lang.GetAddressOf()))) {
+ unsigned int length;
+ const wchar_t* text = lang.GetRawBuffer(&length);
+ NS_LossyConvertUTF16toASCII loc(text, length);
+ if (CanonicalizeLanguageTag(loc)) {
+ if (!loc.Contains('-')) {
+ // DirectWrite font-name code doesn't like to be given a bare
+ // language code with no region subtag, but the
+ // GlobalizationPreferences API may give us one (e.g. "ja").
+ // So if there's no hyphen in the string at this point, we use
+ // AddLikelySubtags to get a suitable region code to
+ // go with it.
+ Locale locale;
+ auto result = LocaleParser::TryParse(loc, locale);
+ if (result.isOk() && locale.AddLikelySubtags().isOk() &&
+ locale.Region().Present()) {
+ loc.Append('-');
+ loc.Append(locale.Region().Span());
+ }
+ }
+ aLocaleList.AppendElement(loc);
+ }
+ }
+ }
+ }
+ }
+#endif
+
+ // Per MSDN, GetUserPreferredUILanguages is available from Vista onwards,
+ // so we can use it unconditionally (although it may not work well!)
+ if (aLocaleList.IsEmpty()) {
+ // Note that according to the questioner at
+ // https://stackoverflow.com/questions/52849233/getuserpreferreduilanguages-never-returns-more-than-two-languages,
+ // this may not always return the full list of languages we'd expect.
+ // We should always get at least the first-preference lang, though.
+ ULONG numLanguages = 0;
+ DWORD cchLanguagesBuffer = 0;
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages, nullptr,
+ &cchLanguagesBuffer)) {
+ return false;
+ }
+
+ AutoTArray<WCHAR, 64> locBuffer;
+ locBuffer.SetCapacity(cchLanguagesBuffer);
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages,
+ locBuffer.Elements(),
+ &cchLanguagesBuffer)) {
+ return false;
+ }
+
+ const WCHAR* start = locBuffer.Elements();
+ const WCHAR* bufEnd = start + cchLanguagesBuffer;
+ while (bufEnd - start > 1 && *start) {
+ const WCHAR* end = start + 1;
+ while (bufEnd - end > 1 && *end) {
+ end++;
+ }
+ NS_LossyConvertUTF16toASCII loc(start, end - start);
+ if (CanonicalizeLanguageTag(loc)) {
+ aLocaleList.AppendElement(loc);
+ }
+ start = end + 1;
+ }
+ }
+
+ return !aLocaleList.IsEmpty();
+}
+
+bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
+ MOZ_ASSERT(aLocaleList.IsEmpty());
+
+ WCHAR locale[LOCALE_NAME_MAX_LENGTH];
+ if (NS_WARN_IF(!LCIDToLocaleName(LOCALE_USER_DEFAULT, locale,
+ LOCALE_NAME_MAX_LENGTH, 0))) {
+ return false;
+ }
+
+ NS_LossyConvertUTF16toASCII loc(locale);
+
+ if (CanonicalizeLanguageTag(loc)) {
+ aLocaleList.AppendElement(loc);
+ return true;
+ }
+ return false;
+}
+
+static LCTYPE ToDateLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
+ switch (aFormatStyle) {
+ case OSPreferences::DateTimeFormatStyle::None:
+ return LOCALE_SLONGDATE;
+ case OSPreferences::DateTimeFormatStyle::Short:
+ return LOCALE_SSHORTDATE;
+ case OSPreferences::DateTimeFormatStyle::Medium:
+ return LOCALE_SSHORTDATE;
+ case OSPreferences::DateTimeFormatStyle::Long:
+ return LOCALE_SLONGDATE;
+ case OSPreferences::DateTimeFormatStyle::Full:
+ return LOCALE_SLONGDATE;
+ case OSPreferences::DateTimeFormatStyle::Invalid:
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid date format");
+ return LOCALE_SLONGDATE;
+ }
+}
+
+static LCTYPE ToTimeLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
+ switch (aFormatStyle) {
+ case OSPreferences::DateTimeFormatStyle::None:
+ return LOCALE_STIMEFORMAT;
+ case OSPreferences::DateTimeFormatStyle::Short:
+ return LOCALE_SSHORTTIME;
+ case OSPreferences::DateTimeFormatStyle::Medium:
+ return LOCALE_SSHORTTIME;
+ case OSPreferences::DateTimeFormatStyle::Long:
+ return LOCALE_STIMEFORMAT;
+ case OSPreferences::DateTimeFormatStyle::Full:
+ return LOCALE_STIMEFORMAT;
+ case OSPreferences::DateTimeFormatStyle::Invalid:
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid time format");
+ return LOCALE_STIMEFORMAT;
+ }
+}
+
+/**
+ * Windows API includes regional preferences from the user only
+ * if we pass empty locale string or if the locale string matches
+ * the current locale.
+ *
+ * Since Windows API only allows us to retrieve two options - short/long
+ * we map it to our four options as:
+ *
+ * short -> short
+ * medium -> short
+ * long -> long
+ * full -> long
+ *
+ * In order to produce a single date/time format, we use CLDR pattern
+ * for combined date/time string, since Windows API does not provide an
+ * option for this.
+ */
+bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
+ DateTimeFormatStyle aTimeStyle,
+ const nsACString& aLocale,
+ nsACString& aRetVal) {
+ nsAutoString localeName;
+ CopyASCIItoUTF16(aLocale, localeName);
+
+ bool isDate = aDateStyle != DateTimeFormatStyle::None &&
+ aDateStyle != DateTimeFormatStyle::Invalid;
+ bool isTime = aTimeStyle != DateTimeFormatStyle::None &&
+ aTimeStyle != DateTimeFormatStyle::Invalid;
+
+ // If both date and time are wanted, we'll initially read them into a
+ // local string, and then insert them into the overall date+time pattern;
+ nsAutoString str;
+ if (isDate && isTime) {
+ if (!GetDateTimeConnectorPattern(aLocale, aRetVal)) {
+ NS_WARNING("failed to get date/time connector");
+ aRetVal.AssignLiteral("{1} {0}");
+ }
+ } else if (!isDate && !isTime) {
+ aRetVal.Truncate(0);
+ return true;
+ }
+
+ if (isDate) {
+ LCTYPE lcType = ToDateLCType(aDateStyle);
+ size_t len = GetLocaleInfoEx(
+ reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
+ nullptr, 0);
+ if (len == 0) {
+ return false;
+ }
+
+ // We're doing it to ensure the terminator will fit when Windows writes the
+ // data to its output buffer. See bug 1358159 for details.
+ str.SetLength(len);
+ GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
+ lcType, (WCHAR*)str.BeginWriting(), len);
+ str.SetLength(len - 1); // -1 because len counts the null terminator
+
+ // Windows uses "ddd" and "dddd" for abbreviated and full day names
+ // respectively,
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317787(v=vs.85).aspx
+ // but in a CLDR/ICU-style pattern these should be "EEE" and "EEEE".
+ // http://userguide.icu-project.org/formatparse/datetime
+ // So we fix that up here.
+ nsAString::const_iterator start, pos, end;
+ start = str.BeginReading(pos);
+ str.EndReading(end);
+ if (FindInReadable(u"dddd"_ns, pos, end)) {
+ str.ReplaceLiteral(pos - start, 4, u"EEEE");
+ } else {
+ pos = start;
+ if (FindInReadable(u"ddd"_ns, pos, end)) {
+ str.ReplaceLiteral(pos - start, 3, u"EEE");
+ }
+ }
+
+ // Also, Windows uses lowercase "g" or "gg" for era, but ICU wants uppercase
+ // "G" (it would interpret "g" as "modified Julian day"!). So fix that.
+ int32_t index = str.FindChar('g');
+ if (index >= 0) {
+ str.Replace(index, 1, 'G');
+ // If it was a double "gg", just drop the second one.
+ index++;
+ if (str.CharAt(index) == 'g') {
+ str.Cut(index, 1);
+ }
+ }
+
+ // If time was also requested, we need to substitute the date pattern from
+ // Windows into the date+time format that we have in aRetVal.
+ if (isTime) {
+ nsACString::const_iterator start, pos, end;
+ start = aRetVal.BeginReading(pos);
+ aRetVal.EndReading(end);
+ if (FindInReadable("{1}"_ns, pos, end)) {
+ aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
+ }
+ } else {
+ aRetVal = NS_ConvertUTF16toUTF8(str);
+ }
+ }
+
+ if (isTime) {
+ LCTYPE lcType = ToTimeLCType(aTimeStyle);
+ size_t len = GetLocaleInfoEx(
+ reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
+ nullptr, 0);
+ if (len == 0) {
+ return false;
+ }
+
+ // We're doing it to ensure the terminator will fit when Windows writes the
+ // data to its output buffer. See bug 1358159 for details.
+ str.SetLength(len);
+ GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
+ lcType, (WCHAR*)str.BeginWriting(), len);
+ str.SetLength(len - 1);
+
+ // Windows uses "t" or "tt" for a "time marker" (am/pm indicator),
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd318148(v=vs.85).aspx
+ // but in a CLDR/ICU-style pattern that should be "a".
+ // http://userguide.icu-project.org/formatparse/datetime
+ // So we fix that up here.
+ int32_t index = str.FindChar('t');
+ if (index >= 0) {
+ str.Replace(index, 1, 'a');
+ index++;
+ if (str.CharAt(index) == 't') {
+ str.Cut(index, 1);
+ }
+ }
+
+ if (isDate) {
+ nsACString::const_iterator start, pos, end;
+ start = aRetVal.BeginReading(pos);
+ aRetVal.EndReading(end);
+ if (FindInReadable("{0}"_ns, pos, end)) {
+ aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
+ }
+ } else {
+ aRetVal = NS_ConvertUTF16toUTF8(str);
+ }
+ }
+
+ return true;
+}
+
+void OSPreferences::RemoveObservers() {}
diff --git a/intl/locale/windows/moz.build b/intl/locale/windows/moz.build
new file mode 100644
index 0000000000..9ac234b354
--- /dev/null
+++ b/intl/locale/windows/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += ["OSPreferences_win.cpp"]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "..",
+]