Backed out changeset cfe0bbc666b8 (bug 1784757) in order to wait some more for a...
[gecko.git] / intl / locale / LocaleService.cpp
blob1527f2791037dfe22f1e1a344cb632b5c7069080
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"
23 #ifdef XP_WIN
24 # include "WinUtils.h"
25 #endif
26 #ifdef MOZ_WIDGET_GTK
27 # include "mozilla/WidgetUtilsGtk.h"
28 #endif
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;
49 /**
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) {
68 nsAutoCString str;
69 nsresult rv = Preferences::GetCString(REQUESTED_LOCALES_PREF, str);
70 // isRepack means this is a version of Firefox specifically
71 // built for one language.
72 const bool isRepack =
73 #ifdef XP_WIN
74 !mozilla::widget::WinUtils::HasPackageIdentity();
75 #elif defined(MOZ_WIDGET_GTK)
76 !widget::IsRunningUnderSnap();
77 #else
78 true;
79 #endif
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) {
89 // Case 3
90 OSPreferences::GetInstance()->GetSystemLocales(aRetVal);
91 } else {
92 // Case 4
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
99 // result.
100 if (aRetVal.IsEmpty()) {
101 if (isRepack) {
102 // Case 1
103 nsAutoCString defaultLocale;
104 LocaleService::GetInstance()->GetDefaultLocale(defaultLocale);
105 aRetVal.AppendElement(defaultLocale);
106 } else {
107 // Case 2
108 OSPreferences::GetInstance()->GetSystemLocales(aRetVal);
113 static void ReadWebExposedLocales(nsTArray<nsCString>& aRetVal) {
114 nsAutoCString str;
115 nsresult rv = Preferences::GetCString(WEB_EXPOSED_LOCALES_PREF, str);
116 if (NS_WARN_IF(NS_FAILED(rv)) || str.Length() == 0) {
117 return;
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) {
132 if (mIsServer) {
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
156 // runtime.
157 aRetVal.AppendElement(lastFallbackLocale);
161 LocaleService* LocaleService::GetInstance() {
162 if (!sInstance) {
163 sInstance = new LocaleService(XRE_IsParentProcess());
165 if (sInstance->IsServer()) {
166 // We're going to observe for requested languages changes which come
167 // from prefs.
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();
174 if (obs) {
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);
183 return sInstance;
186 static void NotifyAppLocaleChanged() {
187 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
188 if (obs) {
189 obs->NotifyObservers(nullptr, "intl:app-locales-changed", nullptr);
191 // The locale in AppDateTimeFormat is cached statically.
192 AppDateTimeFormat::ClearLocaleCache();
195 void LocaleService::RemoveObservers() {
196 if (mIsServer) {
197 Preferences::RemoveObservers(this, kObservedPrefs);
199 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
200 if (obs) {
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();
222 if (obs) {
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();
236 if (obs) {
237 obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
239 LocalesChanged();
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()) {
258 return;
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")) {
279 return true;
281 if (pseudoLocale.EqualsLiteral("accented")) {
282 return false;
286 nsAutoCString locale;
287 GetAppLocaleAsBCP47(locale);
288 return IsLocaleRTL(locale);
291 NS_IMETHODIMP
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)) {
300 RemoveObservers();
301 } else {
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();
314 return NS_OK;
317 bool LocaleService::LanguagesMatch(const nsACString& aRequested,
318 const nsACString& aAvailable) {
319 Locale requested;
320 auto requestedResult = LocaleParser::TryParse(aRequested, requested);
321 Locale available;
322 auto availableResult = LocaleParser::TryParse(aAvailable, available);
324 if (requestedResult.isErr() || availableResult.isErr()) {
325 return false;
328 if (requested.Canonicalize().isErr() || available.Canonicalize().isErr()) {
329 return false;
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);
340 if (zip) {
341 nsZipItemPtr<char> item(zip, aFilePath);
342 if (!item) {
343 return false;
345 aOutString->Assign(item.Buffer(), item.Length());
346 return true;
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)))) {
354 return false;
357 path->AppendRelativeNativePath(nsDependentCString(aFilePath));
358 bool result;
359 if (NS_FAILED(path->IsFile(&result)) || !result ||
360 NS_FAILED(path->IsReadable(&result)) || !result) {
361 return false;
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.
366 FILE* fp;
367 if (NS_FAILED(path->OpenANSIFileDesc("r", &fp)) || !fp) {
368 return false;
371 fseek(fp, 0, SEEK_END);
372 long len = ftell(fp);
373 rewind(fp);
374 aOutString->SetLength(len);
375 size_t cc = fread(aOutString->BeginWriting(), 1, len, fp);
377 fclose(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
405 NS_IMETHODIMP
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");
416 #ifdef MOZ_UPDATER
417 // This should never be empty.
418 MOZ_ASSERT(!locale.IsEmpty());
419 #endif
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;
432 return NS_OK;
435 NS_IMETHODIMP
436 LocaleService::GetLastFallbackLocale(nsACString& aRetVal) {
437 aRetVal.AssignLiteral("en-US");
438 return NS_OK;
441 NS_IMETHODIMP
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");
450 } else {
451 aRetVal.AppendElement(locale);
454 return NS_OK;
457 NS_IMETHODIMP
458 LocaleService::GetAppLocalesAsBCP47(nsTArray<nsCString>& aRetVal) {
459 if (mAppLocales.IsEmpty()) {
460 NegotiateAppLocales(mAppLocales);
462 aRetVal = mAppLocales.Clone();
464 return NS_OK;
467 NS_IMETHODIMP
468 LocaleService::GetAppLocaleAsLangTag(nsACString& aRetVal) {
469 AutoTArray<nsCString, 32> locales;
470 GetAppLocalesAsLangTags(locales);
472 aRetVal = locales[0];
473 return NS_OK;
476 NS_IMETHODIMP
477 LocaleService::GetAppLocaleAsBCP47(nsACString& aRetVal) {
478 if (mAppLocales.IsEmpty()) {
479 NegotiateAppLocales(mAppLocales);
481 aRetVal = mAppLocales[0];
482 return NS_OK;
485 NS_IMETHODIMP
486 LocaleService::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
487 bool useOSLocales =
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.
492 if (useOSLocales) {
493 if (NS_SUCCEEDED(
494 OSPreferences::GetInstance()->GetRegionalPrefsLocales(aRetVal))) {
495 return NS_OK;
498 // If we fail to retrieve them, return the app locales.
499 GetAppLocalesAsBCP47(aRetVal);
500 return NS_OK;
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);
516 return NS_OK;
519 if (LocaleService::LanguagesMatch(appLocale, regionalPrefsLocales[0])) {
520 aRetVal = regionalPrefsLocales.Clone();
521 return NS_OK;
524 // Otherwise use the app locales.
525 GetAppLocalesAsBCP47(aRetVal);
526 return NS_OK;
529 NS_IMETHODIMP
530 LocaleService::GetWebExposedLocales(nsTArray<nsCString>& aRetVal) {
531 if (StaticPrefs::privacy_spoof_english() == 2) {
532 aRetVal = nsTArray<nsCString>({"en-US"_ns});
533 return NS_OK;
536 if (!mWebExposedLocales.IsEmpty()) {
537 aRetVal = mWebExposedLocales.Clone();
538 return NS_OK;
541 return GetRegionalPrefsLocales(aRetVal);
544 NS_IMETHODIMP
545 LocaleService::NegotiateLanguages(const nsTArray<nsCString>& aRequested,
546 const nsTArray<nsCString>& aAvailable,
547 const nsACString& aDefaultLocale,
548 int32_t aStrategy,
549 nsTArray<nsCString>& aRetVal) {
550 if (aStrategy < 0 || aStrategy > 2) {
551 return NS_ERROR_INVALID_ARG;
554 #ifdef DEBUG
555 Locale parsedLocale;
556 auto result = LocaleParser::TryParse(aDefaultLocale, parsedLocale);
558 MOZ_ASSERT(
559 aDefaultLocale.IsEmpty() || result.isOk(),
560 "If specified, default locale must be a well-formed BCP47 language tag.");
561 #endif
563 if (aStrategy == kLangNegStrategyLookup && aDefaultLocale.IsEmpty()) {
564 NS_WARNING(
565 "Default locale should be specified when using lookup strategy.");
568 NegotiationStrategy strategy;
569 switch (aStrategy) {
570 case kLangNegStrategyFiltering:
571 strategy = NegotiationStrategy::Filtering;
572 break;
573 case kLangNegStrategyMatching:
574 strategy = NegotiationStrategy::Matching;
575 break;
576 case kLangNegStrategyLookup:
577 strategy = NegotiationStrategy::Lookup;
578 break;
581 fluent_langneg_negotiate_languages(&aRequested, &aAvailable, &aDefaultLocale,
582 strategy, &aRetVal);
584 return NS_OK;
587 NS_IMETHODIMP
588 LocaleService::GetRequestedLocales(nsTArray<nsCString>& aRetVal) {
589 if (mRequestedLocales.IsEmpty()) {
590 ReadRequestedLocales(mRequestedLocales);
593 aRetVal = mRequestedLocales.Clone();
594 return NS_OK;
597 NS_IMETHODIMP
598 LocaleService::GetRequestedLocale(nsACString& aRetVal) {
599 if (mRequestedLocales.IsEmpty()) {
600 ReadRequestedLocales(mRequestedLocales);
603 if (mRequestedLocales.Length() > 0) {
604 aRetVal = mRequestedLocales[0];
607 return NS_OK;
610 NS_IMETHODIMP
611 LocaleService::SetRequestedLocales(const nsTArray<nsCString>& aRequested) {
612 MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
613 if (!mIsServer) {
614 return NS_ERROR_UNEXPECTED;
617 nsAutoCString str;
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(",");
629 str.Append(locale);
631 Preferences::SetCString(REQUESTED_LOCALES_PREF, str);
633 return NS_OK;
636 NS_IMETHODIMP
637 LocaleService::GetAvailableLocales(nsTArray<nsCString>& aRetVal) {
638 MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
639 if (!mIsServer) {
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();
651 return NS_OK;
654 NS_IMETHODIMP
655 LocaleService::GetIsAppLocaleRTL(bool* aRetVal) {
656 (*aRetVal) = IsAppLocaleRTL();
657 return NS_OK;
660 NS_IMETHODIMP
661 LocaleService::SetAvailableLocales(const nsTArray<nsCString>& aAvailable) {
662 MOZ_ASSERT(mIsServer, "This should only be called in the server mode.");
663 if (!mIsServer) {
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);
680 LocalesChanged();
683 return NS_OK;
686 NS_IMETHODIMP
687 LocaleService::GetPackagedLocales(nsTArray<nsCString>& aRetVal) {
688 if (mPackagedLocales.IsEmpty()) {
689 InitPackagedLocales();
691 aRetVal = mPackagedLocales.Clone();
692 return NS_OK;