Commit 5d76d031 authored by Greg Tatum's avatar Greg Tatum
Browse files

Bug 1829687 - Add a mechanism for deducing the preferred languages for a user; r=nordzilla

This is a big logic change, but with switching the views and adding the
translations command to the app menu, it's much easier to invoke the
translations panel in uncontrolled situations. This patch adds a
best-effort approach to deducing an appropriate language for the user.

Differential Revision: https://phabricator.services.mozilla.com/D179085
parent 11641611
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -911,6 +911,19 @@ export class TranslationsChild extends JSWindowActorChild {
    return this.sendQuery("Translations:GetLanguagePairs");
  }

  /**
   * The ordered list of preferred BCP 47 language tags.
   *
   *   1. App languages
   *   2. Web requested languages
   *   3. OS languages
   *
   * @returns {Promise<string[]>}
   */
  getPreferredLanguages() {
    return this.sendQuery("Translations:GetPreferredLanguages");
  }

  /**
   * Retrieve the payload for creating a LanguageIdEngine.
   *
+98 −0
Original line number Diff line number Diff line
@@ -215,6 +215,17 @@ export class TranslationsParent extends JSWindowActorParent {
   */
  static #translateOnPageReload = null;

  /**
   * An ordered list of preferred languages based on:
   *   1. App languages
   *   2. Web requested languages
   *   3. OS language
   *
   * @type {null | string[]}
   */
  static #preferredLanguages = null;
  static #observingLanguages = false;

  // On a fast connection, 10 concurrent downloads were measured to be the fastest when
  // downloading all of the language files.
  static MAX_CONCURRENT_DOWNLOADS = 10;
@@ -280,6 +291,90 @@ export class TranslationsParent extends JSWindowActorParent {
    );
  }

  static #resetPreferredLanguages() {
    TranslationsParent.#preferredLanguages = null;
    TranslationsParent.getPreferredLanguages();
  }

  static async observe(_subject, topic, _data) {
    switch (topic) {
      case "nsPref:changed":
      case "intl:app-locales-changed": {
        this.#resetPreferredLanguages();
        break;
      }
      default:
        throw new Error("Unknown observer event", topic);
    }
  }

  /**
   * An ordered list of preferred languages based on:
   *
   *   1. App languages
   *   2. Web requested languages
   *   3. OS language
   *
   * @returns {string[]}
   */
  static getPreferredLanguages() {
    if (TranslationsParent.#preferredLanguages) {
      return TranslationsParent.#preferredLanguages;
    }

    if (!TranslationsParent.#observingLanguages) {
      Services.obs.addObserver(
        TranslationsParent.#resetPreferredLanguages,
        "intl:app-locales-changed"
      );
      Services.prefs.addObserver(
        "intl.accept_languages",
        TranslationsParent.#resetPreferredLanguages
      );
      TranslationsParent.#observingLanguages = true;
    }

    // The "Accept-Language" values that the localizer or user has indicated for
    // the preferences for the web. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
    // Note that this preference often falls back ultimately to English, even if the
    // user doesn't actually speak English, or to other languages they do not speak.
    // However, this preference will be used as an indication that a user may prefer
    // this language.
    const webLanguages = Services.prefs
      .getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString)
      .data.split(/\s*,\s*/g);

    // The system language could also be a good option for a language to offer the user.
    const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
      Ci.mozIOSPreferences
    );
    const { systemLocales } = osPrefs;

    // Combine the locales together.
    const preferredLocales = new Set([
      ...Services.locale.appLocalesAsBCP47,
      ...webLanguages,
      ...systemLocales,
    ]);

    // Attempt to convert the locales to lang tags. Do not completely trust the
    // values coming from preferences and the OS to have been validated as correct
    // BCP 47 locale identifiers.
    const langTags = new Set();
    for (const locale of preferredLocales) {
      try {
        langTags.add(new Intl.Locale(locale).language);
      } catch (_) {
        // The locale was invalid, discard it.
      }
    }

    // Convert the Set to an array to indicate that it is an ordered listing of languages.
    TranslationsParent.#preferredLanguages = [...langTags];

    return TranslationsParent.#preferredLanguages;
  }

  async receiveMessage({ name, data }) {
    switch (name) {
      case "Translations:GetTranslationsEnginePayload": {
@@ -357,6 +452,9 @@ export class TranslationsParent extends JSWindowActorParent {
      case "Translations:GetLanguagePairs": {
        return this.getLanguagePairs();
      }
      case "Translations:GetPreferredLanguages": {
        return TranslationsParent.getPreferredLanguages();
      }
      case "Translations:EngineIsReady": {
        this.isEngineReady = true;
        this.languageState.isEngineReady = true;