1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
9 AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
12 if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
13 // This check ensures that the `mockable` API calls can be consisently mocked in tests.
14 // If this requirement needs to be eased, please ensure the test logic remains valid.
15 throw new Error("This code is assumed to run in the parent process.");
19 * Attempts to find an appropriate langpack for a given language. The async function
20 * is infallible, but may not return a langpack.
23 * langPack: LangPack | null,
24 * langPackDisplayName: string | null
27 async function negotiateLangPackForLanguageMismatch() {
28 const localeInfo = getAppAndSystemLocaleInfo();
31 langPackDisplayName: null,
33 if (!localeInfo.systemLocale) {
34 // The system locale info was not valid.
39 * Fetch the available langpacks from AMO.
41 * @type {Array<LangPack>}
43 const availableLangpacks = await mockable.getAvailableLangpacks();
44 if (!availableLangpacks) {
49 * Figure out a langpack to recommend.
50 * @type {LangPack | null}
53 // First look for a langpack that matches the baseName, which may include a script.
54 // e.g. system "fr-FR" matches langpack "fr-FR"
55 // system "en-GB" matches langpack "en-GB".
56 // system "zh-Hant-CN" matches langpack "zh-Hant-CN".
57 availableLangpacks.find(
58 ({ target_locale }) => target_locale === localeInfo.systemLocale.baseName
60 // Next try matching language and region while excluding script
61 // e.g. system "zh-Hant-TW" matches langpack "zh-TW" but not "zh-CN".
62 availableLangpacks.find(
63 ({ target_locale }) =>
65 `${localeInfo.systemLocale.language}-${localeInfo.systemLocale.region}`
67 // Next look for langpacks that just match the language.
68 // e.g. system "fr-FR" matches langpack "fr".
69 // system "en-AU" matches langpack "en".
70 availableLangpacks.find(
71 ({ target_locale }) => target_locale === localeInfo.systemLocale.language
73 // Next look for a langpack that matches the language, but not the region.
74 // e.g. "es-CL" (Chilean Spanish) as a system language matching
75 // "es-ES" (European Spanish)
76 availableLangpacks.find(({ target_locale }) =>
77 target_locale.startsWith(`${localeInfo.systemLocale.language}-`)
87 langPackDisplayName: Services.intl.getLocaleDisplayNames(
89 [langPack.target_locale],
90 { preferNative: true }
95 // If a langpack is being installed, allow blocking on that.
96 let installingLangpack = new Map();
101 * @property {string} target_locale
102 * @property {string} url
103 * @property {string} hash
107 * Ensure that a given lanpack is installed.
109 * @param {LangPack} langPack
110 * @returns {Promise<boolean>} Success or failure.
112 function ensureLangPackInstalled(langPack) {
114 throw new Error("Expected a LangPack to install.");
116 // Make sure any outstanding calls get resolved before attempting another call.
117 // This guards against any quick page refreshes attempting to install the langpack
119 const inProgress = installingLangpack.get(langPack.hash);
123 const promise = _ensureLangPackInstalledImpl(langPack);
124 installingLangpack.set(langPack.hash, promise);
125 promise.finally(() => {
126 installingLangpack.delete(langPack.hash);
132 * @param {LangPack} langPack
133 * @returns {boolean} Success or failure.
135 async function _ensureLangPackInstalledImpl(langPack) {
136 const availablelocales = await getAvailableLocales();
137 if (availablelocales.includes(langPack.target_locale)) {
138 // The langpack is already installed.
142 return mockable.installLangPack(langPack);
146 * These are all functions with side effects or configuration options that should be
147 * mockable for tests.
151 * @returns {LangPack[] | null}
153 async getAvailableLangpacks() {
155 return lazy.AddonRepository.getAvailableLangpacks();
158 `Failed to get the list of available language packs: ${error?.message}`
165 * Use the AddonManager to install an addon from the URL.
166 * @param {LangPack} langPack
168 async installLangPack(langPack) {
171 install = await lazy.AddonManager.getInstallForURL(langPack.url, {
174 source: "about:welcome",
178 console.error(error);
183 await install.install();
185 console.error(error);
192 * Returns the available locales, including the fallback locale, which may not include
193 * all of the resources, in cases where the defaultLocale is not "en-US".
195 * @returns {string[]}
197 getAvailableLocalesIncludingFallback() {
198 return Services.locale.availableLocales;
205 return Services.locale.defaultLocale;
211 getLastFallbackLocale() {
212 return Services.locale.lastFallbackLocale;
218 getAppLocaleAsBCP47() {
219 return Services.locale.appLocaleAsBCP47;
226 // Allow the system locale to be overridden for manual testing.
227 const systemLocaleOverride = Services.prefs.getCharPref(
228 "intl.multilingual.aboutWelcome.systemLocaleOverride",
231 if (systemLocaleOverride) {
233 // If the locale can't be parsed, ignore the pref.
234 new Services.intl.Locale(systemLocaleOverride);
235 return systemLocaleOverride;
239 const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
242 return osPrefs.systemLocale;
246 * @param {string[]} locales The BCP 47 locale identifiers.
248 setRequestedAppLocales(locales) {
249 Services.locale.requestedLocales = locales;
254 * This function is really only setting `Services.locale.requestedLocales`, but it's
255 * using the `mockable` object to allow this behavior to be mocked in tests.
257 * @param {string[]} locales The BCP 47 locale identifiers.
259 function setRequestedAppLocales(locales) {
260 mockable.setRequestedAppLocales(locales);
264 * A serializable Intl.Locale.
266 * @typedef StructuredLocale
268 * @property {string} baseName
269 * @property {string} language
270 * @property {string} region
274 * In telemetry data, some of the system locales show up as blank. Guard against this
275 * and any other malformed locale information provided by the system by wrapping the call
278 * @param {string} locale
279 * @returns {StructuredLocale | null}
281 function getStructuredLocaleOrNull(localeString) {
283 const locale = new Services.intl.Locale(localeString);
285 baseName: locale.baseName,
286 language: locale.language,
287 region: locale.region,
295 * Determine the system and app locales, and how much the locales match.
298 * systemLocale: StructuredLocale,
299 * appLocale: StructuredLocale,
300 * matchType: "unknown" | "language-mismatch" | "region-mismatch" | "match",
303 function getAppAndSystemLocaleInfo() {
304 // Convert locale strings into structured locale objects.
305 const systemLocaleRaw = mockable.getSystemLocale();
306 const appLocaleRaw = mockable.getAppLocaleAsBCP47();
308 const systemLocale = getStructuredLocaleOrNull(systemLocaleRaw);
309 const appLocale = getStructuredLocaleOrNull(appLocaleRaw);
311 let matchType = "unknown";
312 if (systemLocale && appLocale) {
313 if (systemLocale.language !== appLocale.language) {
314 matchType = "language-mismatch";
315 } else if (systemLocale.region !== appLocale.region) {
316 matchType = "region-mismatch";
322 // Live reloading with bidi switching may not be supported.
323 let canLiveReload = null;
324 if (systemLocale && appLocale) {
325 const systemDirection = Services.intl.getScriptDirection(
326 systemLocale.language
328 const appDirection = Services.intl.getScriptDirection(appLocale.language);
329 const supportsBidiSwitching = Services.prefs.getBoolPref(
330 "intl.multilingual.liveReloadBidirectional",
333 canLiveReload = systemDirection === appDirection || supportsBidiSwitching;
336 // Return the Intl.Locale in a serializable form.
344 // These can be used as Fluent message args.
346 systemLanguage: systemLocale
347 ? Services.intl.getLocaleDisplayNames(
349 [systemLocale.baseName],
350 { preferNative: true }
353 appLanguage: appLocale
354 ? Services.intl.getLocaleDisplayNames(undefined, [appLocale.baseName], {
363 * Filter the lastFallbackLocale from availableLocales if it doesn't have all
364 * of the needed strings.
366 * When the lastFallbackLocale isn't the defaultLocale, then by default only
367 * fluent strings are included. To fully use that locale you need the langpack
368 * to be installed, so if it isn't installed remove it from availableLocales.
370 async function getAvailableLocales() {
371 const availableLocales = mockable.getAvailableLocalesIncludingFallback();
372 const defaultLocale = mockable.getDefaultLocale();
373 const lastFallbackLocale = mockable.getLastFallbackLocale();
374 // If defaultLocale isn't lastFallbackLocale, then we still need the langpack
375 // for lastFallbackLocale for it to be useful.
376 if (defaultLocale != lastFallbackLocale) {
377 let lastFallbackId = `langpack-${lastFallbackLocale}@firefox.mozilla.org`;
378 let lastFallbackInstalled = await lazy.AddonManager.getAddonByID(
381 if (!lastFallbackInstalled) {
382 return availableLocales.filter(locale => locale != lastFallbackLocale);
385 return availableLocales;
388 export var LangPackMatcher = {
389 negotiateLangPackForLanguageMismatch,
390 ensureLangPackInstalled,
391 getAppAndSystemLocaleInfo,
392 setRequestedAppLocales,