1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "LocaleService.h"
8 #include "mozilla/ClearOnShutdown.h"
9 #include "mozilla/DebugOnly.h"
10 #include "mozilla/Omnijar.h"
11 #include "mozilla/Preferences.h"
12 #include "mozilla/Services.h"
13 #include "mozilla/StaticPrefs_privacy.h"
14 #include "mozilla/intl/AppDateTimeFormat.h"
15 #include "mozilla/intl/Locale.h"
16 #include "mozilla/intl/OSPreferences.h"
17 #include "nsDirectoryService.h"
18 #include "nsDirectoryServiceDefs.h"
19 #include "nsIObserverService.h"
20 #include "nsStringEnumerator.h"
21 #include "nsXULAppAPI.h"
22 #include "nsZipArchive.h"
24 # include "WinUtils.h"
27 # include "mozilla/WidgetUtilsGtk.h"
30 #define INTL_SYSTEM_LOCALES_CHANGED "intl:system-locales-changed"
32 #define PSEUDO_LOCALE_PREF "intl.l10n.pseudo"
33 #define REQUESTED_LOCALES_PREF "intl.locale.requested"
34 #define WEB_EXPOSED_LOCALES_PREF "intl.locale.privacy.web_exposed"
36 static const char* kObservedPrefs
[] = {REQUESTED_LOCALES_PREF
,
37 WEB_EXPOSED_LOCALES_PREF
,
38 PSEUDO_LOCALE_PREF
, nullptr};
40 using namespace mozilla::intl::ffi
;
41 using namespace mozilla::intl
;
42 using namespace mozilla
;
44 NS_IMPL_ISUPPORTS(LocaleService
, mozILocaleService
, nsIObserver
,
45 nsISupportsWeakReference
)
47 mozilla::StaticRefPtr
<LocaleService
> LocaleService::sInstance
;
50 * This function splits an input string by `,` delimiter, sanitizes the result
51 * language tags and returns them to the caller.
53 static void SplitLocaleListStringIntoArray(nsACString
& str
,
54 nsTArray
<nsCString
>& aRetVal
) {
55 if (str
.Length() > 0) {
56 for (const nsACString
& part
: str
.Split(',')) {
57 nsAutoCString
locale(part
);
58 if (LocaleService::CanonicalizeLanguageId(locale
)) {
59 if (!aRetVal
.Contains(locale
)) {
60 aRetVal
.AppendElement(locale
);
67 static void ReadRequestedLocales(nsTArray
<nsCString
>& aRetVal
) {
69 nsresult rv
= Preferences::GetCString(REQUESTED_LOCALES_PREF
, str
);
70 // isRepack means this is a version of Firefox specifically
71 // built for one language.
74 !mozilla::widget::WinUtils::HasPackageIdentity();
75 #elif defined(MOZ_WIDGET_GTK)
76 !widget::IsRunningUnderSnap();
81 // We handle four scenarios here:
83 // 1) The pref is not set - use default locale
84 // 2) The pref is not set and we're a packaged app - use OS locales
85 // 3) The pref is set to "" - use OS locales
86 // 4) The pref is set to a value - parse the locale list and use it
87 if (NS_SUCCEEDED(rv
)) {
88 if (str
.Length() == 0) {
90 OSPreferences::GetInstance()->GetSystemLocales(aRetVal
);
93 SplitLocaleListStringIntoArray(str
, aRetVal
);
97 // This will happen when either the pref is not set,
98 // or parsing of the pref didn't produce any usable
100 if (aRetVal
.IsEmpty()) {
103 nsAutoCString defaultLocale
;
104 LocaleService::GetInstance()->GetDefaultLocale(defaultLocale
);
105 aRetVal
.AppendElement(defaultLocale
);
108 OSPreferences::GetInstance()->GetSystemLocales(aRetVal
);
113 static void ReadWebExposedLocales(nsTArray
<nsCString
>& aRetVal
) {
115 nsresult rv
= Preferences::GetCString(WEB_EXPOSED_LOCALES_PREF
, str
);
116 if (NS_WARN_IF(NS_FAILED(rv
)) || str
.Length() == 0) {
120 SplitLocaleListStringIntoArray(str
, aRetVal
);
123 LocaleService::LocaleService(bool aIsServer
) : mIsServer(aIsServer
) {}
126 * This function performs the actual language negotiation for the API.
128 * Currently it collects the locale ID used by nsChromeRegistry and
129 * adds hardcoded default locale as a fallback.
131 void LocaleService::NegotiateAppLocales(nsTArray
<nsCString
>& aRetVal
) {
133 nsAutoCString defaultLocale
;
134 AutoTArray
<nsCString
, 100> availableLocales
;
135 AutoTArray
<nsCString
, 10> requestedLocales
;
136 GetDefaultLocale(defaultLocale
);
137 GetAvailableLocales(availableLocales
);
138 GetRequestedLocales(requestedLocales
);
140 NegotiateLanguages(requestedLocales
, availableLocales
, defaultLocale
,
141 kLangNegStrategyFiltering
, aRetVal
);
144 nsAutoCString lastFallbackLocale
;
145 GetLastFallbackLocale(lastFallbackLocale
);
147 if (!aRetVal
.Contains(lastFallbackLocale
)) {
148 // This part is used in one of the two scenarios:
150 // a) We're in a client mode, and no locale has been set yet,
151 // so we need to return last fallback locale temporarily.
152 // b) We're in a server mode, and the last fallback locale was excluded
153 // when negotiating against the requested locales.
154 // Since we currently package it as a last fallback at build
155 // time, we should also add it at the end of the list at
157 aRetVal
.AppendElement(lastFallbackLocale
);
161 LocaleService
* LocaleService::GetInstance() {
163 sInstance
= new LocaleService(XRE_IsParentProcess());
165 if (sInstance
->IsServer()) {
166 // We're going to observe for requested languages changes which come
168 DebugOnly
<nsresult
> rv
=
169 Preferences::AddWeakObservers(sInstance
, kObservedPrefs
);
170 MOZ_ASSERT(NS_SUCCEEDED(rv
), "Adding observers failed.");
172 nsCOMPtr
<nsIObserverService
> obs
=
173 mozilla::services::GetObserverService();
175 obs
->AddObserver(sInstance
, INTL_SYSTEM_LOCALES_CHANGED
, true);
176 obs
->AddObserver(sInstance
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
, true);
179 // DOM might use ICUUtils and LocaleService during UnbindFromTree by
180 // final cycle collection.
181 ClearOnShutdown(&sInstance
, ShutdownPhase::CCPostLastCycleCollection
);
186 static void NotifyAppLocaleChanged() {
187 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
189 obs
->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);
191 // The locale in AppDateTimeFormat is cached statically.
192 AppDateTimeFormat::ClearLocaleCache();
195 void LocaleService::RemoveObservers() {
197 Preferences::RemoveObservers(this, kObservedPrefs
);
199 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
201 obs
->RemoveObserver(this, INTL_SYSTEM_LOCALES_CHANGED
);
202 obs
->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID
);
207 void LocaleService::AssignAppLocales(const nsTArray
<nsCString
>& aAppLocales
) {
208 MOZ_ASSERT(!mIsServer
,
209 "This should only be called for LocaleService in client mode.");
211 mAppLocales
= aAppLocales
.Clone();
212 NotifyAppLocaleChanged();
215 void LocaleService::AssignRequestedLocales(
216 const nsTArray
<nsCString
>& aRequestedLocales
) {
217 MOZ_ASSERT(!mIsServer
,
218 "This should only be called for LocaleService in client mode.");
220 mRequestedLocales
= aRequestedLocales
.Clone();
221 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
223 obs
->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
227 void LocaleService::RequestedLocalesChanged() {
228 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
230 nsTArray
<nsCString
> newLocales
;
231 ReadRequestedLocales(newLocales
);
233 if (mRequestedLocales
!= newLocales
) {
234 mRequestedLocales
= std::move(newLocales
);
235 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
237 obs
->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
243 void LocaleService::WebExposedLocalesChanged() {
244 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
246 nsTArray
<nsCString
> newLocales
;
247 ReadWebExposedLocales(newLocales
);
248 if (mWebExposedLocales
!= newLocales
) {
249 mWebExposedLocales
= std::move(newLocales
);
253 void LocaleService::LocalesChanged() {
254 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
256 // if mAppLocales has not been initialized yet, just return
257 if (mAppLocales
.IsEmpty()) {
261 nsTArray
<nsCString
> newLocales
;
262 NegotiateAppLocales(newLocales
);
264 if (mAppLocales
!= newLocales
) {
265 mAppLocales
= std::move(newLocales
);
266 NotifyAppLocaleChanged();
270 bool LocaleService::IsLocaleRTL(const nsACString
& aLocale
) {
271 return unic_langid_is_rtl(&aLocale
);
274 bool LocaleService::IsAppLocaleRTL() {
275 // Next, check if there is a pseudo locale `bidi` set.
276 nsAutoCString pseudoLocale
;
277 if (NS_SUCCEEDED(Preferences::GetCString("intl.l10n.pseudo", pseudoLocale
))) {
278 if (pseudoLocale
.EqualsLiteral("bidi")) {
281 if (pseudoLocale
.EqualsLiteral("accented")) {
286 nsAutoCString locale
;
287 GetAppLocaleAsBCP47(locale
);
288 return IsLocaleRTL(locale
);
292 LocaleService::Observe(nsISupports
* aSubject
, const char* aTopic
,
293 const char16_t
* aData
) {
294 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
296 if (!strcmp(aTopic
, INTL_SYSTEM_LOCALES_CHANGED
)) {
297 RequestedLocalesChanged();
298 WebExposedLocalesChanged();
299 } else if (!strcmp(aTopic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
)) {
302 NS_ConvertUTF16toUTF8
pref(aData
);
303 // At the moment the only thing we're observing are settings indicating
304 // user requested locales.
305 if (pref
.EqualsLiteral(REQUESTED_LOCALES_PREF
)) {
306 RequestedLocalesChanged();
307 } else if (pref
.EqualsLiteral(WEB_EXPOSED_LOCALES_PREF
)) {
308 WebExposedLocalesChanged();
309 } else if (pref
.EqualsLiteral(PSEUDO_LOCALE_PREF
)) {
310 NotifyAppLocaleChanged();
317 bool LocaleService::LanguagesMatch(const nsACString
& aRequested
,
318 const nsACString
& aAvailable
) {
320 auto requestedResult
= LocaleParser::TryParse(aRequested
, requested
);
322 auto availableResult
= LocaleParser::TryParse(aAvailable
, available
);
324 if (requestedResult
.isErr() || availableResult
.isErr()) {
328 if (requested
.Canonicalize().isErr() || available
.Canonicalize().isErr()) {
332 return requested
.Language().Span() == available
.Language().Span();
335 bool LocaleService::IsServer() { return mIsServer
; }
337 static bool GetGREFileContents(const char* aFilePath
, nsCString
* aOutString
) {
338 // Look for the requested file in omnijar.
339 RefPtr
<nsZipArchive
> zip
= Omnijar::GetReader(Omnijar::GRE
);
341 nsZipItemPtr
<char> item(zip
, aFilePath
);
345 aOutString
->Assign(item
.Buffer(), item
.Length());
349 // If we didn't have an omnijar (i.e. we're running a non-packaged
350 // build), then look in the GRE directory.
351 nsCOMPtr
<nsIFile
> path
;
352 if (NS_FAILED(nsDirectoryService::gService
->Get(
353 NS_GRE_DIR
, NS_GET_IID(nsIFile
), getter_AddRefs(path
)))) {
357 path
->AppendRelativeNativePath(nsDependentCString(aFilePath
));
359 if (NS_FAILED(path
->IsFile(&result
)) || !result
||
360 NS_FAILED(path
->IsReadable(&result
)) || !result
) {
364 // This is a small file, only used once, so it's not worth doing some fancy
365 // off-main-thread file I/O or whatever. Just read it.
367 if (NS_FAILED(path
->OpenANSIFileDesc("r", &fp
)) || !fp
) {
371 fseek(fp
, 0, SEEK_END
);
372 long len
= ftell(fp
);
374 aOutString
->SetLength(len
);
375 size_t cc
= fread(aOutString
->BeginWriting(), 1, len
, fp
);
379 return cc
== size_t(len
);
382 void LocaleService::InitPackagedLocales() {
383 MOZ_ASSERT(mPackagedLocales
.IsEmpty());
385 nsAutoCString localesString
;
386 if (GetGREFileContents("res/multilocale.txt", &localesString
)) {
387 localesString
.Trim(" \t\n\r");
388 // This should never be empty in a correctly-built product.
389 MOZ_ASSERT(!localesString
.IsEmpty());
390 SplitLocaleListStringIntoArray(localesString
, mPackagedLocales
);
393 // Last resort in case of broken build
394 if (mPackagedLocales
.IsEmpty()) {
395 nsAutoCString defaultLocale
;
396 GetDefaultLocale(defaultLocale
);
397 mPackagedLocales
.AppendElement(defaultLocale
);
402 * mozILocaleService methods
406 LocaleService::GetDefaultLocale(nsACString
& aRetVal
) {
407 // We don't allow this to change during a session (it's set at build/package
408 // time), so we cache the result the first time we're called.
409 if (mDefaultLocale
.IsEmpty()) {
410 nsAutoCString locale
;
411 // Try to get the package locale from update.locale in omnijar. If the
412 // update.locale file is not found, item.len will remain 0 and we'll
413 // just use our hard-coded default below.
414 GetGREFileContents("update.locale", &locale
);
415 locale
.Trim(" \t\n\r");
417 // This should never be empty.
418 MOZ_ASSERT(!locale
.IsEmpty());
420 if (CanonicalizeLanguageId(locale
)) {
421 mDefaultLocale
.Assign(locale
);
424 // Hard-coded fallback to allow us to survive even if update.locale was
425 // missing/broken in some way.
426 if (mDefaultLocale
.IsEmpty()) {
427 GetLastFallbackLocale(mDefaultLocale
);
431 aRetVal
= mDefaultLocale
;
436 LocaleService::GetLastFallbackLocale(nsACString
& aRetVal
) {
437 aRetVal
.AssignLiteral("en-US");
442 LocaleService::GetAppLocalesAsLangTags(nsTArray
<nsCString
>& aRetVal
) {
443 if (mAppLocales
.IsEmpty()) {
444 NegotiateAppLocales(mAppLocales
);
446 for (uint32_t i
= 0; i
< mAppLocales
.Length(); i
++) {
447 nsAutoCString
locale(mAppLocales
[i
]);
448 if (locale
.LowerCaseEqualsASCII("ja-jp-macos")) {
449 aRetVal
.AppendElement("ja-JP-mac");
451 aRetVal
.AppendElement(locale
);
458 LocaleService::GetAppLocalesAsBCP47(nsTArray
<nsCString
>& aRetVal
) {
459 if (mAppLocales
.IsEmpty()) {
460 NegotiateAppLocales(mAppLocales
);
462 aRetVal
= mAppLocales
.Clone();
468 LocaleService::GetAppLocaleAsLangTag(nsACString
& aRetVal
) {
469 AutoTArray
<nsCString
, 32> locales
;
470 GetAppLocalesAsLangTags(locales
);
472 aRetVal
= locales
[0];
477 LocaleService::GetAppLocaleAsBCP47(nsACString
& aRetVal
) {
478 if (mAppLocales
.IsEmpty()) {
479 NegotiateAppLocales(mAppLocales
);
481 aRetVal
= mAppLocales
[0];
486 LocaleService::GetRegionalPrefsLocales(nsTArray
<nsCString
>& aRetVal
) {
488 Preferences::GetBool("intl.regional_prefs.use_os_locales", false);
490 // If the user specified that they want to use OS Regional Preferences
491 // locales, try to retrieve them and use.
494 OSPreferences::GetInstance()->GetRegionalPrefsLocales(aRetVal
))) {
498 // If we fail to retrieve them, return the app locales.
499 GetAppLocalesAsBCP47(aRetVal
);
503 // Otherwise, fetch OS Regional Preferences locales and compare the first one
504 // to the app locale. If the language subtag matches, we can safely use
505 // the OS Regional Preferences locale.
507 // This facilitates scenarios such as Firefox in "en-US" and User sets
508 // regional prefs to "en-GB".
509 nsAutoCString appLocale
;
510 AutoTArray
<nsCString
, 10> regionalPrefsLocales
;
511 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocale
);
513 if (NS_FAILED(OSPreferences::GetInstance()->GetRegionalPrefsLocales(
514 regionalPrefsLocales
))) {
515 GetAppLocalesAsBCP47(aRetVal
);
519 if (LocaleService::LanguagesMatch(appLocale
, regionalPrefsLocales
[0])) {
520 aRetVal
= regionalPrefsLocales
.Clone();
524 // Otherwise use the app locales.
525 GetAppLocalesAsBCP47(aRetVal
);
530 LocaleService::GetWebExposedLocales(nsTArray
<nsCString
>& aRetVal
) {
531 if (StaticPrefs::privacy_spoof_english() == 2) {
532 aRetVal
= nsTArray
<nsCString
>({"en-US"_ns
});
536 if (!mWebExposedLocales
.IsEmpty()) {
537 aRetVal
= mWebExposedLocales
.Clone();
541 return GetRegionalPrefsLocales(aRetVal
);
545 LocaleService::NegotiateLanguages(const nsTArray
<nsCString
>& aRequested
,
546 const nsTArray
<nsCString
>& aAvailable
,
547 const nsACString
& aDefaultLocale
,
549 nsTArray
<nsCString
>& aRetVal
) {
550 if (aStrategy
< 0 || aStrategy
> 2) {
551 return NS_ERROR_INVALID_ARG
;
556 auto result
= LocaleParser::TryParse(aDefaultLocale
, parsedLocale
);
559 aDefaultLocale
.IsEmpty() || result
.isOk(),
560 "If specified, default locale must be a well-formed BCP47 language tag.");
563 if (aStrategy
== kLangNegStrategyLookup
&& aDefaultLocale
.IsEmpty()) {
565 "Default locale should be specified when using lookup strategy.");
568 NegotiationStrategy strategy
;
570 case kLangNegStrategyFiltering
:
571 strategy
= NegotiationStrategy::Filtering
;
573 case kLangNegStrategyMatching
:
574 strategy
= NegotiationStrategy::Matching
;
576 case kLangNegStrategyLookup
:
577 strategy
= NegotiationStrategy::Lookup
;
581 fluent_langneg_negotiate_languages(&aRequested
, &aAvailable
, &aDefaultLocale
,
588 LocaleService::GetRequestedLocales(nsTArray
<nsCString
>& aRetVal
) {
589 if (mRequestedLocales
.IsEmpty()) {
590 ReadRequestedLocales(mRequestedLocales
);
593 aRetVal
= mRequestedLocales
.Clone();
598 LocaleService::GetRequestedLocale(nsACString
& aRetVal
) {
599 if (mRequestedLocales
.IsEmpty()) {
600 ReadRequestedLocales(mRequestedLocales
);
603 if (mRequestedLocales
.Length() > 0) {
604 aRetVal
= mRequestedLocales
[0];
611 LocaleService::SetRequestedLocales(const nsTArray
<nsCString
>& aRequested
) {
612 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
614 return NS_ERROR_UNEXPECTED
;
619 for (auto& req
: aRequested
) {
620 nsAutoCString
locale(req
);
621 if (!CanonicalizeLanguageId(locale
)) {
622 NS_ERROR("Invalid language tag provided to SetRequestedLocales!");
623 return NS_ERROR_INVALID_ARG
;
626 if (!str
.IsEmpty()) {
627 str
.AppendLiteral(",");
631 Preferences::SetCString(REQUESTED_LOCALES_PREF
, str
);
637 LocaleService::GetAvailableLocales(nsTArray
<nsCString
>& aRetVal
) {
638 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
640 return NS_ERROR_UNEXPECTED
;
643 if (mAvailableLocales
.IsEmpty()) {
644 // If there are no available locales set, it means that L10nRegistry
645 // did not register its locale pool yet. The best course of action
646 // is to use packaged locales until that happens.
647 GetPackagedLocales(mAvailableLocales
);
650 aRetVal
= mAvailableLocales
.Clone();
655 LocaleService::GetIsAppLocaleRTL(bool* aRetVal
) {
656 (*aRetVal
) = IsAppLocaleRTL();
661 LocaleService::SetAvailableLocales(const nsTArray
<nsCString
>& aAvailable
) {
662 MOZ_ASSERT(mIsServer
, "This should only be called in the server mode.");
664 return NS_ERROR_UNEXPECTED
;
667 nsTArray
<nsCString
> newLocales
;
669 for (auto& avail
: aAvailable
) {
670 nsAutoCString
locale(avail
);
671 if (!CanonicalizeLanguageId(locale
)) {
672 NS_ERROR("Invalid language tag provided to SetAvailableLocales!");
673 return NS_ERROR_INVALID_ARG
;
675 newLocales
.AppendElement(locale
);
678 if (newLocales
!= mAvailableLocales
) {
679 mAvailableLocales
= std::move(newLocales
);
687 LocaleService::GetPackagedLocales(nsTArray
<nsCString
>& aRetVal
) {
688 if (mPackagedLocales
.IsEmpty()) {
689 InitPackagedLocales();
691 aRetVal
= mPackagedLocales
.Clone();