tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

LangPackMatcher.sys.mjs (11305B)


      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/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
      9  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
     10 });
     11 
     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.");
     16 }
     17 
     18 /**
     19 * Attempts to find an appropriate langpack for a given language. The async function
     20 * is infallible, but may not return a langpack.
     21 *
     22 * @returns {{
     23 *   langPack: LangPack | null,
     24 *   langPackDisplayName: string | null
     25 * }}
     26 */
     27 async function negotiateLangPackForLanguageMismatch() {
     28  const localeInfo = getAppAndSystemLocaleInfo();
     29  const nullResult = {
     30    langPack: null,
     31    langPackDisplayName: null,
     32  };
     33  if (!localeInfo.systemLocale) {
     34    // The system locale info was not valid.
     35    return nullResult;
     36  }
     37 
     38  /**
     39   * Fetch the available langpacks from AMO.
     40   *
     41   * @type {Array<LangPack>}
     42   */
     43  const availableLangpacks = await mockable.getAvailableLangpacks();
     44  if (!availableLangpacks) {
     45    return nullResult;
     46  }
     47 
     48  /**
     49   * Figure out a langpack to recommend.
     50   *
     51   * @type {LangPack | null}
     52   */
     53  const langPack =
     54    // First look for a langpack that matches the baseName, which may include a script.
     55    // e.g. system "fr-FR" matches langpack "fr-FR"
     56    //      system "en-GB" matches langpack "en-GB".
     57    //      system "zh-Hant-CN" matches langpack "zh-Hant-CN".
     58    availableLangpacks.find(
     59      ({ target_locale }) => target_locale === localeInfo.systemLocale.baseName
     60    ) ||
     61    // Next try matching language and region while excluding script
     62    // e.g. system "zh-Hant-TW" matches langpack "zh-TW" but not "zh-CN".
     63    availableLangpacks.find(
     64      ({ target_locale }) =>
     65        target_locale ===
     66        `${localeInfo.systemLocale.language}-${localeInfo.systemLocale.region}`
     67    ) ||
     68    // Next look for langpacks that just match the language.
     69    // e.g. system "fr-FR" matches langpack "fr".
     70    //      system "en-AU" matches langpack "en".
     71    availableLangpacks.find(
     72      ({ target_locale }) => target_locale === localeInfo.systemLocale.language
     73    ) ||
     74    // Next look for a langpack that matches the language, but not the region.
     75    // e.g. "es-CL" (Chilean Spanish) as a system language matching
     76    //      "es-ES" (European Spanish)
     77    availableLangpacks.find(({ target_locale }) =>
     78      target_locale.startsWith(`${localeInfo.systemLocale.language}-`)
     79    ) ||
     80    null;
     81 
     82  if (!langPack) {
     83    return nullResult;
     84  }
     85 
     86  return {
     87    langPack,
     88    langPackDisplayName: Services.intl.getLocaleDisplayNames(
     89      undefined,
     90      [langPack.target_locale],
     91      { preferNative: true }
     92    )[0],
     93  };
     94 }
     95 
     96 // If a langpack is being installed, allow blocking on that.
     97 let installingLangpack = new Map();
     98 
     99 /**
    100 * @typedef {LangPack}
    101 * @type {object}
    102 * @property {string} target_locale
    103 * @property {string} url
    104 * @property {string} hash
    105 */
    106 
    107 /**
    108 * Ensure that a given lanpack is installed.
    109 *
    110 * @param {LangPack} langPack
    111 * @returns {Promise<boolean>} Success or failure.
    112 */
    113 function ensureLangPackInstalled(langPack) {
    114  if (!langPack) {
    115    throw new Error("Expected a LangPack to install.");
    116  }
    117  // Make sure any outstanding calls get resolved before attempting another call.
    118  // This guards against any quick page refreshes attempting to install the langpack
    119  // twice.
    120  const inProgress = installingLangpack.get(langPack.hash);
    121  if (inProgress) {
    122    return inProgress;
    123  }
    124  const promise = _ensureLangPackInstalledImpl(langPack);
    125  installingLangpack.set(langPack.hash, promise);
    126  promise.finally(() => {
    127    installingLangpack.delete(langPack.hash);
    128  });
    129  return promise;
    130 }
    131 
    132 /**
    133 * @param {LangPack} langPack
    134 * @returns {boolean} Success or failure.
    135 */
    136 async function _ensureLangPackInstalledImpl(langPack) {
    137  const availablelocales = await getAvailableLocales();
    138  if (availablelocales.includes(langPack.target_locale)) {
    139    // The langpack is already installed.
    140    return true;
    141  }
    142 
    143  return mockable.installLangPack(langPack);
    144 }
    145 
    146 /**
    147 * These are all functions with side effects or configuration options that should be
    148 * mockable for tests.
    149 */
    150 const mockable = {
    151  /**
    152   * @returns {LangPack[] | null}
    153   */
    154  async getAvailableLangpacks() {
    155    try {
    156      return lazy.AddonRepository.getAvailableLangpacks();
    157    } catch (error) {
    158      console.error(
    159        `Failed to get the list of available language packs: ${error?.message}`
    160      );
    161      return null;
    162    }
    163  },
    164 
    165  /**
    166   * Use the AddonManager to install an addon from the URL.
    167   *
    168   * @param {LangPack} langPack
    169   */
    170  async installLangPack(langPack) {
    171    let install;
    172    try {
    173      install = await lazy.AddonManager.getInstallForURL(langPack.url, {
    174        hash: langPack.hash,
    175        telemetryInfo: {
    176          source: "about:welcome",
    177        },
    178      });
    179    } catch (error) {
    180      console.error(error);
    181      return false;
    182    }
    183 
    184    try {
    185      await install.install();
    186    } catch (error) {
    187      console.error(error);
    188      return false;
    189    }
    190    return true;
    191  },
    192 
    193  /**
    194   * Returns the available locales, including the fallback locale, which may not include
    195   * all of the resources, in cases where the defaultLocale is not "en-US".
    196   *
    197   * @returns {string[]}
    198   */
    199  getAvailableLocalesIncludingFallback() {
    200    return Services.locale.availableLocales;
    201  },
    202 
    203  /**
    204   * @returns {string}
    205   */
    206  getDefaultLocale() {
    207    return Services.locale.defaultLocale;
    208  },
    209 
    210  /**
    211   * @returns {string}
    212   */
    213  getLastFallbackLocale() {
    214    return Services.locale.lastFallbackLocale;
    215  },
    216 
    217  /**
    218   * @returns {string}
    219   */
    220  getAppLocaleAsBCP47() {
    221    return Services.locale.appLocaleAsBCP47;
    222  },
    223 
    224  /**
    225   * @returns {string}
    226   */
    227  getSystemLocale() {
    228    // Allow the system locale to be overridden for manual testing.
    229    const systemLocaleOverride = Services.prefs.getCharPref(
    230      "intl.multilingual.aboutWelcome.systemLocaleOverride",
    231      null
    232    );
    233    if (systemLocaleOverride) {
    234      try {
    235        // If the locale can't be parsed, ignore the pref.
    236        new Services.intl.Locale(systemLocaleOverride);
    237        return systemLocaleOverride;
    238      } catch (_error) {}
    239    }
    240 
    241    const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
    242      Ci.mozIOSPreferences
    243    );
    244    return osPrefs.systemLocale;
    245  },
    246 
    247  /**
    248   * @param {string[]} locales The BCP 47 locale identifiers.
    249   */
    250  setRequestedAppLocales(locales) {
    251    Services.locale.requestedLocales = locales;
    252  },
    253 };
    254 
    255 /**
    256 * This function is really only setting `Services.locale.requestedLocales`, but it's
    257 * using the `mockable` object to allow this behavior to be mocked in tests.
    258 *
    259 * @param {string[]} locales The BCP 47 locale identifiers.
    260 */
    261 function setRequestedAppLocales(locales) {
    262  mockable.setRequestedAppLocales(locales);
    263 }
    264 
    265 /**
    266 * A serializable Intl.Locale.
    267 *
    268 * @typedef StructuredLocale
    269 * @type {object}
    270 * @property {string} baseName
    271 * @property {string} language
    272 * @property {string} region
    273 */
    274 
    275 /**
    276 * In telemetry data, some of the system locales show up as blank. Guard against this
    277 * and any other malformed locale information provided by the system by wrapping the call
    278 * into a catch/try.
    279 *
    280 * @param {string} locale
    281 * @returns {StructuredLocale | null}
    282 */
    283 function getStructuredLocaleOrNull(localeString) {
    284  try {
    285    const locale = new Services.intl.Locale(localeString);
    286    return {
    287      baseName: locale.baseName,
    288      language: locale.language,
    289      region: locale.region,
    290    };
    291  } catch (_err) {
    292    return null;
    293  }
    294 }
    295 
    296 /**
    297 * Determine the system and app locales, and how much the locales match.
    298 *
    299 * @returns {{
    300 *  systemLocale: StructuredLocale,
    301 *  appLocale: StructuredLocale,
    302 *  matchType: "unknown" | "language-mismatch" | "region-mismatch" | "match",
    303 * }}
    304 */
    305 function getAppAndSystemLocaleInfo() {
    306  // Convert locale strings into structured locale objects.
    307  const systemLocaleRaw = mockable.getSystemLocale();
    308  const appLocaleRaw = mockable.getAppLocaleAsBCP47();
    309 
    310  const systemLocale = getStructuredLocaleOrNull(systemLocaleRaw);
    311  const appLocale = getStructuredLocaleOrNull(appLocaleRaw);
    312 
    313  let matchType = "unknown";
    314  if (systemLocale && appLocale) {
    315    if (systemLocale.language !== appLocale.language) {
    316      matchType = "language-mismatch";
    317    } else if (systemLocale.region !== appLocale.region) {
    318      matchType = "region-mismatch";
    319    } else {
    320      matchType = "match";
    321    }
    322  }
    323 
    324  // Live reloading with bidi switching may not be supported.
    325  let canLiveReload = null;
    326  if (systemLocale && appLocale) {
    327    const systemDirection = Services.intl.getScriptDirection(
    328      systemLocale.language
    329    );
    330    const appDirection = Services.intl.getScriptDirection(appLocale.language);
    331    const supportsBidiSwitching = Services.prefs.getBoolPref(
    332      "intl.multilingual.liveReloadBidirectional",
    333      false
    334    );
    335    canLiveReload = systemDirection === appDirection || supportsBidiSwitching;
    336  }
    337  return {
    338    // Return the Intl.Locale in a serializable form.
    339    systemLocaleRaw,
    340    systemLocale,
    341    appLocaleRaw,
    342    appLocale,
    343    matchType,
    344    canLiveReload,
    345 
    346    // These can be used as Fluent message args.
    347    displayNames: {
    348      systemLanguage: systemLocale
    349        ? Services.intl.getLocaleDisplayNames(
    350            undefined,
    351            [systemLocale.baseName],
    352            { preferNative: true }
    353          )[0]
    354        : null,
    355      appLanguage: appLocale
    356        ? Services.intl.getLocaleDisplayNames(undefined, [appLocale.baseName], {
    357            preferNative: true,
    358          })[0]
    359        : null,
    360    },
    361  };
    362 }
    363 
    364 /**
    365 * Filter the lastFallbackLocale from availableLocales if it doesn't have all
    366 * of the needed strings.
    367 *
    368 * When the lastFallbackLocale isn't the defaultLocale, then by default only
    369 * fluent strings are included. To fully use that locale you need the langpack
    370 * to be installed, so if it isn't installed remove it from availableLocales.
    371 */
    372 async function getAvailableLocales() {
    373  const availableLocales = mockable.getAvailableLocalesIncludingFallback();
    374  const defaultLocale = mockable.getDefaultLocale();
    375  const lastFallbackLocale = mockable.getLastFallbackLocale();
    376  // If defaultLocale isn't lastFallbackLocale, then we still need the langpack
    377  // for lastFallbackLocale for it to be useful.
    378  if (defaultLocale != lastFallbackLocale) {
    379    let lastFallbackId = `langpack-${lastFallbackLocale}@firefox.mozilla.org`;
    380    let lastFallbackInstalled =
    381      await lazy.AddonManager.getAddonByID(lastFallbackId);
    382    if (!lastFallbackInstalled) {
    383      return availableLocales.filter(locale => locale != lastFallbackLocale);
    384    }
    385  }
    386  return availableLocales;
    387 }
    388 
    389 export var LangPackMatcher = {
    390  negotiateLangPackForLanguageMismatch,
    391  ensureLangPackInstalled,
    392  getAppAndSystemLocaleInfo,
    393  setRequestedAppLocales,
    394  getAvailableLocales,
    395  mockable,
    396 };