diff options
Diffstat (limited to 'intl/locale')
61 files changed, 7379 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/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..2be9966043 --- /dev/null +++ b/intl/locale/components.conf @@ -0,0 +1,27 @@ +# -*- 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 = [ + { + 'name': 'Locale', + '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..2f40d20aa5 --- /dev/null +++ b/intl/locale/language.properties @@ -0,0 +1,290 @@ +# 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 +sat.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..8099d1a695 --- /dev/null +++ b/intl/locale/moz.build @@ -0,0 +1,97 @@ +# -*- 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.toml"] + +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", +] + +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..6c57fb8743 --- /dev/null +++ b/intl/locale/nsLanguageAtomService.cpp @@ -0,0 +1,256 @@ +/* -*- 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" +#include "mozilla/StaticPtr.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 StaticAutoPtr<nsLanguageAtomService> gLangAtomService; + +// static +nsLanguageAtomService* nsLanguageAtomService::GetService() { + if (!gLangAtomService) { + gLangAtomService = new 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..521ead4e87 --- /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 "nsAtomHashKeys.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<RefPtr<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_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/xpcshell.toml b/intl/locale/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..bbce8bd46e --- /dev/null +++ b/intl/locale/tests/unit/xpcshell.toml @@ -0,0 +1,21 @@ +[DEFAULT] +head ="" +support-files = [ + "data/intl_on_workers_worker.js", + "data/chrome.manifest", +] + +["test_bug22310.js"] +skip-if = ["os != 'win' && os != 'mac'"] + +["test_intl_on_workers.js"] +skip-if = ["os == 'android'"] # bug 1309447 + +["test_langPackMatcher.js"] + +["test_localeService.js"] + +["test_localeService_negotiateLanguages.js"] + +["test_osPreferences.js"] +skip-if = ["os == 'android'"] # bug 1344596 diff --git a/intl/locale/windows/OSPreferences_win.cpp b/intl/locale/windows/OSPreferences_win.cpp new file mode 100644 index 0000000000..d790be7202 --- /dev/null +++ b/intl/locale/windows/OSPreferences_win.cpp @@ -0,0 +1,318 @@ +/* -*- 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 "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__ + // 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 += [ + "..", +] |