1569 lines
48 KiB
JavaScript
1569 lines
48 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/* eslint-env mozilla/browser-window */
|
|
|
|
/* eslint-disable jsdoc/valid-types */
|
|
/**
|
|
* @typedef {import("../../../../toolkit/components/translations/translations").LangTags} LangTags
|
|
*/
|
|
/* eslint-enable jsdoc/valid-types */
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
TranslationsTelemetry:
|
|
"chrome://browser/content/translations/TranslationsTelemetry.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* The set of actions that can occur from interaction with the
|
|
* translations panel.
|
|
*/
|
|
const PageAction = Object.freeze({
|
|
NO_CHANGE: "NO_CHANGE",
|
|
RESTORE_PAGE: "RESTORE_PAGE",
|
|
TRANSLATE_PAGE: "TRANSLATE_PAGE",
|
|
});
|
|
|
|
/**
|
|
* A mechanism for determining the next relevant page action
|
|
* based on the current translated state of the page and the state
|
|
* of the persistent options in the translations panel settings.
|
|
*/
|
|
class CheckboxPageAction {
|
|
/**
|
|
* Whether or not translations is active on the page.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#translationsActive = false;
|
|
|
|
/**
|
|
* Whether the always-translate-language menuitem is checked
|
|
* in the translations panel settings menu.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#alwaysTranslateLanguage = false;
|
|
|
|
/**
|
|
* Whether the never-translate-language menuitem is checked
|
|
* in the translations panel settings menu.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#neverTranslateLanguage = false;
|
|
|
|
/**
|
|
* Whether the never-translate-site menuitem is checked
|
|
* in the translations panel settings menu.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#neverTranslateSite = false;
|
|
|
|
/**
|
|
* @param {boolean} translationsActive
|
|
* @param {boolean} alwaysTranslateLanguage
|
|
* @param {boolean} neverTranslateLanguage
|
|
* @param {boolean} neverTranslateSite
|
|
*/
|
|
constructor(
|
|
translationsActive,
|
|
alwaysTranslateLanguage,
|
|
neverTranslateLanguage,
|
|
neverTranslateSite
|
|
) {
|
|
this.#translationsActive = translationsActive;
|
|
this.#alwaysTranslateLanguage = alwaysTranslateLanguage;
|
|
this.#neverTranslateLanguage = neverTranslateLanguage;
|
|
this.#neverTranslateSite = neverTranslateSite;
|
|
}
|
|
|
|
/**
|
|
* Accepts four integers that are either 0 or 1 and returns
|
|
* a single, unique number for each possible combination of
|
|
* values.
|
|
*
|
|
* @param {number} translationsActive
|
|
* @param {number} alwaysTranslateLanguage
|
|
* @param {number} neverTranslateLanguage
|
|
* @param {number} neverTranslateSite
|
|
*
|
|
* @returns {number} - An integer representation of the state
|
|
*/
|
|
static #computeState(
|
|
translationsActive,
|
|
alwaysTranslateLanguage,
|
|
neverTranslateLanguage,
|
|
neverTranslateSite
|
|
) {
|
|
return (
|
|
(translationsActive << 3) |
|
|
(alwaysTranslateLanguage << 2) |
|
|
(neverTranslateLanguage << 1) |
|
|
neverTranslateSite
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the current state of the data members as a single number.
|
|
*
|
|
* @returns {number} - An integer representation of the state
|
|
*/
|
|
#state() {
|
|
return CheckboxPageAction.#computeState(
|
|
Number(this.#translationsActive),
|
|
Number(this.#alwaysTranslateLanguage),
|
|
Number(this.#neverTranslateLanguage),
|
|
Number(this.#neverTranslateSite)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the next page action to take when the always-translate-language
|
|
* menuitem is toggled in the translations panel settings menu.
|
|
*
|
|
* @returns {PageAction}
|
|
*/
|
|
alwaysTranslateLanguage() {
|
|
switch (this.#state()) {
|
|
case CheckboxPageAction.#computeState(1, 1, 0, 1):
|
|
case CheckboxPageAction.#computeState(1, 1, 0, 0):
|
|
return PageAction.RESTORE_PAGE;
|
|
case CheckboxPageAction.#computeState(0, 0, 1, 0):
|
|
case CheckboxPageAction.#computeState(0, 0, 0, 0):
|
|
return PageAction.TRANSLATE_PAGE;
|
|
}
|
|
return PageAction.NO_CHANGE;
|
|
}
|
|
|
|
/**
|
|
* Returns the next page action to take when the never-translate-language
|
|
* menuitem is toggled in the translations panel settings menu.
|
|
*
|
|
* @returns {PageAction}
|
|
*/
|
|
neverTranslateLanguage() {
|
|
switch (this.#state()) {
|
|
case CheckboxPageAction.#computeState(1, 1, 0, 1):
|
|
case CheckboxPageAction.#computeState(1, 1, 0, 0):
|
|
case CheckboxPageAction.#computeState(1, 0, 0, 1):
|
|
case CheckboxPageAction.#computeState(1, 0, 0, 0):
|
|
return PageAction.RESTORE_PAGE;
|
|
}
|
|
return PageAction.NO_CHANGE;
|
|
}
|
|
|
|
/**
|
|
* Returns the next page action to take when the never-translate-site
|
|
* menuitem is toggled in the translations panel settings menu.
|
|
*
|
|
* @returns {PageAction}
|
|
*/
|
|
neverTranslateSite() {
|
|
switch (this.#state()) {
|
|
case CheckboxPageAction.#computeState(1, 1, 0, 0):
|
|
case CheckboxPageAction.#computeState(1, 0, 1, 0):
|
|
case CheckboxPageAction.#computeState(1, 0, 0, 0):
|
|
return PageAction.RESTORE_PAGE;
|
|
case CheckboxPageAction.#computeState(0, 1, 0, 1):
|
|
return PageAction.TRANSLATE_PAGE;
|
|
}
|
|
return PageAction.NO_CHANGE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This singleton class controls the Translations popup panel.
|
|
*
|
|
* This component is a `/browser` component, and the actor is a `/toolkit` actor, so care
|
|
* must be taken to keep the presentation (this component) from the state management
|
|
* (the Translations actor). This class reacts to state changes coming from the
|
|
* Translations actor.
|
|
*/
|
|
var TranslationsPanel = new (class {
|
|
/** @type {Console?} */
|
|
#console;
|
|
|
|
/**
|
|
* The cached detected languages for both the document and the user.
|
|
*
|
|
* @type {null | LangTags}
|
|
*/
|
|
detectedLanguages = null;
|
|
|
|
/**
|
|
* Lazily get a console instance. Note that this script is loaded in very early to
|
|
* the browser loading process, and may run before the console is avialable. In
|
|
* this case the console will return as `undefined`.
|
|
*
|
|
* @returns {Console | void}
|
|
*/
|
|
get console() {
|
|
if (!this.#console) {
|
|
try {
|
|
this.#console = console.createInstance({
|
|
maxLogLevelPref: "browser.translations.logLevel",
|
|
prefix: "Translations",
|
|
});
|
|
} catch {
|
|
// The console may not be initialized yet.
|
|
}
|
|
}
|
|
return this.#console;
|
|
}
|
|
|
|
/**
|
|
* Tracks if the popup is open, or scheduled to be open.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#isPopupOpen = false;
|
|
|
|
/**
|
|
* Show the introductory message on the first load, and keep on showing it for
|
|
* this URI, until the user navigates away.
|
|
*
|
|
* @type {string | null}
|
|
*/
|
|
#firstShowUriSpec = null;
|
|
|
|
/**
|
|
* Where the lazy elements are stored.
|
|
*
|
|
* @type {Record<string, Element>?}
|
|
*/
|
|
#lazyElements;
|
|
|
|
/**
|
|
* Lazily creates the dom elements, and lazily selects them.
|
|
*
|
|
* @returns {Record<string, Element>}
|
|
*/
|
|
get elements() {
|
|
if (!this.#lazyElements) {
|
|
// Lazily turn the template into a DOM element.
|
|
/** @type {HTMLTemplateElement} */
|
|
const wrapper = document.getElementById("template-translations-panel");
|
|
const panel = wrapper.content.firstElementChild;
|
|
wrapper.replaceWith(wrapper.content);
|
|
|
|
const settingsButton = document.getElementById(
|
|
"translations-panel-settings"
|
|
);
|
|
// Clone the settings toolbarbutton across all the views.
|
|
for (const header of panel.querySelectorAll(".panel-header")) {
|
|
if (header.contains(settingsButton)) {
|
|
continue;
|
|
}
|
|
const settingsButtonClone = settingsButton.cloneNode(true);
|
|
settingsButtonClone.removeAttribute("id");
|
|
header.appendChild(settingsButtonClone);
|
|
}
|
|
|
|
// Lazily select the elements.
|
|
this.#lazyElements = {
|
|
panel,
|
|
settingsButton,
|
|
// The rest of the elements are set by the getter below.
|
|
};
|
|
|
|
/**
|
|
* Define a getter on #lazyElements that gets the element by an id
|
|
* or class name.
|
|
*/
|
|
const getter = (name, discriminator) => {
|
|
let element;
|
|
Object.defineProperty(this.#lazyElements, name, {
|
|
get: () => {
|
|
if (!element) {
|
|
if (discriminator[0] === ".") {
|
|
// Lookup by class
|
|
element = document.querySelector(discriminator);
|
|
} else {
|
|
// Lookup by id
|
|
element = document.getElementById(discriminator);
|
|
}
|
|
}
|
|
if (!element) {
|
|
throw new Error(
|
|
`Could not find "${name}" at "#${discriminator}".`
|
|
);
|
|
}
|
|
return element;
|
|
},
|
|
});
|
|
};
|
|
|
|
// Getters by id
|
|
getter("appMenuButton", "PanelUI-menu-button");
|
|
getter("cancelButton", "translations-panel-cancel");
|
|
getter(
|
|
"changeSourceLanguageButton",
|
|
"translations-panel-change-source-language"
|
|
);
|
|
getter("defaultTranslate", "translations-panel-translate");
|
|
getter("dismissErrorButton", "translations-panel-dismiss-error");
|
|
getter("error", "translations-panel-error");
|
|
getter("errorMessage", "translations-panel-error-message");
|
|
getter("errorMessageHint", "translations-panel-error-message-hint");
|
|
getter("errorHintAction", "translations-panel-translate-hint-action");
|
|
getter("fromMenuList", "translations-panel-from");
|
|
getter("fromLabel", "translations-panel-from-label");
|
|
getter("header", "translations-panel-header");
|
|
getter("intro", "translations-panel-intro");
|
|
getter("langSelection", "translations-panel-lang-selection");
|
|
getter("multiview", "translations-panel-multiview");
|
|
getter("restoreButton", "translations-panel-restore-button");
|
|
getter("toLabel", "translations-panel-to-label");
|
|
getter("toMenuList", "translations-panel-to");
|
|
getter("translateButton", "translations-panel-translate");
|
|
getter("unsupportedHint", "translations-panel-error-unsupported-hint");
|
|
|
|
// Getters by class
|
|
getter(
|
|
"alwaysTranslateLanguageMenuItem",
|
|
".always-translate-language-menuitem"
|
|
);
|
|
getter("manageLanguagesMenuItem", ".manage-languages-menuitem");
|
|
getter(
|
|
"neverTranslateLanguageMenuItem",
|
|
".never-translate-language-menuitem"
|
|
);
|
|
getter("neverTranslateSiteMenuItem", ".never-translate-site-menuitem");
|
|
}
|
|
|
|
return this.#lazyElements;
|
|
}
|
|
|
|
#lazyButtonElements = null;
|
|
|
|
/**
|
|
* When accessing `this.elements` the first time, it de-lazifies the custom components
|
|
* that are needed for the popup. Avoid that by having a second element lookup
|
|
* just for modifying the button.
|
|
*/
|
|
get buttonElements() {
|
|
if (!this.#lazyButtonElements) {
|
|
this.#lazyButtonElements = {
|
|
button: document.getElementById("translations-button"),
|
|
buttonLocale: document.getElementById("translations-button-locale"),
|
|
buttonCircleArrows: document.getElementById(
|
|
"translations-button-circle-arrows"
|
|
),
|
|
};
|
|
}
|
|
return this.#lazyButtonElements;
|
|
}
|
|
|
|
/**
|
|
* Cache the last command used for error hints so that it can be later removed.
|
|
*/
|
|
#lastHintCommand = null;
|
|
|
|
/**
|
|
* @param {object} options
|
|
* @param {string} options.message - l10n id
|
|
* @param {string} options.hint - l10n id
|
|
* @param {string} options.actionText - l10n id
|
|
* @param {Function} options.actionCommand - The action to perform.
|
|
*/
|
|
#showError({
|
|
message,
|
|
hint,
|
|
actionText: hintCommandText,
|
|
actionCommand: hintCommand,
|
|
}) {
|
|
const { error, errorMessage, errorMessageHint, errorHintAction, intro } =
|
|
this.elements;
|
|
error.hidden = false;
|
|
intro.hidden = true;
|
|
document.l10n.setAttributes(errorMessage, message);
|
|
|
|
if (hint) {
|
|
errorMessageHint.hidden = false;
|
|
document.l10n.setAttributes(errorMessageHint, hint);
|
|
} else {
|
|
errorMessageHint.hidden = true;
|
|
}
|
|
|
|
if (hintCommand && hintCommandText) {
|
|
errorHintAction.removeEventListener("command", this.#lastHintCommand);
|
|
this.#lastHintCommand = hintCommand;
|
|
errorHintAction.addEventListener("command", hintCommand);
|
|
errorHintAction.hidden = false;
|
|
document.l10n.setAttributes(errorHintAction, hintCommandText);
|
|
} else {
|
|
errorHintAction.hidden = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {TranslationsParent}
|
|
*/
|
|
#getTranslationsActor() {
|
|
const actor =
|
|
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
|
|
"Translations"
|
|
);
|
|
|
|
if (!actor) {
|
|
throw new Error("Unable to get the TranslationsParent");
|
|
}
|
|
return actor;
|
|
}
|
|
|
|
/**
|
|
* Fetches the language tags for the document and the user and caches the results
|
|
* Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched.
|
|
* This requires a bit of work to do, so prefer the cached version when possible.
|
|
*
|
|
* @returns {Promise<LangTags>}
|
|
*/
|
|
async #fetchDetectedLanguages() {
|
|
this.detectedLanguages =
|
|
await this.#getTranslationsActor().getDetectedLanguages();
|
|
return this.detectedLanguages;
|
|
}
|
|
|
|
/**
|
|
* If the detected language tags have been retrieved previously, return the cached
|
|
* version. Otherwise do a fresh lookup of the document's language tag.
|
|
*
|
|
* @returns {Promise<LangTags>}
|
|
*/
|
|
async #getCachedDetectedLanguages() {
|
|
if (!this.detectedLanguages) {
|
|
return this.#fetchDetectedLanguages();
|
|
}
|
|
return this.detectedLanguages;
|
|
}
|
|
|
|
/**
|
|
* @type {"initialized" | "error" | "uninitialized"}
|
|
*/
|
|
#langListsPhase = "uninitialized";
|
|
|
|
/**
|
|
* Builds the <menulist> of languages for both the "from" and "to". This can be
|
|
* called every time the popup is shown, as it will retry when there is an error
|
|
* (such as a network error) or be a noop if it's already initialized.
|
|
*
|
|
* TODO(Bug 1813796) This needs to be updated when the supported languages change
|
|
* via RemoteSettings.
|
|
*/
|
|
async #ensureLangListsBuilt() {
|
|
switch (this.#langListsPhase) {
|
|
case "initialized":
|
|
// This has already been initialized.
|
|
return;
|
|
case "error":
|
|
// Attempt to re-initialize.
|
|
this.#langListsPhase = "uninitialized";
|
|
break;
|
|
case "uninitialized":
|
|
// Ready to initialize.
|
|
break;
|
|
default:
|
|
this.console?.error("Unknown langList phase", this.#langListsPhase);
|
|
}
|
|
|
|
try {
|
|
/** @type {SupportedLanguages} */
|
|
const { languagePairs, fromLanguages, toLanguages } =
|
|
await TranslationsParent.getSupportedLanguages();
|
|
|
|
// Verify that we are in a proper state.
|
|
if (languagePairs.length === 0) {
|
|
throw new Error("No translation languages were retrieved.");
|
|
}
|
|
|
|
const { panel } = this.elements;
|
|
const fromPopups = panel.querySelectorAll(
|
|
".translations-panel-language-menupopup-from"
|
|
);
|
|
const toPopups = panel.querySelectorAll(
|
|
".translations-panel-language-menupopup-to"
|
|
);
|
|
|
|
for (const popup of fromPopups) {
|
|
for (const { langTag, displayName } of fromLanguages) {
|
|
const fromMenuItem = document.createXULElement("menuitem");
|
|
fromMenuItem.setAttribute("value", langTag);
|
|
fromMenuItem.setAttribute("label", displayName);
|
|
popup.appendChild(fromMenuItem);
|
|
}
|
|
}
|
|
|
|
for (const popup of toPopups) {
|
|
for (const { langTag, displayName } of toLanguages) {
|
|
const toMenuItem = document.createXULElement("menuitem");
|
|
toMenuItem.setAttribute("value", langTag);
|
|
toMenuItem.setAttribute("label", displayName);
|
|
popup.appendChild(toMenuItem);
|
|
}
|
|
}
|
|
|
|
this.#langListsPhase = "initialized";
|
|
} catch (error) {
|
|
this.console?.error(error);
|
|
this.#langListsPhase = "error";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reactively sets the views based on the async state changes of the engine, and
|
|
* other component state changes.
|
|
*
|
|
* @param {TranslationsLanguageState} languageState
|
|
*/
|
|
#updateViewFromTranslationStatus(
|
|
languageState = this.#getTranslationsActor().languageState
|
|
) {
|
|
const { defaultTranslate, toMenuList, fromMenuList, header, cancelButton } =
|
|
this.elements;
|
|
const { requestedTranslationPair, isEngineReady } = languageState;
|
|
|
|
if (
|
|
requestedTranslationPair &&
|
|
!isEngineReady &&
|
|
toMenuList.value === requestedTranslationPair.toLanguage &&
|
|
fromMenuList.value === requestedTranslationPair.fromLanguage
|
|
) {
|
|
// A translation has been requested, but is not ready yet.
|
|
document.l10n.setAttributes(
|
|
defaultTranslate,
|
|
"translations-panel-translate-button-loading"
|
|
);
|
|
defaultTranslate.disabled = true;
|
|
cancelButton.hidden = false;
|
|
this.updateUIForReTranslation(false /* isReTranslation */);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
defaultTranslate,
|
|
"translations-panel-translate-button"
|
|
);
|
|
defaultTranslate.disabled =
|
|
// The translation languages are the same, don't allow this translation.
|
|
toMenuList.value === fromMenuList.value ||
|
|
// No "to" language was provided.
|
|
!toMenuList.value ||
|
|
// No "from" language was provided.
|
|
!fromMenuList.value ||
|
|
// This is the requested translation pair.
|
|
(requestedTranslationPair &&
|
|
requestedTranslationPair.fromLanguage === fromMenuList.value &&
|
|
requestedTranslationPair.toLanguage === toMenuList.value);
|
|
}
|
|
|
|
if (requestedTranslationPair && isEngineReady) {
|
|
const { fromLanguage, toLanguage } = requestedTranslationPair;
|
|
const displayNames = new Services.intl.DisplayNames(undefined, {
|
|
type: "language",
|
|
});
|
|
cancelButton.hidden = true;
|
|
this.updateUIForReTranslation(true /* isReTranslation */);
|
|
|
|
document.l10n.setAttributes(header, "translations-panel-revisit-header", {
|
|
fromLanguage: displayNames.of(fromLanguage),
|
|
toLanguage: displayNames.of(toLanguage),
|
|
});
|
|
} else {
|
|
document.l10n.setAttributes(header, "translations-panel-header");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} isReTranslation
|
|
*/
|
|
updateUIForReTranslation(isReTranslation) {
|
|
const { restoreButton, fromLabel, fromMenuList, toLabel } = this.elements;
|
|
restoreButton.hidden = !isReTranslation;
|
|
// When offering to re-translate a page, hide the "from" language so users don't
|
|
// get confused.
|
|
fromLabel.hidden = isReTranslation;
|
|
fromMenuList.hidden = isReTranslation;
|
|
if (isReTranslation) {
|
|
fromLabel.style.marginBlockStart = "";
|
|
toLabel.style.marginBlockStart = 0;
|
|
} else {
|
|
fromLabel.style.marginBlockStart = 0;
|
|
toLabel.style.marginBlockStart = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the panel is currently showing the default view, otherwise false.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
#isShowingDefaultView() {
|
|
const { multiview } = this.elements;
|
|
return (
|
|
multiview.getAttribute("mainViewId") === "translations-panel-view-default"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Show the default view of choosing a source and target language.
|
|
*
|
|
* @param {boolean} force - Force the page to show translation options.
|
|
*/
|
|
async #showDefaultView(force = false) {
|
|
const {
|
|
fromMenuList,
|
|
multiview,
|
|
panel,
|
|
error,
|
|
toMenuList,
|
|
defaultTranslate,
|
|
langSelection,
|
|
intro,
|
|
header,
|
|
} = this.elements;
|
|
|
|
this.#updateViewFromTranslationStatus();
|
|
|
|
// Unconditionally hide the intro text in case the panel is re-shown.
|
|
intro.hidden = true;
|
|
|
|
if (this.#langListsPhase === "error") {
|
|
// There was an error, display it in the view rather than the language
|
|
// dropdowns.
|
|
const { cancelButton, errorHintAction } = this.elements;
|
|
|
|
this.#showError({
|
|
message: "translations-panel-error-load-languages",
|
|
hint: "translations-panel-error-load-languages-hint",
|
|
actionText: "translations-panel-error-load-languages-hint-button",
|
|
actionCommand: () => this.#reloadLangList(),
|
|
});
|
|
|
|
defaultTranslate.disabled = true;
|
|
this.updateUIForReTranslation(false /* isReTranslation */);
|
|
cancelButton.hidden = false;
|
|
langSelection.hidden = true;
|
|
errorHintAction.disabled = false;
|
|
return;
|
|
}
|
|
|
|
// Remove any old selected values synchronously before asking for new ones.
|
|
fromMenuList.value = "";
|
|
error.hidden = true;
|
|
langSelection.hidden = false;
|
|
|
|
/** @type {null | LangTags} */
|
|
const langTags = await this.#fetchDetectedLanguages();
|
|
if (langTags?.isDocLangTagSupported || force) {
|
|
// Show the default view with the language selection
|
|
const { cancelButton } = this.elements;
|
|
|
|
if (langTags?.isDocLangTagSupported) {
|
|
fromMenuList.value = langTags?.docLangTag ?? "";
|
|
} else {
|
|
fromMenuList.value = "";
|
|
}
|
|
toMenuList.value = langTags?.userLangTag ?? "";
|
|
|
|
this.onChangeLanguages();
|
|
|
|
this.updateUIForReTranslation(false /* isReTranslation */);
|
|
cancelButton.hidden = false;
|
|
multiview.setAttribute("mainViewId", "translations-panel-view-default");
|
|
|
|
if (!this._hasShownPanel) {
|
|
this.#firstShowUriSpec = gBrowser.currentURI.spec;
|
|
}
|
|
|
|
if (
|
|
this._hasShownPanel &&
|
|
gBrowser.currentURI.spec !== this.#firstShowUriSpec
|
|
) {
|
|
document.l10n.setAttributes(header, "translations-panel-header");
|
|
this.#firstShowUriSpec === null;
|
|
} else {
|
|
Services.prefs.setBoolPref("browser.translations.panelShown", true);
|
|
intro.hidden = false;
|
|
document.l10n.setAttributes(header, "translations-panel-intro-header");
|
|
}
|
|
} else {
|
|
// Show the "unsupported language" view.
|
|
const { unsupportedHint } = this.elements;
|
|
multiview.setAttribute(
|
|
"mainViewId",
|
|
"translations-panel-view-unsupported-language"
|
|
);
|
|
let language;
|
|
if (langTags?.docLangTag) {
|
|
const displayNames = new Intl.DisplayNames(undefined, {
|
|
type: "language",
|
|
fallback: "none",
|
|
});
|
|
language = displayNames.of(langTags.docLangTag);
|
|
}
|
|
if (language) {
|
|
document.l10n.setAttributes(
|
|
unsupportedHint,
|
|
"translations-panel-error-unsupported-hint-known",
|
|
{ language }
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
unsupportedHint,
|
|
"translations-panel-error-unsupported-hint-unknown"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Focus the "from" language, as it is the only field not set.
|
|
panel.addEventListener(
|
|
"ViewShown",
|
|
() => {
|
|
if (!fromMenuList.value) {
|
|
fromMenuList.focus();
|
|
}
|
|
if (!toMenuList.value) {
|
|
toMenuList.focus();
|
|
}
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates the checked states of the settings menu checkboxes that
|
|
* pertain to languages.
|
|
*/
|
|
async #updateSettingsMenuLanguageCheckboxStates() {
|
|
const { docLangTag, isDocLangTagSupported } =
|
|
await this.#getCachedDetectedLanguages();
|
|
|
|
const { panel } = this.elements;
|
|
const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
|
|
".always-translate-language-menuitem"
|
|
);
|
|
const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
|
|
".never-translate-language-menuitem"
|
|
);
|
|
|
|
const alwaysTranslateLanguage =
|
|
TranslationsParent.shouldAlwaysTranslateLanguage(docLangTag);
|
|
const neverTranslateLanguage =
|
|
TranslationsParent.shouldNeverTranslateLanguage(docLangTag);
|
|
const shouldDisable =
|
|
!docLangTag ||
|
|
!isDocLangTagSupported ||
|
|
docLangTag === new Intl.Locale(Services.locale.appLocaleAsBCP47).language;
|
|
|
|
for (const menuitem of alwaysTranslateMenuItems) {
|
|
menuitem.setAttribute(
|
|
"checked",
|
|
alwaysTranslateLanguage ? "true" : "false"
|
|
);
|
|
menuitem.disabled = shouldDisable;
|
|
}
|
|
for (const menuitem of neverTranslateMenuItems) {
|
|
menuitem.setAttribute(
|
|
"checked",
|
|
neverTranslateLanguage ? "true" : "false"
|
|
);
|
|
menuitem.disabled = shouldDisable;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the checked states of the settings menu checkboxes that
|
|
* pertain to site permissions.
|
|
*/
|
|
async #updateSettingsMenuSiteCheckboxStates() {
|
|
const { panel } = this.elements;
|
|
const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll(
|
|
".never-translate-site-menuitem"
|
|
);
|
|
const neverTranslateSite =
|
|
await this.#getTranslationsActor().shouldNeverTranslateSite();
|
|
|
|
for (const menuitem of neverTranslateSiteMenuItems) {
|
|
menuitem.setAttribute("checked", neverTranslateSite ? "true" : "false");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populates the language-related settings menuitems by adding the
|
|
* localized display name of the document's detected language tag.
|
|
*/
|
|
async #populateSettingsMenuItems() {
|
|
const { docLangTag } = await this.#getCachedDetectedLanguages();
|
|
|
|
const { panel } = this.elements;
|
|
|
|
const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
|
|
".always-translate-language-menuitem"
|
|
);
|
|
const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
|
|
".never-translate-language-menuitem"
|
|
);
|
|
|
|
/** @type {string | undefined} */
|
|
let docLangDisplayName;
|
|
if (docLangTag) {
|
|
const displayNames = new Services.intl.DisplayNames(undefined, {
|
|
type: "language",
|
|
fallback: "none",
|
|
});
|
|
// The display name will still be empty if the docLangTag is not known.
|
|
docLangDisplayName = displayNames.of(docLangTag);
|
|
}
|
|
|
|
for (const menuitem of alwaysTranslateMenuItems) {
|
|
if (docLangDisplayName) {
|
|
document.l10n.setAttributes(
|
|
menuitem,
|
|
"translations-panel-settings-always-translate-language",
|
|
{ language: docLangDisplayName }
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
menuitem,
|
|
"translations-panel-settings-always-translate-unknown-language"
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const menuitem of neverTranslateMenuItems) {
|
|
if (docLangDisplayName) {
|
|
document.l10n.setAttributes(
|
|
menuitem,
|
|
"translations-panel-settings-never-translate-language",
|
|
{ language: docLangDisplayName }
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
menuitem,
|
|
"translations-panel-settings-never-translate-unknown-language"
|
|
);
|
|
}
|
|
}
|
|
|
|
await Promise.all([
|
|
this.#updateSettingsMenuLanguageCheckboxStates(),
|
|
this.#updateSettingsMenuSiteCheckboxStates(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Configures the panel for the user to reset the page after it has been translated.
|
|
*
|
|
* @param {TranslationPair} translationPair
|
|
*/
|
|
async #showRevisitView({ fromLanguage, toLanguage }) {
|
|
const { fromMenuList, toMenuList, intro } = this.elements;
|
|
if (!this.#isShowingDefaultView()) {
|
|
await this.#showDefaultView();
|
|
}
|
|
intro.hidden = true;
|
|
fromMenuList.value = fromLanguage;
|
|
toMenuList.value = toLanguage;
|
|
this.onChangeLanguages();
|
|
}
|
|
|
|
/**
|
|
* Handle the disable logic for when the menulist is changed for the "Translate to"
|
|
* on the "revisit" subview.
|
|
*/
|
|
onChangeRevisitTo() {
|
|
const { revisitTranslate, revisitMenuList } = this.elements;
|
|
revisitTranslate.disabled = !revisitMenuList.value;
|
|
}
|
|
|
|
/**
|
|
* Handle logic and telemetry for changing the selected from-language option.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
onChangeFromLanguage(event) {
|
|
const { target } = event;
|
|
if (target?.value) {
|
|
TranslationsParent.telemetry().panel().onChangeFromLanguage(target.value);
|
|
}
|
|
this.onChangeLanguages();
|
|
}
|
|
|
|
/**
|
|
* Handle logic and telemetry for changing the selected to-language option.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
onChangeToLanguage(event) {
|
|
const { target } = event;
|
|
if (target?.value) {
|
|
TranslationsParent.telemetry().panel().onChangeToLanguage(target.value);
|
|
}
|
|
this.onChangeLanguages();
|
|
}
|
|
|
|
/**
|
|
* When changing the language selection, the translate button will need updating.
|
|
*/
|
|
onChangeLanguages() {
|
|
this.#updateViewFromTranslationStatus();
|
|
}
|
|
|
|
/**
|
|
* Hide the pop up (for event handlers).
|
|
*/
|
|
close() {
|
|
PanelMultiView.hidePopup(this.elements.panel);
|
|
}
|
|
|
|
/*
|
|
* Handler for clicking the learn more link from linked text
|
|
* within the translations panel.
|
|
*/
|
|
onLearnMoreLink() {
|
|
TranslationsParent.telemetry().panel().onLearnMoreLink();
|
|
TranslationsPanel.close();
|
|
}
|
|
|
|
/*
|
|
* Handler for clicking the learn more link from the gear menu.
|
|
*/
|
|
onAboutTranslations() {
|
|
TranslationsParent.telemetry().panel().onAboutTranslations();
|
|
PanelMultiView.hidePopup(this.elements.panel);
|
|
const window =
|
|
gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
|
|
window.openTrustedLinkIn(
|
|
"https://support.mozilla.org/kb/website-translation",
|
|
"tab",
|
|
{
|
|
forceForeground: true,
|
|
triggeringPrincipal:
|
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* When a language is not supported and the menu is manually invoked, an error message
|
|
* is shown. This method switches the panel back to the language selection view.
|
|
* Note that this bypasses the showSubView method since the main view doesn't support
|
|
* a subview.
|
|
*/
|
|
async onChangeSourceLanguage(event) {
|
|
const { panel } = this.elements;
|
|
PanelMultiView.hidePopup(panel);
|
|
|
|
await this.#showDefaultView(true /* force this view to be shown */);
|
|
|
|
await this.#openPanelPopup(this.elements.appMenuButton, {
|
|
event,
|
|
viewName: "defaultView",
|
|
maintainFlow: true,
|
|
});
|
|
}
|
|
|
|
async #reloadLangList() {
|
|
try {
|
|
await this.#ensureLangListsBuilt();
|
|
await this.#showDefaultView();
|
|
} catch (error) {
|
|
this.elements.errorHintAction.disabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle telemetry events when buttons are invoked in the panel.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
handlePanelButtonEvent(event) {
|
|
const {
|
|
cancelButton,
|
|
changeSourceLanguageButton,
|
|
dismissErrorButton,
|
|
restoreButton,
|
|
translateButton,
|
|
} = this.elements;
|
|
switch (event.target.id) {
|
|
case cancelButton.id: {
|
|
TranslationsParent.telemetry().panel().onCancelButton();
|
|
break;
|
|
}
|
|
case changeSourceLanguageButton.id: {
|
|
TranslationsParent.telemetry().panel().onChangeSourceLanguageButton();
|
|
break;
|
|
}
|
|
case dismissErrorButton.id: {
|
|
TranslationsParent.telemetry().panel().onDismissErrorButton();
|
|
break;
|
|
}
|
|
case restoreButton.id: {
|
|
TranslationsParent.telemetry().panel().onRestorePageButton();
|
|
break;
|
|
}
|
|
case translateButton.id: {
|
|
TranslationsParent.telemetry().panel().onTranslateButton();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle telemetry events when popups are shown in the panel.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
handlePanelPopupShownEvent(event) {
|
|
const { panel, fromMenuList, toMenuList } = this.elements;
|
|
switch (event.target.id) {
|
|
case panel.id: {
|
|
// This telemetry event is invoked externally because it requires
|
|
// extra logic about from where the panel was opened and whether
|
|
// or not the flow should be maintained or started anew.
|
|
break;
|
|
}
|
|
case fromMenuList.firstChild.id: {
|
|
TranslationsParent.telemetry().panel().onOpenFromLanguageMenu();
|
|
break;
|
|
}
|
|
case toMenuList.firstChild.id: {
|
|
TranslationsParent.telemetry().panel().onOpenToLanguageMenu();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle telemetry events when popups are hidden in the panel.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
handlePanelPopupHiddenEvent(event) {
|
|
const { panel, fromMenuList, toMenuList } = this.elements;
|
|
switch (event.target.id) {
|
|
case panel.id: {
|
|
TranslationsParent.telemetry().panel().onClose();
|
|
this.#isPopupOpen = false;
|
|
this.elements.error.hidden = true;
|
|
break;
|
|
}
|
|
case fromMenuList.firstChild.id: {
|
|
TranslationsParent.telemetry().panel().onCloseFromLanguageMenu();
|
|
break;
|
|
}
|
|
case toMenuList.firstChild.id: {
|
|
TranslationsParent.telemetry().panel().onCloseToLanguageMenu();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle telemetry events when the settings menu is shown.
|
|
*/
|
|
handleSettingsPopupShownEvent() {
|
|
TranslationsParent.telemetry().panel().onOpenSettingsMenu();
|
|
}
|
|
|
|
/**
|
|
* Handle telemetry events when the settings menu is hidden.
|
|
*/
|
|
handleSettingsPopupHiddenEvent() {
|
|
TranslationsParent.telemetry().panel().onCloseSettingsMenu();
|
|
}
|
|
|
|
/**
|
|
* Opens the Translations panel popup at the given target.
|
|
*
|
|
* @param {object} target - The target element at which to open the popup.
|
|
* @param {object} telemetryData
|
|
* @param {string} telemetryData.event
|
|
* The trigger event for opening the popup.
|
|
* @param {string} telemetryData.viewName
|
|
* The name of the view shown by the panel.
|
|
* @param {boolean} telemetryData.autoShow
|
|
* True if the panel was automatically opened, otherwise false.
|
|
* @param {boolean} telemetryData.maintainFlow
|
|
* Whether or not to maintain the flow of telemetry.
|
|
* @param {boolean} telemetryData.isFirstUserInteraction
|
|
* Whether or not this is the first user interaction with the panel.
|
|
*/
|
|
async #openPanelPopup(
|
|
target,
|
|
{
|
|
event = null,
|
|
viewName = null,
|
|
autoShow = false,
|
|
maintainFlow = false,
|
|
isFirstUserInteraction = null,
|
|
}
|
|
) {
|
|
const { panel, appMenuButton } = this.elements;
|
|
const openedFromAppMenu = target.id === appMenuButton.id;
|
|
const { docLangTag } = await this.#getCachedDetectedLanguages();
|
|
|
|
TranslationsParent.telemetry().panel().onOpen({
|
|
viewName,
|
|
autoShow,
|
|
docLangTag,
|
|
maintainFlow,
|
|
openedFromAppMenu,
|
|
isFirstUserInteraction,
|
|
});
|
|
|
|
this.#isPopupOpen = true;
|
|
|
|
PanelMultiView.openPopup(panel, target, {
|
|
position: "bottomright topright",
|
|
triggerEvent: event,
|
|
}).catch(error => this.console?.error(error));
|
|
}
|
|
|
|
/**
|
|
* Keeps track of open requests to guard against race conditions.
|
|
*
|
|
* @type {Promise<void> | null}
|
|
*/
|
|
#openPromise = null;
|
|
|
|
/**
|
|
* Opens the TranslationsPanel.
|
|
*
|
|
* @param {Event} event
|
|
* @param {boolean} reportAsAutoShow
|
|
* True to report to telemetry that the panel was opened automatically, otherwise false.
|
|
*/
|
|
async open(event, reportAsAutoShow = false) {
|
|
if (this.#openPromise) {
|
|
// There is already an open event happening, do not open.
|
|
return;
|
|
}
|
|
|
|
this.#openPromise = this.#openImpl(event, reportAsAutoShow);
|
|
this.#openPromise.finally(() => {
|
|
this.#openPromise = null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Implementation function for opening the panel. Prefer TranslationsPanel.open.
|
|
*
|
|
* @param {Event} event
|
|
*/
|
|
async #openImpl(event, reportAsAutoShow) {
|
|
event.stopPropagation();
|
|
if (
|
|
(event.type == "click" && event.button != 0) ||
|
|
(event.type == "keypress" &&
|
|
event.charCode != KeyEvent.DOM_VK_SPACE &&
|
|
event.keyCode != KeyEvent.DOM_VK_RETURN)
|
|
) {
|
|
// Allow only left click, space, or enter.
|
|
return;
|
|
}
|
|
|
|
const window =
|
|
gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
|
|
window.ensureCustomElements("moz-support-link");
|
|
|
|
const { button } = this.buttonElements;
|
|
|
|
const { requestedTranslationPair, locationChangeId } =
|
|
this.#getTranslationsActor().languageState;
|
|
|
|
// Store this value because it gets modified when #showDefaultView is called below.
|
|
const isFirstUserInteraction = !this._hasShownPanel;
|
|
|
|
await this.#ensureLangListsBuilt();
|
|
|
|
if (requestedTranslationPair) {
|
|
await this.#showRevisitView(requestedTranslationPair).catch(error => {
|
|
this.console?.error(error);
|
|
});
|
|
} else {
|
|
await this.#showDefaultView().catch(error => {
|
|
this.console?.error(error);
|
|
});
|
|
}
|
|
|
|
this.#populateSettingsMenuItems();
|
|
|
|
const targetButton =
|
|
button.contains(event.target) ||
|
|
event.type === "TranslationsParent:OfferTranslation"
|
|
? button
|
|
: this.elements.appMenuButton;
|
|
|
|
if (!TranslationsParent.isActiveLocation(locationChangeId)) {
|
|
this.console?.log(`A translation panel open request was stale.`, {
|
|
locationChangeId,
|
|
newlocationChangeId:
|
|
this.#getTranslationsActor().languageState.locationChangeId,
|
|
currentURISpec: gBrowser.currentURI.spec,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec);
|
|
|
|
await this.#openPanelPopup(targetButton, {
|
|
event,
|
|
autoShow: reportAsAutoShow,
|
|
viewName: requestedTranslationPair ? "revisitView" : "defaultView",
|
|
maintainFlow: false,
|
|
isFirstUserInteraction,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes the translations button.
|
|
*/
|
|
#hideTranslationsButton() {
|
|
const { button, buttonLocale, buttonCircleArrows } = this.buttonElements;
|
|
button.hidden = true;
|
|
buttonLocale.hidden = true;
|
|
buttonCircleArrows.hidden = true;
|
|
button.removeAttribute("translationsactive");
|
|
}
|
|
|
|
/**
|
|
* Returns true if translations is currently active, otherwise false.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
#isTranslationsActive() {
|
|
const { requestedTranslationPair } =
|
|
this.#getTranslationsActor().languageState;
|
|
return requestedTranslationPair !== null;
|
|
}
|
|
|
|
/**
|
|
* Handle the translation button being clicked when there are two language options.
|
|
*/
|
|
async onTranslate() {
|
|
PanelMultiView.hidePopup(this.elements.panel);
|
|
|
|
const actor = this.#getTranslationsActor();
|
|
actor.translate(
|
|
this.elements.fromMenuList.value,
|
|
this.elements.toMenuList.value,
|
|
false // reportAsAutoTranslate
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle the cancel button being clicked.
|
|
*/
|
|
onCancel() {
|
|
PanelMultiView.hidePopup(this.elements.panel);
|
|
}
|
|
|
|
/**
|
|
* A handler for opening the settings context menu.
|
|
*/
|
|
openSettingsPopup(button) {
|
|
this.#updateSettingsMenuLanguageCheckboxStates();
|
|
this.#updateSettingsMenuSiteCheckboxStates();
|
|
const popup = button.ownerDocument.getElementById(
|
|
"translations-panel-settings-menupopup"
|
|
);
|
|
popup.openPopup(button, "after_end");
|
|
}
|
|
|
|
/**
|
|
* Creates a new CheckboxPageAction based on the current translated
|
|
* state of the page and the state of the persistent options in the
|
|
* translations panel settings.
|
|
*
|
|
* @returns {CheckboxPageAction}
|
|
*/
|
|
getCheckboxPageActionFor() {
|
|
const {
|
|
alwaysTranslateLanguageMenuItem,
|
|
neverTranslateLanguageMenuItem,
|
|
neverTranslateSiteMenuItem,
|
|
} = this.elements;
|
|
|
|
const alwaysTranslateLanguage =
|
|
alwaysTranslateLanguageMenuItem.getAttribute("checked") === "true";
|
|
const neverTranslateLanguage =
|
|
neverTranslateLanguageMenuItem.getAttribute("checked") === "true";
|
|
const neverTranslateSite =
|
|
neverTranslateSiteMenuItem.getAttribute("checked") === "true";
|
|
|
|
return new CheckboxPageAction(
|
|
this.#isTranslationsActive(),
|
|
alwaysTranslateLanguage,
|
|
neverTranslateLanguage,
|
|
neverTranslateSite
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Redirect the user to about:preferences
|
|
*/
|
|
openManageLanguages() {
|
|
TranslationsParent.telemetry().panel().onManageLanguages();
|
|
const window =
|
|
gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
|
|
window.openTrustedLinkIn("about:preferences#general-translations", "tab");
|
|
}
|
|
|
|
/**
|
|
* Performs the given page action.
|
|
*
|
|
* @param {PageAction} pageAction
|
|
*/
|
|
async #doPageAction(pageAction) {
|
|
switch (pageAction) {
|
|
case PageAction.NO_CHANGE: {
|
|
break;
|
|
}
|
|
case PageAction.RESTORE_PAGE: {
|
|
await this.onRestore();
|
|
break;
|
|
}
|
|
case PageAction.TRANSLATE_PAGE: {
|
|
await this.onTranslate();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the always-translate-language menuitem prefs and checked state.
|
|
* If auto-translate is currently active for the doc language, deactivates it.
|
|
* If auto-translate is currently inactive for the doc language, activates it.
|
|
*/
|
|
async onAlwaysTranslateLanguage() {
|
|
const { docLangTag } = await this.#getCachedDetectedLanguages();
|
|
if (!docLangTag) {
|
|
throw new Error("Expected to have a document language tag.");
|
|
}
|
|
const pageAction =
|
|
this.getCheckboxPageActionFor().alwaysTranslateLanguage();
|
|
const toggledOn =
|
|
TranslationsParent.toggleAlwaysTranslateLanguagePref(docLangTag);
|
|
TranslationsParent.telemetry()
|
|
.panel()
|
|
.onAlwaysTranslateLanguage(docLangTag, toggledOn);
|
|
this.#updateSettingsMenuLanguageCheckboxStates();
|
|
await this.#doPageAction(pageAction);
|
|
}
|
|
|
|
/**
|
|
* Updates the never-translate-language menuitem prefs and checked state.
|
|
* If never-translate is currently active for the doc language, deactivates it.
|
|
* If never-translate is currently inactive for the doc language, activates it.
|
|
*/
|
|
async onNeverTranslateLanguage() {
|
|
const { docLangTag } = await this.#getCachedDetectedLanguages();
|
|
if (!docLangTag) {
|
|
throw new Error("Expected to have a document language tag.");
|
|
}
|
|
const pageAction = this.getCheckboxPageActionFor().neverTranslateLanguage();
|
|
const toggledOn =
|
|
TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag);
|
|
TranslationsParent.telemetry()
|
|
.panel()
|
|
.onNeverTranslateLanguage(docLangTag, toggledOn);
|
|
this.#updateSettingsMenuLanguageCheckboxStates();
|
|
await this.#doPageAction(pageAction);
|
|
}
|
|
|
|
/**
|
|
* Updates the never-translate-site menuitem permissions and checked state.
|
|
* If never-translate is currently active for the site, deactivates it.
|
|
* If never-translate is currently inactive for the site, activates it.
|
|
*/
|
|
async onNeverTranslateSite() {
|
|
const pageAction = this.getCheckboxPageActionFor().neverTranslateSite();
|
|
const toggledOn =
|
|
await this.#getTranslationsActor().toggleNeverTranslateSitePermissions();
|
|
TranslationsParent.telemetry().panel().onNeverTranslateSite(toggledOn);
|
|
this.#updateSettingsMenuSiteCheckboxStates();
|
|
await this.#doPageAction(pageAction);
|
|
}
|
|
|
|
/**
|
|
* Handle the restore button being clicked.
|
|
*/
|
|
async onRestore() {
|
|
const { panel } = this.elements;
|
|
PanelMultiView.hidePopup(panel);
|
|
const { docLangTag } = await this.#getCachedDetectedLanguages();
|
|
if (!docLangTag) {
|
|
throw new Error("Expected to have a document language tag.");
|
|
}
|
|
|
|
this.#getTranslationsActor().restorePage(docLangTag);
|
|
}
|
|
|
|
handleEventId = 0;
|
|
|
|
/**
|
|
* Set the state of the translations button in the URL bar.
|
|
*
|
|
* @param {CustomEvent} event
|
|
*/
|
|
handleEvent = async event => {
|
|
switch (event.type) {
|
|
case "TranslationsParent:OfferTranslation": {
|
|
if (Services.wm.getMostRecentBrowserWindow()?.gBrowser === gBrowser) {
|
|
this.open(event, /* reportAsAutoShow */ true);
|
|
}
|
|
break;
|
|
}
|
|
case "TranslationsParent:LanguageState":
|
|
// Check this value after every `await` to guard against race conditions.
|
|
const handleEventId = ++this.handleEventId;
|
|
const {
|
|
detectedLanguages,
|
|
requestedTranslationPair,
|
|
error,
|
|
isEngineReady,
|
|
} = event.detail;
|
|
|
|
const { button, buttonLocale, buttonCircleArrows } =
|
|
this.buttonElements;
|
|
|
|
const hasSupportedLanguage =
|
|
detectedLanguages?.docLangTag &&
|
|
detectedLanguages?.userLangTag &&
|
|
detectedLanguages?.isDocLangTagSupported;
|
|
|
|
if (detectedLanguages) {
|
|
// Ensure the cached detected languages are up to date, for instance whenever
|
|
// the user switches tabs.
|
|
TranslationsPanel.detectedLanguages = detectedLanguages;
|
|
}
|
|
|
|
if (this.#isPopupOpen) {
|
|
// Make sure to use the language state that is passed by the event.detail, and
|
|
// don't read it from the actor here, as it's possible the actor isn't available
|
|
// via the gBrowser.selectedBrowser.
|
|
this.#updateViewFromTranslationStatus(event.detail);
|
|
}
|
|
|
|
if (
|
|
// We've already requested to translate this page, so always show the icon.
|
|
requestedTranslationPair ||
|
|
// There was an error translating, so always show the icon. This can happen
|
|
// when a user manually invokes the translation and we wouldn't normally show
|
|
// the icon.
|
|
error ||
|
|
// Finally check that we can translate this language.
|
|
(hasSupportedLanguage &&
|
|
(await TranslationsParent.getIsTranslationsEngineSupported()))
|
|
) {
|
|
if (handleEventId !== this.handleEventId) {
|
|
// A new handleEvent was received, this one is stale.
|
|
return;
|
|
}
|
|
button.hidden = false;
|
|
if (requestedTranslationPair) {
|
|
// The translation is active, update the urlbar button.
|
|
button.setAttribute("translationsactive", true);
|
|
if (isEngineReady) {
|
|
const displayNames = new Services.intl.DisplayNames(undefined, {
|
|
type: "language",
|
|
});
|
|
|
|
document.l10n.setAttributes(
|
|
button,
|
|
"urlbar-translations-button-translated",
|
|
{
|
|
fromLanguage: displayNames.of(
|
|
requestedTranslationPair.fromLanguage
|
|
),
|
|
toLanguage: displayNames.of(
|
|
requestedTranslationPair.toLanguage
|
|
),
|
|
}
|
|
);
|
|
// Show the locale of the page in the button.
|
|
buttonLocale.hidden = false;
|
|
buttonCircleArrows.hidden = true;
|
|
buttonLocale.innerText = requestedTranslationPair.toLanguage;
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
button,
|
|
"urlbar-translations-button-loading"
|
|
);
|
|
// Show the spinning circle arrows to indicate that the engine is
|
|
// still loading.
|
|
buttonCircleArrows.hidden = false;
|
|
buttonLocale.hidden = true;
|
|
}
|
|
} else {
|
|
// The translation is not active, update the urlbar button.
|
|
button.removeAttribute("translationsactive");
|
|
buttonLocale.hidden = true;
|
|
buttonCircleArrows.hidden = true;
|
|
|
|
// Follow the same rules for displaying the first-run intro text for the
|
|
// button's accessible tooltip label.
|
|
if (
|
|
this._hasShownPanel &&
|
|
gBrowser.currentURI.spec !== this.#firstShowUriSpec
|
|
) {
|
|
document.l10n.setAttributes(
|
|
button,
|
|
"urlbar-translations-button2"
|
|
);
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
button,
|
|
"urlbar-translations-button-intro"
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if (handleEventId !== this.handleEventId) {
|
|
// A new handleEvent was received, this one is stale.
|
|
return;
|
|
}
|
|
this.#hideTranslationsButton();
|
|
}
|
|
|
|
switch (error) {
|
|
case null:
|
|
break;
|
|
case "engine-load-failure":
|
|
if (!this.#isShowingDefaultView()) {
|
|
await this.#showDefaultView().catch(e => {
|
|
this.console?.error(e);
|
|
});
|
|
}
|
|
this.elements.error.hidden = false;
|
|
this.#showError({
|
|
message: "translations-panel-error-translating",
|
|
});
|
|
const targetButton = button.hidden
|
|
? this.elements.appMenuButton
|
|
: button;
|
|
|
|
// Re-open the menu on an error.
|
|
await this.#openPanelPopup(targetButton, {
|
|
autoShow: true,
|
|
viewName: "errorView",
|
|
maintainFlow: true,
|
|
});
|
|
break;
|
|
default:
|
|
console.error("Unknown translation error", error);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
})();
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
TranslationsPanel,
|
|
"_hasShownPanel",
|
|
"browser.translations.panelShown",
|
|
false
|
|
);
|