diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /intl/locale/LocaleService.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | intl/locale/LocaleService.cpp | 693 |
1 files changed, 693 insertions, 0 deletions
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; +} |