summaryrefslogtreecommitdiffstats
path: root/browser/components/translations/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/translations/content')
-rw-r--r--browser/components/translations/content/TranslationsPanelShared.sys.mjs122
-rw-r--r--browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml (renamed from browser/components/translations/content/translationsPanel.inc.xhtml)85
-rw-r--r--browser/components/translations/content/fullPageTranslationsPanel.js (renamed from browser/components/translations/content/translationsPanel.js)194
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.inc.xhtml102
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.js220
5 files changed, 540 insertions, 183 deletions
diff --git a/browser/components/translations/content/TranslationsPanelShared.sys.mjs b/browser/components/translations/content/TranslationsPanelShared.sys.mjs
new file mode 100644
index 0000000000..570528df3f
--- /dev/null
+++ b/browser/components/translations/content/TranslationsPanelShared.sys.mjs
@@ -0,0 +1,122 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
+});
+
+/**
+ * A class containing static functionality that is shared by both
+ * the FullPageTranslationsPanel and SelectTranslationsPanel classes.
+ */
+export class TranslationsPanelShared {
+ static #langListsInitState = new Map();
+
+ /**
+ * Defines lazy getters for accessing elements in the document based on provided entries.
+ *
+ * @param {Document} document - The document object.
+ * @param {object} lazyElements - An object where lazy getters will be defined.
+ * @param {object} entries - An object of key/value pairs for which to define lazy getters.
+ */
+ static defineLazyElements(document, lazyElements, entries) {
+ for (const [name, discriminator] of Object.entries(entries)) {
+ let element;
+ Object.defineProperty(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;
+ },
+ });
+ }
+ }
+
+ /**
+ * Retrieves the initialization state of language lists for the specified panel.
+ *
+ * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
+ * - The panel for which to look up the state.
+ */
+ static getLangListsInitState(panel) {
+ return TranslationsPanelShared.#langListsInitState.get(panel.id);
+ }
+
+ /**
+ * 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.
+ *
+ * @param {Document} document - The document object.
+ * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
+ * - The panel for which to ensure language lists are built.
+ */
+ static async ensureLangListsBuilt(document, panel, innerWindowId) {
+ const { id } = panel;
+ switch (
+ TranslationsPanelShared.#langListsInitState.get(`${id}-${innerWindowId}`)
+ ) {
+ case "initialized":
+ // This has already been initialized.
+ return;
+ case "error":
+ case undefined:
+ // attempt to initialize
+ break;
+ default:
+ throw new Error(
+ `Unknown langList phase ${
+ TranslationsPanelShared.#langListsInitState
+ }`
+ );
+ }
+ /** @type {SupportedLanguages} */
+ const { languagePairs, fromLanguages, toLanguages } =
+ await lazy.TranslationsParent.getSupportedLanguages();
+
+ // Verify that we are in a proper state.
+ if (languagePairs.length === 0) {
+ throw new Error("No translation languages were retrieved.");
+ }
+
+ 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);
+ }
+ }
+
+ TranslationsPanelShared.#langListsInitState.set(id, "initialized");
+ }
+}
diff --git a/browser/components/translations/content/translationsPanel.inc.xhtml b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml
index 18769eec83..bc0c5b319f 100644
--- a/browser/components/translations/content/translationsPanel.inc.xhtml
+++ b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml
@@ -3,68 +3,69 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html:template id="template-translations-panel">
-<panel id="translations-panel"
+<panel id="full-page-translations-panel"
class="panel-no-padding translations-panel"
type="arrow"
role="alertdialog"
noautofocus="true"
- aria-labelledby="translations-panel-header"
+ aria-labelledby="full-page-translations-panel-header"
orient="vertical"
- onclick="TranslationsPanel.handlePanelButtonEvent(event)"
- onpopupshown="TranslationsPanel.handlePanelPopupShownEvent(event)"
- onpopuphidden="TranslationsPanel.handlePanelPopupHiddenEvent(event)">
- <panelmultiview id="translations-panel-multiview"
- mainViewId="translations-panel-view-default">
- <panelview id="translations-panel-view-default"
+ onclick="FullPageTranslationsPanel.handlePanelButtonEvent(event)"
+ onpopupshown="FullPageTranslationsPanel.handlePanelPopupShownEvent(event)"
+ onpopuphidden="FullPageTranslationsPanel.handlePanelPopupHiddenEvent(event)">
+ <panelmultiview id="full-page-translations-panel-multiview"
+ mainViewId="full-page-translations-panel-view-default">
+ <panelview id="full-page-translations-panel-view-default"
class="PanelUI-subView translations-panel-view"
role="document"
mainview-with-header="true"
has-custom-header="true">
<hbox class="panel-header translations-panel-header">
<html:h1 class="translations-panel-header-wrapper">
- <html:span id="translations-panel-header"></html:span>
+ <html:span id="full-page-translations-panel-header"></html:span>
</html:h1>
<hbox class="translations-panel-beta">
<image class="translations-panel-beta-icon"></image>
</hbox>
- <toolbarbutton id="translations-panel-settings" class="panel-info-button"
+ <toolbarbutton id="translations-panel-settings"
+ class="panel-info-button translations-panel-settings-gear-icon"
data-l10n-id="translations-panel-settings-button"
closemenu="none"
- oncommand="TranslationsPanel.openSettingsPopup(this)"/>
+ oncommand="FullPageTranslationsPanel.openSettingsPopup(this)"/>
</hbox>
<vbox class="translations-panel-content">
- <html:div id="translations-panel-intro">
+ <html:div id="full-page-translations-panel-intro">
<html:span data-l10n-id="translations-panel-intro-description"></html:span>
- <html:a id="translations-panel-intro-learn-more-link"
+ <html:a id="full-page-translations-panel-intro-learn-more-link"
is="moz-support-link"
data-l10n-id="translations-panel-learn-more-link"
support-page="website-translation"
- onclick="TranslationsPanel.onLearnMoreLink()" />
+ onclick="FullPageTranslationsPanel.onLearnMoreLink()" />
</html:div>
- <vbox id="translations-panel-lang-selection">
- <label data-l10n-id="translations-panel-from-label" id="translations-panel-from-label"></label>
- <menulist id="translations-panel-from"
+ <vbox id="full-page-translations-panel-lang-selection">
+ <label data-l10n-id="translations-panel-from-label" id="full-page-translations-panel-from-label"></label>
+ <menulist id="full-page-translations-panel-from"
flex="1"
value="detect"
size="large"
aria-labelledby="translations-panel-from-label"
- oncommand="TranslationsPanel.onChangeFromLanguage(event)">
- <menupopup id="translations-panel-from-menupopup"
+ oncommand="FullPageTranslationsPanel.onChangeFromLanguage(event)">
+ <menupopup id="full-page-translations-panel-from-menupopup"
class="translations-panel-language-menupopup-from">
<menuitem data-l10n-id="translations-panel-choose-language" value=""></menuitem>
<!-- The list of <menuitem> will be dynamically inserted. -->
</menupopup>
</menulist>
- <label data-l10n-id="translations-panel-to-label" id="translations-panel-to-label"></label>
- <menulist id="translations-panel-to"
+ <label data-l10n-id="translations-panel-to-label" id="full-page-translations-panel-to-label"></label>
+ <menulist id="full-page-translations-panel-to"
flex="1"
value="detect"
size="large"
aria-labelledby="translations-panel-to-label"
- oncommand="TranslationsPanel.onChangeToLanguage(event)">
- <menupopup id="translations-panel-to-menupopup"
+ oncommand="FullPageTranslationsPanel.onChangeToLanguage(event)">
+ <menupopup id="full-page-translations-panel-to-menupopup"
class="translations-panel-language-menupopup-to">
<menuitem data-l10n-id="translations-panel-choose-language" value=""></menuitem>
<!-- The list of <menuitem> will be dynamically inserted. -->
@@ -72,69 +73,69 @@
</menulist>
</vbox>
- <vbox id="translations-panel-error" hidden="true">
+ <vbox id="full-page-translations-panel-error" hidden="true">
<hbox class="translations-panel-error-header">
<image class="translations-panel-error-icon translations-panel-error-header-icon" />
- <description id="translations-panel-error-message"></description>
+ <description id="full-page-translations-panel-error-message"></description>
</hbox>
- <hbox id="translations-panel-error-message-hint"></hbox>
+ <hbox id="full-page-translations-panel-error-message-hint"></hbox>
<hbox pack="end">
- <button id="translations-panel-translate-hint-action" />
+ <button id="full-page-translations-panel-translate-hint-action" />
</hbox>
</vbox>
</vbox>
<html:moz-button-group class="panel-footer translations-panel-footer">
- <button id="translations-panel-restore-button"
+ <button id="full-page-translations-panel-restore-button"
class="footer-button"
- oncommand="TranslationsPanel.onRestore(event);"
+ oncommand="FullPageTranslationsPanel.onRestore(event);"
data-l10n-id="translations-panel-restore-button">
</button>
- <button id="translations-panel-cancel"
+ <button id="full-page-translations-panel-cancel"
class="footer-button"
- oncommand="TranslationsPanel.onCancel(event);"
+ oncommand="FullPageTranslationsPanel.onCancel(event);"
data-l10n-id="translations-panel-translate-cancel">
</button>
- <button id="translations-panel-translate"
+ <button id="full-page-translations-panel-translate"
class="footer-button"
- oncommand="TranslationsPanel.onTranslate(event);"
+ oncommand="FullPageTranslationsPanel.onTranslate(event);"
data-l10n-id="translations-panel-translate-button"
default="true">
</button>
</html:moz-button-group>
</panelview>
- <panelview id="translations-panel-view-unsupported-language"
+ <panelview id="full-page-translations-panel-view-unsupported-language"
class="PanelUI-subView translations-panel-view"
role="document"
has-custom-header="true">
<hbox class="panel-header translations-panel-header">
<image class="translations-panel-error-icon" />
- <html:h1 id="translations-panel-unsupported-language-header">
+ <html:h1 id="full-page-translations-panel-unsupported-language-header">
<html:span data-l10n-id="translations-panel-error-unsupported"></html:span>
</html:h1>
</hbox>
<vbox class="translations-panel-content">
<html:div>
- <html:span id="translations-panel-error-unsupported-hint"></html:span>
- <html:a id="translations-panel-unsupported-learn-more-link"
+ <html:span id="full-page-translations-panel-error-unsupported-hint"></html:span>
+ <html:a id="full-page-translations-panel-unsupported-learn-more-link"
is="moz-support-link"
data-l10n-id="translations-panel-learn-more-link"
support-page="website-translation"
- onclick="TranslationsPanel.onLearnMoreLink()" />
+ onclick="FullPageTranslationsPanel.onLearnMoreLink()" />
</html:div>
</vbox>
<html:moz-button-group class="panel-footer translations-panel-footer">
- <button id="translations-panel-change-source-language"
+ <button id="full-page-translations-panel-change-source-language"
class="footer-button"
- oncommand="TranslationsPanel.onChangeSourceLanguage(event);"
+ oncommand="FullPageTranslationsPanel.onChangeSourceLanguage(event);"
data-l10n-id="translations-panel-error-change-button">
</button>
- <button id="translations-panel-dismiss-error"
+ <button id="full-page-translations-panel-dismiss-error"
class="footer-button"
- oncommand="TranslationsPanel.onCancel(event);"
+ oncommand="FullPageTranslationsPanel.onCancel(event);"
data-l10n-id="translations-panel-error-dismiss-button"
default="true">
</button>
diff --git a/browser/components/translations/content/translationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js
index 71aaacac4f..2e35440160 100644
--- a/browser/components/translations/content/translationsPanel.js
+++ b/browser/components/translations/content/fullPageTranslationsPanel.js
@@ -14,6 +14,8 @@ ChromeUtils.defineESModuleGetters(this, {
PageActions: "resource:///modules/PageActions.sys.mjs",
TranslationsTelemetry:
"chrome://browser/content/translations/TranslationsTelemetry.sys.mjs",
+ TranslationsPanelShared:
+ "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs",
});
/**
@@ -193,7 +195,7 @@ class CheckboxPageAction {
* (the Translations actor). This class reacts to state changes coming from the
* Translations actor.
*/
-var TranslationsPanel = new (class {
+var FullPageTranslationsPanel = new (class {
/** @type {Console?} */
#console;
@@ -272,77 +274,38 @@ var TranslationsPanel = new (class {
// 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("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("introLearnMoreLink", "translations-panel-intro-learn-more-link");
- 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(
- "unsupportedHeader",
- "translations-panel-unsupported-language-header"
- );
- getter("unsupportedHint", "translations-panel-error-unsupported-hint");
- getter(
- "unsupportedLearnMoreLink",
- "translations-panel-unsupported-learn-more-link"
- );
-
- // 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");
+ TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, {
+ alwaysTranslateLanguageMenuItem: ".always-translate-language-menuitem",
+ appMenuButton: "PanelUI-menu-button",
+ cancelButton: "full-page-translations-panel-cancel",
+ changeSourceLanguageButton:
+ "full-page-translations-panel-change-source-language",
+ dismissErrorButton: "full-page-translations-panel-dismiss-error",
+ error: "full-page-translations-panel-error",
+ errorMessage: "full-page-translations-panel-error-message",
+ errorMessageHint: "full-page-translations-panel-error-message-hint",
+ errorHintAction: "full-page-translations-panel-translate-hint-action",
+ fromMenuList: "full-page-translations-panel-from",
+ fromLabel: "full-page-translations-panel-from-label",
+ header: "full-page-translations-panel-header",
+ intro: "full-page-translations-panel-intro",
+ introLearnMoreLink:
+ "full-page-translations-panel-intro-learn-more-link",
+ langSelection: "full-page-translations-panel-lang-selection",
+ manageLanguagesMenuItem: ".manage-languages-menuitem",
+ multiview: "full-page-translations-panel-multiview",
+ neverTranslateLanguageMenuItem: ".never-translate-language-menuitem",
+ neverTranslateSiteMenuItem: ".never-translate-site-menuitem",
+ restoreButton: "full-page-translations-panel-restore-button",
+ toLabel: "full-page-translations-panel-to-label",
+ toMenuList: "full-page-translations-panel-to",
+ translateButton: "full-page-translations-panel-translate",
+ unsupportedHeader:
+ "full-page-translations-panel-unsupported-language-header",
+ unsupportedHint: "full-page-translations-panel-error-unsupported-hint",
+ unsupportedLearnMoreLink:
+ "full-page-translations-panel-unsupported-learn-more-link",
+ });
}
return this.#lazyElements;
@@ -452,74 +415,19 @@ var TranslationsPanel = new (class {
}
/**
- * @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"
+ await TranslationsPanelShared.ensureLangListsBuilt(
+ document,
+ this.elements.panel,
+ gBrowser.selectedBrowser.innerWindowID
);
-
- 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";
}
}
@@ -616,7 +524,8 @@ var TranslationsPanel = new (class {
}
const { multiview } = this.elements;
return (
- multiview.getAttribute("mainViewId") === "translations-panel-view-default"
+ multiview.getAttribute("mainViewId") ===
+ "full-page-translations-panel-view-default"
);
}
@@ -644,7 +553,7 @@ var TranslationsPanel = new (class {
// Unconditionally hide the intro text in case the panel is re-shown.
intro.hidden = true;
- if (this.#langListsPhase === "error") {
+ if (TranslationsPanelShared.getLangListsInitState(panel) === "error") {
// There was an error, display it in the view rather than the language
// dropdowns.
const { cancelButton, errorHintAction } = this.elements;
@@ -686,7 +595,10 @@ var TranslationsPanel = new (class {
this.updateUIForReTranslation(false /* isReTranslation */);
cancelButton.hidden = false;
- multiview.setAttribute("mainViewId", "translations-panel-view-default");
+ multiview.setAttribute(
+ "mainViewId",
+ "full-page-translations-panel-view-default"
+ );
if (!this._hasShownPanel) {
actor.firstShowUriSpec = gBrowser.currentURI.spec;
@@ -709,7 +621,7 @@ var TranslationsPanel = new (class {
const { unsupportedHint } = this.elements;
multiview.setAttribute(
"mainViewId",
- "translations-panel-view-unsupported-language"
+ "full-page-translations-panel-view-unsupported-language"
);
let language;
if (langTags?.docLangTag) {
@@ -952,7 +864,7 @@ var TranslationsPanel = new (class {
*/
onLearnMoreLink() {
TranslationsParent.telemetry().panel().onLearnMoreLink();
- TranslationsPanel.close();
+ FullPageTranslationsPanel.close();
}
/*
@@ -1166,7 +1078,7 @@ var TranslationsPanel = new (class {
#openPromise = null;
/**
- * Opens the TranslationsPanel.
+ * Opens the FullPageTranslationsPanel.
*
* @param {Event} event
* @param {boolean} reportAsAutoShow
@@ -1185,7 +1097,7 @@ var TranslationsPanel = new (class {
}
/**
- * Implementation function for opening the panel. Prefer TranslationsPanel.open.
+ * Implementation function for opening the panel. Prefer FullPageTranslationsPanel.open.
*
* @param {Event} event
*/
@@ -1441,7 +1353,7 @@ var TranslationsPanel = new (class {
}
/**
- * An event handler that allows the TranslationsPanel object
+ * An event handler that allows the FullPageTranslationsPanel object
* to be compatible with the addTabsProgressListener function.
*
* @param {tabbrowser} browser
@@ -1513,7 +1425,7 @@ var TranslationsPanel = new (class {
if (detectedLanguages) {
// Ensure the cached detected languages are up to date, for instance whenever
// the user switches tabs.
- TranslationsPanel.detectedLanguages = detectedLanguages;
+ FullPageTranslationsPanel.detectedLanguages = detectedLanguages;
}
if (this.#isPopupOpen) {
@@ -1623,7 +1535,7 @@ var TranslationsPanel = new (class {
})();
XPCOMUtils.defineLazyPreferenceGetter(
- TranslationsPanel,
+ FullPageTranslationsPanel,
"_hasShownPanel",
"browser.translations.panelShown",
false
diff --git a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
new file mode 100644
index 0000000000..72e2bd7095
--- /dev/null
+++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
@@ -0,0 +1,102 @@
+<!-- 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/. -->
+
+<html:template id="template-select-translations-panel">
+ <panel id="select-translations-panel"
+ class="panel-no-padding translations-panel"
+ type="arrow"
+ role="alertdialog"
+ noautofocus="true"
+ aria-labelledby="translations-panel-header"
+ orient="vertical">
+ <panelmultiview id="select-translations-panel-multiview" mainViewId="select-translations-panel-view-default">
+ <panelview id="select-translations-panel-view-default"
+ class="PanelUI-subView translations-panel-view"
+ role="document"
+ mainview-with-header="true"
+ has-custom-header="true">
+ <hbox class="panel-header select-translations-panel-header">
+ <html:h1 class="translations-panel-header-wrapper">
+ <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header">
+ </html:span>
+ </html:h1>
+ <hbox class="translations-panel-beta">
+ <image id="select-translations-panel-beta-icon"
+ class="translations-panel-beta-icon">
+ </image>
+ </hbox>
+ <toolbarbutton id="select-translations-panel-settings"
+ class="panel-info-button translations-panel-settings-gear-icon"
+ data-l10n-id="translations-panel-settings-button"
+ closemenu="none" />
+ </hbox>
+ <vbox class="select-translations-panel-content">
+ <hbox id="select-translations-panel-lang-selection">
+ <vbox flex="1">
+ <label id="select-translations-panel-from-label"
+ class="select-translations-panel-label"
+ data-l10n-id="select-translations-panel-from-label">
+ </label>
+ <menulist id="select-translations-panel-from"
+ flex="1"
+ value="detect"
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="translations-panel-from-label">
+ <menupopup id="select-translations-panel-from-menupopup"
+ class="translations-panel-language-menupopup-from">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
+ </vbox>
+ <vbox flex="1">
+ <label id="select-translations-panel-to-label"
+ class="select-translations-panel-label"
+ data-l10n-id="select-translations-panel-to-label">
+ </label>
+ <menulist id="select-translations-panel-to"
+ flex="1"
+ value="detect"
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="translations-panel-to-label">
+ <menupopup id="select-translations-panel-to-menupopup"
+ class="translations-panel-language-menupopup-to">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox class="select-translations-panel-content">
+ <html:textarea id="select-translations-panel-translation-area"
+ data-l10n-id="select-translations-panel-placeholder-text"
+ readonly="true"
+ tabindex="0">
+ </html:textarea>
+ </vbox>
+
+ <hbox class="select-translations-panel-content">
+ <button id="select-translations-panel-copy-button"
+ class="footer-button select-translations-panel-button select-translations-panel-copy-button"
+ data-l10n-id="select-translations-panel-copy-button">
+ </button>
+ </hbox>
+
+ <html:moz-button-group class="panel-footer translations-panel-footer">
+ <button id="select-translations-panel-translate-full-page-button"
+ class="footer-button select-translations-panel-button"
+ data-l10n-id="select-translations-panel-translate-full-page-button">
+ </button>
+ <button id="select-translations-panel-done-button"
+ class="footer-button select-translations-panel-button"
+ data-l10n-id="select-translations-panel-done-button"
+ default="true"
+ oncommand = "SelectTranslationsPanel.close()">
+ </button>
+ </html:moz-button-group>
+ </panelview>
+ </panelmultiview>
+ </panel>
+</html:template>
diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js
new file mode 100644
index 0000000000..b4fe3e9735
--- /dev/null
+++ b/browser/components/translations/content/selectTranslationsPanel.js
@@ -0,0 +1,220 @@
+/* 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 */
+
+ChromeUtils.defineESModuleGetters(this, {
+ LanguageDetector:
+ "resource://gre/modules/translation/LanguageDetector.sys.mjs",
+ TranslationsPanelShared:
+ "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs",
+});
+
+/**
+ * This singleton class controls the Translations popup panel.
+ */
+var SelectTranslationsPanel = new (class {
+ /** @type {Console?} */
+ #console;
+
+ /**
+ * 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 available. 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;
+ }
+
+ /**
+ * 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-select-translations-panel"
+ );
+
+ const panel = wrapper.content.firstElementChild;
+ const settingsButton = document.getElementById(
+ "translations-panel-settings"
+ );
+ wrapper.replaceWith(wrapper.content);
+
+ // Lazily select the elements.
+ this.#lazyElements = {
+ panel,
+ settingsButton,
+ };
+
+ TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, {
+ betaIcon: "select-translations-panel-beta-icon",
+ copyButton: "select-translations-panel-copy-button",
+ doneButton: "select-translations-panel-done-button",
+ fromLabel: "select-translations-panel-from-label",
+ fromMenuList: "select-translations-panel-from",
+ header: "select-translations-panel-header",
+ multiview: "select-translations-panel-multiview",
+ textArea: "select-translations-panel-translation-area",
+ toLabel: "select-translations-panel-to-label",
+ toMenuList: "select-translations-panel-to",
+ translateFullPageButton:
+ "select-translations-panel-translate-full-page-button",
+ });
+ }
+
+ return this.#lazyElements;
+ }
+
+ /**
+ * Detects the language of the provided text and retrieves a language pair for translation
+ * based on user settings.
+ *
+ * @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed.
+ * @returns {Promise<{fromLang?: string, toLang?: string}>} - An object containing the language pair for the translation.
+ * The `fromLang` property is omitted if it is a language that is not currently supported by Firefox Translations.
+ * The `toLang` property is omitted if it is the same as `fromLang`.
+ */
+ async getLangPairPromise(textToTranslate) {
+ const [fromLang, toLang] = await Promise.all([
+ LanguageDetector.detectLanguage(textToTranslate).then(
+ ({ language }) => language
+ ),
+ TranslationsParent.getTopPreferredSupportedToLang(),
+ ]);
+
+ return {
+ fromLang,
+ // If the fromLang and toLang are the same, discard the toLang.
+ toLang: fromLang === toLang ? undefined : toLang,
+ };
+ }
+
+ /**
+ * Close the Select Translations Panel.
+ */
+ close() {
+ PanelMultiView.hidePopup(this.elements.panel);
+ }
+
+ /**
+ * 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.
+ */
+ async #ensureLangListsBuilt() {
+ try {
+ await TranslationsPanelShared.ensureLangListsBuilt(
+ document,
+ this.elements.panel
+ );
+ } catch (error) {
+ this.console?.error(error);
+ }
+ }
+
+ /**
+ * Updates the language dropdown based on the provided language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ * @param {Element} menuList - The dropdown menu element that will be updated based on language support.
+ * @returns {Promise<void>}
+ */
+ async #updateLanguageDropdown(langTag, menuList) {
+ const langTagIsSupported =
+ menuList.id === this.elements.fromMenuList.id
+ ? await TranslationsParent.isSupportedAsFromLang(langTag)
+ : await TranslationsParent.isSupportedAsToLang(langTag);
+
+ if (langTagIsSupported) {
+ // Remove the data-l10n-id because the menulist label will
+ // be populated from the supported language's display name.
+ menuList.value = langTag;
+ menuList.removeAttribute("data-l10n-id");
+ } else {
+ // Set the data-l10n-id placeholder because no valid
+ // language will be selected when the panel opens.
+ menuList.value = undefined;
+ document.l10n.setAttributes(
+ menuList,
+ "translations-panel-choose-language"
+ );
+ await document.l10n.translateElements([menuList]);
+ }
+ }
+
+ /**
+ * Updates the language selection dropdowns based on the given langPairPromise.
+ *
+ * @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise
+ * @returns {Promise<void>}
+ */
+ async #updateLanguageDropdowns(langPairPromise) {
+ const { fromLang, toLang } = await langPairPromise;
+
+ this.console?.debug(`fromLang(${fromLang})`);
+ this.console?.debug(`toLang(${toLang})`);
+
+ const { fromMenuList, toMenuList } = this.elements;
+
+ await Promise.all([
+ this.#updateLanguageDropdown(fromLang, fromMenuList),
+ this.#updateLanguageDropdown(toLang, toMenuList),
+ ]);
+ }
+
+ /**
+ * Opens the panel and populates the currently selected fromLang and toLang based
+ * on the result of the langPairPromise.
+ *
+ * @param {Event} event - The triggering event for opening the panel.
+ * @param {Promise} langPairPromise - Promise resolving to language pair data for initializing dropdowns.
+ * @returns {Promise<void>}
+ */
+ async open(event, langPairPromise) {
+ this.console?.log("Showing a translation panel.");
+
+ await this.#ensureLangListsBuilt();
+ await this.#updateLanguageDropdowns(langPairPromise);
+
+ // TODO(Bug 1878721) Rework the logic of where to open the panel.
+ //
+ // For the moment, the Select Translations panel opens at the
+ // AppMenu Button, but it will eventually need to open near
+ // to the selected content.
+ const appMenuButton = document.getElementById("PanelUI-menu-button");
+ const { panel, textArea } = this.elements;
+
+ panel.addEventListener("popupshown", () => textArea.focus(), {
+ once: true,
+ });
+ await PanelMultiView.openPopup(panel, appMenuButton, {
+ position: "bottomright topright",
+ triggerEvent: event,
+ }).catch(error => this.console?.error(error));
+ }
+})();