summaryrefslogtreecommitdiffstats
path: root/toolkit/components/translation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /toolkit/components/translation
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--toolkit/components/translation/moz.build2
-rw-r--r--toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs3
-rw-r--r--toolkit/components/translations/actors/TranslationsParent.sys.mjs281
-rw-r--r--toolkit/components/translations/content/Translator.mjs227
-rw-r--r--toolkit/components/translations/content/translations.mjs201
-rw-r--r--toolkit/components/translations/jar.mn1
-rw-r--r--toolkit/components/translations/moz.build2
-rw-r--r--toolkit/components/translations/tests/browser/browser.toml1
-rw-r--r--toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js2
-rw-r--r--toolkit/components/translations/tests/browser/shared-head.js154
-rw-r--r--toolkit/components/translations/tests/browser/translations-test.mjs29
-rw-r--r--toolkit/components/translations/tests/browser/translations-tester-es.html10
-rw-r--r--toolkit/components/translations/tests/browser/translations-tester-select.html76
-rw-r--r--toolkit/components/translations/translations.d.ts7
14 files changed, 675 insertions, 321 deletions
diff --git a/toolkit/components/translation/moz.build b/toolkit/components/translation/moz.build
index 75dfd04bc3..38214a2dc1 100644
--- a/toolkit/components/translation/moz.build
+++ b/toolkit/components/translation/moz.build
@@ -3,7 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files("**"):
- BUG_COMPONENT = ("Firefox", "Translation")
+ BUG_COMPONENT = ("Firefox", "Translations")
EXTRA_JS_MODULES.translation = [
"cld2/cld-worker.js",
diff --git a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
index 9d0b27a6a1..0b600bb03c 100644
--- a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
+++ b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
@@ -53,7 +53,7 @@ export class AboutTranslationsChild extends JSWindowActorChild {
receiveMessage({ name, data }) {
switch (name) {
- case "AboutTranslations:SendTranslationsPort":
+ case "AboutTranslations:SendTranslationsPort": {
const { fromLanguage, toLanguage, port } = data;
const transferables = [port];
this.contentWindow.postMessage(
@@ -67,6 +67,7 @@ export class AboutTranslationsChild extends JSWindowActorChild {
transferables
);
break;
+ }
default:
throw new Error("Unknown AboutTranslations message: " + name);
}
diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs
index 70754d95c4..f262cbeab2 100644
--- a/toolkit/components/translations/actors/TranslationsParent.sys.mjs
+++ b/toolkit/components/translations/actors/TranslationsParent.sys.mjs
@@ -150,10 +150,110 @@ const VERIFY_SIGNATURES_FROM_FS = false;
*/
/**
- * The translations parent is used to orchestrate translations in Firefox. It can
- * download the wasm translation engines, and the machine learning language models.
+ * The state that is stored per a "top" ChromeWindow. This "top" ChromeWindow is the JS
+ * global associated with a browser window. Some state is unique to a browser window, and
+ * using the top ChromeWindow is a unique key that ensures the state will be unique to
+ * that browser window.
*
- * See Bug 971044 for more details of planned work.
+ * See BrowsingContext.webidl for information on the "top"
+ * See the TranslationsParent JSDoc for more information on the state management.
+ */
+class StatePerTopChromeWindow {
+ /**
+ * The storage backing for the states.
+ *
+ * @type {WeakMap<ChromeWindow, StatePerTopChromeWindow>}
+ */
+ static #states = new WeakMap();
+
+ /**
+ * When reloading the page, store the translation pair that needs translating.
+ *
+ * @type {null | TranslationPair}
+ */
+ translateOnPageReload = null;
+
+ /**
+ * The page may auto-translate due to user settings. On a page restore, always
+ * skip the page restore logic.
+ *
+ * @type {boolean}
+ */
+ isPageRestored = false;
+
+ /**
+ * Remember the detected languages on a page reload. This will keep the translations
+ * button from disappearing and reappearing, which causes the button to lose focus.
+ *
+ * @type {LangTags | null} previousDetectedLanguages
+ */
+ previousDetectedLanguages = null;
+
+ static #id = 0;
+ /**
+ * @param {ChromeWindow} topChromeWindow
+ */
+ constructor(topChromeWindow) {
+ this.id = StatePerTopChromeWindow.#id++;
+ StatePerTopChromeWindow.#states.set(topChromeWindow, this);
+ }
+
+ /**
+ * @param {ChromeWindow} topChromeWindow
+ * @returns {StatePerTopChromeWindow}
+ */
+ static getOrCreate(topChromeWindow) {
+ let state = StatePerTopChromeWindow.#states.get(topChromeWindow);
+ if (state) {
+ return state;
+ }
+ state = new StatePerTopChromeWindow(topChromeWindow);
+ StatePerTopChromeWindow.#states.set(topChromeWindow, state);
+ return state;
+ }
+}
+
+/**
+ * The TranslationsParent is used to orchestrate translations in Firefox. It can
+ * download the Wasm translation engine, and the language models. It manages the life
+ * cycle for offering and performing translations.
+ *
+ * Care must be taken for the life cycle of the state management and data caching. The
+ * following examples use a fictitious `myState` property to show how state can be stored.
+ *
+ * There is only 1 TranslationsParent static class in the parent process. At this
+ * layer it is safe to store things like translation models and general browser
+ * configuration as these don't change across browser windows. This is accessed like
+ * `TranslationsParent.myState`
+ *
+ * The next layer down are the top ChromeWindows. These map to the UI and user's conception
+ * of a browser window, such as what you would get by hitting cmd+n or ctrl+n to get a new
+ * browser window. State such as whether a page is reloaded or general navigation events
+ * must be unique per ChromeWindow. State here is stored in the `StatePerTopChromeWindow`
+ * abstraction, like `this.getWindowState().myState`. This layer also consists of a
+ * `FullPageTranslationsPanel` instance per top ChromeWindow (at least on Desktop).
+ *
+ * The final layer consists of the multiple tabs and navigation history inside of a
+ * ChromeWindow. Data for this layer is safe to store on the TranslationsParent instance,
+ * like `this.myState`.
+ *
+ * Below is an ascii diagram of this relationship.
+ *
+ * ┌─────────────────────────────────────────────────────────────────────────────┐
+ * │ static TranslationsParent │
+ * └─────────────────────────────────────────────────────────────────────────────┘
+ * | |
+ * v v
+ * ┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
+ * │ top ChromeWindow │ │ top ChromeWindow │
+ * │ (FullPageTranslationsPanel instance) │ │ (FullPageTranslationsPanel instance) │
+ * └──────────────────────────────────────┘ └──────────────────────────────────────┘
+ * | | | | | |
+ * v v v v v v
+ * ┌────────────────────┐ ┌─────┐ ┌─────┐ ┌────────────────────┐ ┌─────┐ ┌─────┐
+ * │ TranslationsParent │ │ ... │ │ ... │ │ TranslationsParent │ │ ... │ │ ... │
+ * │ (actor instance) │ │ │ │ │ │ (actor instance) │ │ │ │ │
+ * └────────────────────┘ └─────┘ └─────┘ └────────────────────┘ └─────┘ └─────┘
*/
export class TranslationsParent extends JSWindowActorParent {
/**
@@ -205,26 +305,32 @@ export class TranslationsParent extends JSWindowActorParent {
#isDestroyed = false;
/**
- * Remember the detected languages on a page reload. This will keep the translations
- * button from disappearing and reappearing, which causes the button to lose focus.
+ * There is only one static TranslationsParent for all of the top ChromeWindows.
+ * The top ChromeWindow maps to the user's conception of a window such as when you hit
+ * cmd+n or ctrl+n.
*
- * @type {LangTags | null} previousDetectedLanguages
+ * @returns {StatePerTopChromeWindow}
*/
- static #previousDetectedLanguages = null;
+ getWindowState() {
+ const state = StatePerTopChromeWindow.getOrCreate(
+ this.browsingContext.top.embedderWindowGlobal
+ );
+ return state;
+ }
actorCreated() {
this.innerWindowId = this.browsingContext.top.embedderElement.innerWindowID;
+ const windowState = this.getWindowState();
this.languageState = new TranslationsLanguageState(
this,
- TranslationsParent.#previousDetectedLanguages
+ windowState.previousDetectedLanguages
);
- TranslationsParent.#previousDetectedLanguages = null;
+ windowState.previousDetectedLanguages = null;
- if (TranslationsParent.#translateOnPageReload) {
+ if (windowState.translateOnPageReload) {
// The actor was recreated after a page reload, start the translation.
- const { fromLanguage, toLanguage } =
- TranslationsParent.#translateOnPageReload;
- TranslationsParent.#translateOnPageReload = null;
+ const { fromLanguage, toLanguage } = windowState.translateOnPageReload;
+ windowState.translateOnPageReload = null;
lazy.console.log(
`Translating on a page reload from "${fromLanguage}" to "${toLanguage}".`
@@ -261,12 +367,6 @@ export class TranslationsParent extends JSWindowActorParent {
static #translationsWasmRemoteClient = null;
/**
- * The page may auto-translate due to user settings. On a page restore, always
- * skip the page restore logic.
- */
- static #isPageRestored = false;
-
- /**
* Allows the actor's behavior to be changed when the translations engine is mocked via
* a dummy RemoteSettingsClient.
*
@@ -280,13 +380,6 @@ export class TranslationsParent extends JSWindowActorParent {
static #isTranslationsEngineSupported = null;
/**
- * When reloading the page, store the translation pair that needs translating.
- *
- * @type {null | TranslationPair}
- */
- static #translateOnPageReload = null;
-
- /**
* An ordered list of preferred languages based on:
* 1. App languages
* 2. Web requested languages
@@ -477,6 +570,24 @@ export class TranslationsParent extends JSWindowActorParent {
}
/**
+ * Retrieves the Translations actor from the current browser context.
+ *
+ * @param {object} browser - The browser object from which to get the context.
+ *
+ * @returns {object} The Translations actor for handling translation actions.
+ * @throws {Error} Throws an error if the TranslationsParent actor cannot be found.
+ */
+ static getTranslationsActor(browser) {
+ const actor =
+ browser.browsingContext.currentWindowGlobal.getActor("Translations");
+
+ if (!actor) {
+ throw new Error("Unable to get the TranslationsParent actor.");
+ }
+ return actor;
+ }
+
+ /**
* Detect if Wasm SIMD is supported, and cache the value. It's better to check
* for support before downloading large binary blobs to a user who can't even
* use the feature. This function also respects mocks and simulating unsupported
@@ -670,6 +781,42 @@ export class TranslationsParent extends JSWindowActorParent {
return TranslationsParent.#preferredLanguages;
}
+ /**
+ * Requests a new translations port.
+ *
+ * @param {number} innerWindowId - The id of the current window.
+ * @param {string} fromLanguage - The BCP-47 from-language tag.
+ * @param {string} toLanguage - The BCP-47 to-language tag.
+ *
+ * @returns {Promise<MessagePort | undefined>} The port for communication with the translation engine, or undefined on failure.
+ */
+ static async requestTranslationsPort(
+ innerWindowId,
+ fromLanguage,
+ toLanguage
+ ) {
+ let translationsEngineParent;
+ try {
+ translationsEngineParent =
+ await lazy.EngineProcess.getTranslationsEngineParent();
+ } catch (error) {
+ console.error("Failed to get the translation engine process", error);
+ return undefined;
+ }
+
+ // The MessageChannel will be used for communicating directly between the content
+ // process and the engine's process.
+ const { port1, port2 } = new MessageChannel();
+ translationsEngineParent.startTranslation(
+ fromLanguage,
+ toLanguage,
+ port1,
+ innerWindowId
+ );
+
+ return port2;
+ }
+
async receiveMessage({ name, data }) {
switch (name) {
case "Translations:ReportLangTags": {
@@ -826,10 +973,11 @@ export class TranslationsParent extends JSWindowActorParent {
* @param {LangTags} langTags
* @returns {boolean}
*/
- static #maybeAutoTranslate(langTags) {
- if (TranslationsParent.#isPageRestored) {
+ #maybeAutoTranslate(langTags) {
+ const windowState = this.getWindowState();
+ if (windowState.isPageRestored) {
// The user clicked the restore button. Respect it for one page load.
- TranslationsParent.#isPageRestored = false;
+ windowState.isPageRestored = false;
// Skip this auto-translation.
return false;
@@ -875,6 +1023,9 @@ export class TranslationsParent extends JSWindowActorParent {
}
return Array.from(languagePairMap.values());
});
+ TranslationsParent.#languagePairs.catch(() => {
+ TranslationsParent.#languagePairs = null;
+ });
}
return TranslationsParent.#languagePairs;
}
@@ -1671,7 +1822,8 @@ export class TranslationsParent extends JSWindowActorParent {
`Translation model fetched in ${duration / 1000} seconds:`,
record.fromLang,
record.toLang,
- record.fileType
+ record.fileType,
+ record.version
);
})
);
@@ -1901,7 +2053,8 @@ export class TranslationsParent extends JSWindowActorParent {
if (this.languageState.requestedTranslationPair) {
// This page has already been translated, restore it and translate it
// again once the actor has been recreated.
- TranslationsParent.#translateOnPageReload = { fromLanguage, toLanguage };
+ const windowState = this.getWindowState();
+ windowState.translateOnPageReload = { fromLanguage, toLanguage };
this.restorePage(fromLanguage);
} else {
const { docLangTag } = this.languageState.detectedLanguages;
@@ -1970,47 +2123,29 @@ export class TranslationsParent extends JSWindowActorParent {
restorePage() {
TranslationsParent.telemetry().onRestorePage();
// Skip auto-translate for one page load.
- TranslationsParent.#isPageRestored = true;
+ const windowState = this.getWindowState();
+ windowState.isPageRestored = true;
this.languageState.requestedTranslationPair = null;
- TranslationsParent.#previousDetectedLanguages =
+ windowState.previousDetectedLanguages =
this.languageState.detectedLanguages;
const browser = this.browsingContext.embedderElement;
browser.reload();
}
- /**
- * Keep track of when the location changes.
- */
- static #locationChangeId = 0;
-
static onLocationChange(browser) {
if (!lazy.translationsEnabledPref) {
// The pref isn't enabled, so don't attempt to get the actor.
return;
}
- let windowGlobal = browser.browsingContext.currentWindowGlobal;
- TranslationsParent.#locationChangeId++;
let actor;
try {
- actor = windowGlobal.getActor("Translations");
- } catch (_) {
- // The actor may not be supported on this page.
- }
- if (actor) {
- actor.languageState.locationChangeId =
- TranslationsParent.#locationChangeId;
+ actor =
+ browser.browsingContext.currentWindowGlobal.getActor("Translations");
+ } catch {
+ // The actor may not be supported on this page, which throws an error.
}
- }
-
- /**
- * Is this actor active for the current location change?
- *
- * @param {number} locationChangeId - The id sent by the "TranslationsParent:LanguageState" event.
- * @returns {boolean}
- */
- static isActiveLocation(locationChangeId) {
- return locationChangeId === TranslationsParent.#locationChangeId;
+ actor?.languageState.locationChanged();
}
async queryIdentifyLanguage() {
@@ -2046,7 +2181,7 @@ export class TranslationsParent extends JSWindowActorParent {
langTags.docLangTag &&
langTags.userLangTag &&
langTags.isDocLangTagSupported &&
- TranslationsParent.#maybeAutoTranslate(langTags) &&
+ this.#maybeAutoTranslate(langTags) &&
!TranslationsParent.shouldNeverTranslateLanguage(langTags.docLangTag) &&
!this.shouldNeverTranslateSite()
) {
@@ -2599,7 +2734,6 @@ class TranslationsLanguageState {
constructor(actor, previousDetectedLanguages = null) {
this.#actor = actor;
this.#detectedLanguages = previousDetectedLanguages;
- this.dispatch();
}
/**
@@ -2616,9 +2750,6 @@ class TranslationsLanguageState {
/** @type {LangTags | null} */
#detectedLanguages = null;
- /** @type {number} */
- #locationChangeId = -1;
-
/** @type {null | TranslationErrors} */
#error = null;
@@ -2628,11 +2759,6 @@ class TranslationsLanguageState {
* Dispatch anytime the language details change, so that any UI can react to it.
*/
dispatch() {
- if (!TranslationsParent.isActiveLocation(this.#locationChangeId)) {
- // Do not dispatch as this location is not active.
- return;
- }
-
const browser = this.#actor.browsingContext.top.embedderElement;
if (!browser) {
return;
@@ -2690,26 +2816,11 @@ class TranslationsLanguageState {
}
/**
- * This id represents the last location change that happened for this actor. This
- * allows the UI to disambiguate when there are races and out of order events that
- * are dispatched. Only the most up to date `locationChangeId` is used.
- *
- * @returns {number}
+ * When the location changes remove the previous error and dispatch a change event
+ * so that any browser chrome UI that needs to be updated can get the latest state.
*/
- get locationChangeId() {
- return this.#locationChangeId;
- }
-
- set locationChangeId(locationChangeId) {
- if (this.#locationChangeId === locationChangeId) {
- return;
- }
-
- this.#locationChangeId = locationChangeId;
-
- // When the location changes remove the previous error.
+ locationChanged() {
this.#error = null;
-
this.dispatch();
}
diff --git a/toolkit/components/translations/content/Translator.mjs b/toolkit/components/translations/content/Translator.mjs
new file mode 100644
index 0000000000..9a0de6a2c2
--- /dev/null
+++ b/toolkit/components/translations/content/Translator.mjs
@@ -0,0 +1,227 @@
+/* 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/. */
+
+/**
+ * This class manages the communications to the translations engine via MessagePort.
+ */
+export class Translator {
+ /**
+ * The port through with to communicate with the Translations Engine.
+ *
+ * @type {MessagePort}
+ */
+ #port;
+
+ /**
+ * True if the current #port is closed, otherwise false.
+ *
+ * @type {boolean}
+ */
+ #portClosed = true;
+
+ /**
+ * A promise that resolves when the Translator has successfully established communication with
+ * the translations engine, or rejects if communication was not successfully established.
+ *
+ * @type {Promise<void>}
+ */
+ #ready = Promise.reject;
+
+ /**
+ * The BCP-47 language tag for the from-language.
+ *
+ * @type {string}
+ */
+ #fromLanguage;
+
+ /**
+ * The BCP-47 language tag for the to-language.
+ *
+ * @type {string}
+ */
+ #toLanguage;
+
+ /**
+ * The callback function to request a new port, provided at construction time
+ * by the caller.
+ *
+ * @type {Function}
+ */
+ #requestTranslationsPort;
+
+ /**
+ * An id for each message sent. This is used to match up the request and response.
+ *
+ * @type {number}
+ */
+ #nextMessageId = 0;
+
+ /**
+ * Tie together a message id to a resolved response.
+ *
+ * @type {Map<number, TranslationRequest>}
+ */
+ #requests = new Map();
+
+ /**
+ * Initializes a new Translator.
+ *
+ * Prefer using the Translator.create() function.
+ *
+ * @see Translator.create
+ *
+ * @param {string} fromLanguage - The BCP-47 from-language tag.
+ * @param {string} toLanguage - The BCP-47 to-language tag.
+ * @param {Function} requestTranslationsPort - A callback function to request a new MessagePort.
+ */
+ constructor(fromLanguage, toLanguage, requestTranslationsPort) {
+ this.#fromLanguage = fromLanguage;
+ this.#toLanguage = toLanguage;
+ this.#requestTranslationsPort = requestTranslationsPort;
+ this.#createNewPortIfClosed();
+ }
+
+ /**
+ * @returns {Promise<void>} A promise that indicates if the Translator is ready to translate.
+ */
+ get ready() {
+ return this.#ready;
+ }
+
+ /**
+ * @returns {boolean} True if the translation port is closed, false otherwise.
+ */
+ get portClosed() {
+ return this.#portClosed;
+ }
+
+ /**
+ * @returns {string} The BCP-47 language tag of the from-language.
+ */
+ get fromLanguage() {
+ return this.#fromLanguage;
+ }
+
+ /**
+ * @returns {string} The BCP-47 language tag of the to-language.
+ */
+ get toLanguage() {
+ return this.#toLanguage;
+ }
+
+ /**
+ * Opens up a port and creates a new translator.
+ *
+ * @param {string} fromLanguage
+ * @param {string} toLanguage
+ * @returns {Promise<Translator>}
+ */
+ static async create(fromLanguage, toLanguage, requestTranslationsPort) {
+ if (!fromLanguage || !toLanguage || !requestTranslationsPort) {
+ return undefined;
+ }
+
+ const translator = new Translator(
+ fromLanguage,
+ toLanguage,
+ requestTranslationsPort
+ );
+ await translator.ready;
+
+ return translator;
+ }
+
+ /**
+ * Creates a new translation port if the current one is closed.
+ *
+ * @returns {Promise<void>} - Whether the Translator is ready to translate.
+ */
+ async #createNewPortIfClosed() {
+ if (!this.#portClosed) {
+ return this.#ready;
+ }
+
+ this.#port = await this.#requestTranslationsPort(
+ this.#fromLanguage,
+ this.#toLanguage
+ );
+
+ // Create a promise that will be resolved when the engine is ready.
+ const { promise, resolve, reject } = Promise.withResolvers();
+
+ // Match up a response on the port to message that was sent.
+ this.#port.onmessage = ({ data }) => {
+ switch (data.type) {
+ case "TranslationsPort:TranslationResponse": {
+ const { targetText, messageId } = data;
+ // A request may not match match a messageId if there is a race during the pausing
+ // and discarding of the queue.
+ this.#requests.get(messageId)?.resolve(targetText);
+ break;
+ }
+ case "TranslationsPort:GetEngineStatusResponse": {
+ if (data.status === "ready") {
+ this.#portClosed = false;
+ resolve();
+ } else {
+ this.#portClosed = true;
+ reject();
+ }
+ break;
+ }
+ case "TranslationsPort:EngineTerminated": {
+ this.#portClosed = true;
+ break;
+ }
+ default:
+ break;
+ }
+ };
+
+ this.#ready = promise;
+ this.#port.postMessage({ type: "TranslationsPort:GetEngineStatusRequest" });
+
+ return this.#ready;
+ }
+
+ /**
+ * Send a request to translate text to the Translations Engine. If it returns `null`
+ * then the request is stale. A rejection means there was an error in the translation.
+ * This request may be queued.
+ *
+ * @param {string} sourceText
+ * @returns {Promise<string>}
+ */
+ async translate(sourceText, isHTML = false) {
+ await this.#createNewPortIfClosed();
+ const { promise, resolve, reject } = Promise.withResolvers();
+ const messageId = this.#nextMessageId++;
+
+ // Store the "resolve" for the promise. It will be matched back up with the
+ // `messageId` in #handlePortMessage.
+ this.#requests.set(messageId, {
+ sourceText,
+ isHTML,
+ resolve,
+ reject,
+ });
+ this.#port.postMessage({
+ type: "TranslationsPort:TranslationRequest",
+ messageId,
+ sourceText,
+ isHTML,
+ });
+
+ return promise;
+ }
+
+ /**
+ * Close the port and remove any pending or queued requests.
+ */
+ destroy() {
+ this.#port.close();
+ this.#portClosed = true;
+ this.#ready = Promise.reject;
+ }
+}
diff --git a/toolkit/components/translations/content/translations.mjs b/toolkit/components/translations/content/translations.mjs
index 478f854bb5..d5541408d4 100644
--- a/toolkit/components/translations/content/translations.mjs
+++ b/toolkit/components/translations/content/translations.mjs
@@ -10,6 +10,8 @@
AT_logError, AT_createTranslationsPort, AT_isHtmlTranslation,
AT_isTranslationEngineSupported, AT_identifyLanguage */
+import { Translator } from "chrome://global/content/translations/Translator.mjs";
+
// Allow tests to override this value so that they can run faster.
// This is the delay in milliseconds.
window.DEBOUNCE_DELAY = 200;
@@ -66,7 +68,7 @@ class TranslationsState {
* The translator is only valid for a single language pair, and needs
* to be recreated if the language pair changes.
*
- * @type {null | Promise<Translator>}
+ * @type {null | Translator}
*/
translator = null;
@@ -133,42 +135,28 @@ class TranslationsState {
onDebounce: async () => {
// The contents of "this" can change between async steps, store a local variable
// binding of these values.
- const {
- fromLanguage,
- toLanguage,
- messageToTranslate,
- translator: translatorPromise,
- } = this;
+ const { fromLanguage, toLanguage, messageToTranslate, translator } = this;
if (!this.isTranslationEngineSupported) {
// Never translate when the engine isn't supported.
return;
}
- if (
- !fromLanguage ||
- !toLanguage ||
- !messageToTranslate ||
- !translatorPromise
- ) {
+ if (!fromLanguage || !toLanguage || !messageToTranslate || !translator) {
// Not everything is set for translation.
this.ui.updateTranslation("");
return;
}
- const [translator] = await Promise.all([
- // Ensure the engine is ready to go.
- translatorPromise,
- // Ensure the previous translation has finished so that only the latest
- // translation goes through.
- this.translationRequest,
- ]);
+ // Ensure the previous translation has finished so that only the latest
+ // translation goes through.
+ await this.translationRequest;
if (
// Check if the current configuration has changed and if this is stale. If so
// then skip this request, as there is already a newer request with more up to
// date information.
- this.translator !== translatorPromise ||
+ this.translator !== translator ||
this.fromLanguage !== fromLanguage ||
this.toLanguage !== toLanguage ||
this.messageToTranslate !== messageToTranslate
@@ -177,8 +165,10 @@ class TranslationsState {
}
const start = performance.now();
-
- this.translationRequest = translator.translate(messageToTranslate);
+ this.translationRequest = this.translator.translate(
+ messageToTranslate,
+ AT_isHtmlTranslation()
+ );
const translation = await this.translationRequest;
// The measure events will show up in the Firefox Profiler.
@@ -236,15 +226,40 @@ class TranslationsState {
`Creating a new translator for "${this.fromLanguage}" to "${this.toLanguage}"`
);
- this.translator = Translator.create(this.fromLanguage, this.toLanguage);
- this.maybeRequestTranslation();
+ const translationPortPromise = (fromLanguage, toLanguage) => {
+ const { promise, resolve } = Promise.withResolvers();
+
+ const getResponse = ({ data }) => {
+ if (
+ data.type == "GetTranslationsPort" &&
+ data.fromLanguage === fromLanguage &&
+ data.toLanguage === toLanguage
+ ) {
+ window.removeEventListener("message", getResponse);
+ resolve(data.port);
+ }
+ };
+
+ window.addEventListener("message", getResponse);
+ AT_createTranslationsPort(fromLanguage, toLanguage);
+
+ return promise;
+ };
try {
- await this.translator;
+ const translatorPromise = Translator.create(
+ this.fromLanguage,
+ this.toLanguage,
+ translationPortPromise
+ );
const duration = performance.now() - start;
+
// Signal to tests that the translator was created so they can exit.
window.postMessage("translator-ready");
AT_log(`Created a new Translator in ${duration / 1000} seconds`);
+
+ this.translator = await translatorPromise;
+ this.maybeRequestTranslation();
} catch (error) {
this.ui.showInfo("about-translations-engine-error");
AT_logError("Failed to get the Translations worker", error);
@@ -655,137 +670,3 @@ function debounce({ onDebounce, doEveryTime }) {
}, timeLeft);
};
}
-
-/**
- * Perform transalations over a `MessagePort`. This class manages the communications to
- * the translations engine.
- */
-class Translator {
- /**
- * @type {MessagePort}
- */
- #port;
-
- /**
- * An id for each message sent. This is used to match up the request and response.
- */
- #nextMessageId = 0;
-
- /**
- * Tie together a message id to a resolved response.
- *
- * @type {Map<number, TranslationRequest>}
- */
- #requests = new Map();
-
- engineStatus = "initializing";
-
- /**
- * @param {MessagePort} port
- */
- constructor(port) {
- this.#port = port;
-
- // Create a promise that will be resolved when the engine is ready.
- let engineLoaded;
- let engineFailed;
- this.ready = new Promise((resolve, reject) => {
- engineLoaded = resolve;
- engineFailed = reject;
- });
-
- // Match up a response on the port to message that was sent.
- port.onmessage = ({ data }) => {
- switch (data.type) {
- case "TranslationsPort:TranslationResponse": {
- const { targetText, messageId } = data;
- // A request may not match match a messageId if there is a race during the pausing
- // and discarding of the queue.
- this.#requests.get(messageId)?.resolve(targetText);
- break;
- }
- case "TranslationsPort:GetEngineStatusResponse": {
- if (data.status === "ready") {
- engineLoaded();
- } else {
- engineFailed();
- }
- break;
- }
- default:
- AT_logError("Unknown translations port message: " + data.type);
- break;
- }
- };
-
- port.postMessage({ type: "TranslationsPort:GetEngineStatusRequest" });
- }
-
- /**
- * Opens up a port and creates a new translator.
- *
- * @param {string} fromLanguage
- * @param {string} toLanguage
- * @returns {Promise<Translator>}
- */
- static create(fromLanguage, toLanguage) {
- return new Promise((resolve, reject) => {
- AT_createTranslationsPort(fromLanguage, toLanguage);
-
- function getResponse({ data }) {
- if (
- data.type == "GetTranslationsPort" &&
- fromLanguage === data.fromLanguage &&
- toLanguage === data.toLanguage
- ) {
- // The response matches, resolve the port.
- const translator = new Translator(data.port);
-
- // Resolve the translator once it is ready, or propagate the rejection
- // if it failed.
- translator.ready.then(() => resolve(translator), reject);
- window.removeEventListener("message", getResponse);
- }
- }
-
- // Listen for a response for the message port.
- window.addEventListener("message", getResponse);
- });
- }
-
- /**
- * Send a request to translate text to the Translations Engine. If it returns `null`
- * then the request is stale. A rejection means there was an error in the translation.
- * This request may be queued.
- *
- * @param {string} sourceText
- * @returns {Promise<string>}
- */
- translate(sourceText) {
- return new Promise((resolve, reject) => {
- const messageId = this.#nextMessageId++;
- // Store the "resolve" for the promise. It will be matched back up with the
- // `messageId` in #handlePortMessage.
- const isHTML = AT_isHtmlTranslation();
- this.#requests.set(messageId, {
- sourceText,
- isHTML,
- resolve,
- reject,
- });
- this.#port.postMessage({
- type: "TranslationsPort:TranslationRequest",
- messageId,
- sourceText,
- isHTML,
- });
- });
- }
-
- /**
- * Close the port and remove any pending or queued requests.
- */
- destroy() {
- this.#port.close();
- }
-}
diff --git a/toolkit/components/translations/jar.mn b/toolkit/components/translations/jar.mn
index b31f17718f..1dde76d3ff 100644
--- a/toolkit/components/translations/jar.mn
+++ b/toolkit/components/translations/jar.mn
@@ -11,6 +11,7 @@ toolkit.jar:
content/global/translations/translations.html (content/translations.html)
content/global/translations/translations.css (content/translations.css)
content/global/translations/translations.mjs (content/translations.mjs)
+ content/global/translations/Translator.mjs (content/Translator.mjs)
content/global/translations/TranslationsTelemetry.sys.mjs (TranslationsTelemetry.sys.mjs)
# Uncomment this line to test a local build of Bergamot. It will automatically be loaded in.
diff --git a/toolkit/components/translations/moz.build b/toolkit/components/translations/moz.build
index cdb0ca4e75..0e6c222126 100644
--- a/toolkit/components/translations/moz.build
+++ b/toolkit/components/translations/moz.build
@@ -7,7 +7,7 @@ SPHINX_TREES["/toolkit/components/translations"] = "docs"
JAR_MANIFESTS += ["jar.mn"]
with Files("**"):
- BUG_COMPONENT = ("Firefox", "Translation")
+ BUG_COMPONENT = ("Firefox", "Translations")
DIRS += ["actors"]
diff --git a/toolkit/components/translations/tests/browser/browser.toml b/toolkit/components/translations/tests/browser/browser.toml
index 50a1be7150..c940bea9a0 100644
--- a/toolkit/components/translations/tests/browser/browser.toml
+++ b/toolkit/components/translations/tests/browser/browser.toml
@@ -9,6 +9,7 @@ support-files = [
"translations-tester-es-2.html",
"translations-tester-fr.html",
"translations-tester-no-tag.html",
+ "translations-tester-select.html",
"translations-tester-shadow-dom-es.html",
"translations-tester-shadow-dom-mutation-es.html",
"translations-tester-shadow-dom-mutation-es-2.html",
diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js b/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js
index 6aed512952..381903e5b1 100644
--- a/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js
+++ b/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js
@@ -13,7 +13,7 @@ add_task(async function test_about_translations_dropdowns() {
await openAboutTranslations({
languagePairs,
dataForContent: languagePairs,
- runInPage: async ({ dataForContent: languagePairs, selectors }) => {
+ runInPage: async ({ selectors }) => {
const { document } = content;
await ContentTaskUtils.waitForCondition(
diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js
index 82b3e783a7..afa060c8a3 100644
--- a/toolkit/components/translations/tests/browser/shared-head.js
+++ b/toolkit/components/translations/tests/browser/shared-head.js
@@ -9,6 +9,9 @@
const { EngineProcess } = ChromeUtils.importESModule(
"chrome://global/content/ml/EngineProcess.sys.mjs"
);
+const { TranslationsPanelShared } = ChromeUtils.importESModule(
+ "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs"
+);
// Avoid about:blank's non-standard behavior.
const BLANK_PAGE =
@@ -32,6 +35,8 @@ const NO_LANGUAGE_URL =
URL_COM_PREFIX + DIR_PATH + "translations-tester-no-tag.html";
const EMPTY_PDF_URL =
URL_COM_PREFIX + DIR_PATH + "translations-tester-empty-pdf-file.pdf";
+const SELECT_TEST_PAGE_URL =
+ URL_COM_PREFIX + DIR_PATH + "translations-tester-select.html";
const PIVOT_LANGUAGE = "en";
const LANGUAGE_PAIRS = [
@@ -349,33 +354,37 @@ function getTranslationsParent() {
/**
* Closes all open panels and menu popups related to Translations.
+ *
+ * @param {ChromeWindow} [win]
*/
-async function closeAllOpenPanelsAndMenus() {
- await closeSettingsMenuIfOpen();
- await closeFullPageTranslationsPanelIfOpen();
- await closeSelectTranslationsPanelIfOpen();
- await closeContextMenuIfOpen();
+async function closeAllOpenPanelsAndMenus(win) {
+ await closeSettingsMenuIfOpen(win);
+ await closeFullPageTranslationsPanelIfOpen(win);
+ await closeSelectTranslationsPanelIfOpen(win);
+ await closeContextMenuIfOpen(win);
}
/**
* Closes the popup element with the given Id if it is open.
*
* @param {string} popupElementId
+ * @param {ChromeWindow} [win]
*/
-async function closePopupIfOpen(popupElementId) {
+async function closePopupIfOpen(popupElementId, win = window) {
await waitForCondition(async () => {
- const contextMenu = document.getElementById(popupElementId);
- if (!contextMenu) {
+ const popupElement = win.document.getElementById(popupElementId);
+ if (!popupElement) {
return true;
}
- if (contextMenu.state === "closed") {
+ if (popupElement.state === "closed") {
return true;
}
let popuphiddenPromise = BrowserTestUtils.waitForEvent(
- contextMenu,
+ popupElement,
"popuphidden"
);
- PanelMultiView.hidePopup(contextMenu);
+ popupElement.hidePopup();
+ PanelMultiView.hidePopup(popupElement);
await popuphiddenPromise;
return false;
});
@@ -383,30 +392,41 @@ async function closePopupIfOpen(popupElementId) {
/**
* Closes the context menu if it is open.
+ *
+ * @param {ChromeWindow} [win]
*/
-async function closeContextMenuIfOpen() {
- await closePopupIfOpen("contentAreaContextMenu");
+async function closeContextMenuIfOpen(win) {
+ await closePopupIfOpen("contentAreaContextMenu", win);
}
/**
* Closes the translations panel settings menu if it is open.
+ *
+ * @param {ChromeWindow} [win]
*/
-async function closeSettingsMenuIfOpen() {
- await closePopupIfOpen("full-page-translations-panel-settings-menupopup");
+async function closeSettingsMenuIfOpen(win) {
+ await closePopupIfOpen(
+ "full-page-translations-panel-settings-menupopup",
+ win
+ );
}
/**
* Closes the translations panel if it is open.
+ *
+ * @param {ChromeWindow} [win]
*/
-async function closeFullPageTranslationsPanelIfOpen() {
- await closePopupIfOpen("full-page-translations-panel");
+async function closeFullPageTranslationsPanelIfOpen(win) {
+ await closePopupIfOpen("full-page-translations-panel", win);
}
/**
* Closes the translations panel if it is open.
+ *
+ * @param {ChromeWindow} [win]
*/
-async function closeSelectTranslationsPanelIfOpen() {
- await closePopupIfOpen("select-translations-panel");
+async function closeSelectTranslationsPanelIfOpen(win) {
+ await closePopupIfOpen("select-translations-panel", win);
}
/**
@@ -471,6 +491,7 @@ async function createAndMockRemoteSettings({
// The TranslationsParent will pull the language pair values from the JSON dump
// of Remote Settings. Clear these before mocking the translations engine.
TranslationsParent.clearCache();
+ TranslationsPanelShared.clearCache();
TranslationsParent.mockTranslationsEngine(
remoteClients.translationModels.client,
@@ -485,6 +506,7 @@ async function createAndMockRemoteSettings({
TranslationsParent.unmockTranslationsEngine();
TranslationsParent.clearCache();
+ TranslationsPanelShared.clearCache();
},
remoteClients,
};
@@ -497,38 +519,56 @@ async function loadTestPage({
prefs,
autoOffer,
permissionsUrls,
+ win = window,
}) {
info(`Loading test page starting at url: ${page}`);
- // Ensure no engine is being carried over from a previous test.
- await EngineProcess.destroyTranslationsEngine();
- Services.fog.testResetFOG();
- await SpecialPowers.pushPrefEnv({
- set: [
- // Enabled by default.
- ["browser.translations.enable", true],
- ["browser.translations.logLevel", "All"],
- ["browser.translations.panelShown", true],
- ["browser.translations.automaticallyPopup", true],
- ["browser.translations.alwaysTranslateLanguages", ""],
- ["browser.translations.neverTranslateLanguages", ""],
- ...(prefs ?? []),
- ],
- });
- await SpecialPowers.pushPermissions(
- [
- ENGLISH_PAGE_URL,
- FRENCH_PAGE_URL,
- NO_LANGUAGE_URL,
- SPANISH_PAGE_URL,
- SPANISH_PAGE_URL_2,
- SPANISH_PAGE_URL_DOT_ORG,
- ...(permissionsUrls || []),
- ].map(url => ({
- type: TRANSLATIONS_PERMISSION,
- allow: true,
- context: url,
- }))
- );
+
+ // If there are multiple windows, only do the first time setup on the main window.
+ const isFirstTimeSetup = win === window;
+
+ let remoteClients = null;
+ let removeMocks = () => {};
+
+ if (isFirstTimeSetup) {
+ // Ensure no engine is being carried over from a previous test.
+ await EngineProcess.destroyTranslationsEngine();
+
+ Services.fog.testResetFOG();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Enabled by default.
+ ["browser.translations.enable", true],
+ ["browser.translations.logLevel", "All"],
+ ["browser.translations.panelShown", true],
+ ["browser.translations.automaticallyPopup", true],
+ ["browser.translations.alwaysTranslateLanguages", ""],
+ ["browser.translations.neverTranslateLanguages", ""],
+ ...(prefs ?? []),
+ ],
+ });
+ await SpecialPowers.pushPermissions(
+ [
+ ENGLISH_PAGE_URL,
+ FRENCH_PAGE_URL,
+ NO_LANGUAGE_URL,
+ SPANISH_PAGE_URL,
+ SPANISH_PAGE_URL_2,
+ SPANISH_PAGE_URL_DOT_ORG,
+ ...(permissionsUrls || []),
+ ].map(url => ({
+ type: TRANSLATIONS_PERMISSION,
+ allow: true,
+ context: url,
+ }))
+ );
+
+ const result = await createAndMockRemoteSettings({
+ languagePairs,
+ autoDownloadFromRemoteSettings,
+ });
+ remoteClients = result.remoteClients;
+ removeMocks = result.removeMocks;
+ }
if (autoOffer) {
TranslationsParent.testAutomaticPopup = true;
@@ -536,16 +576,11 @@ async function loadTestPage({
// Start the tab at a blank page.
const tab = await BrowserTestUtils.openNewForegroundTab(
- gBrowser,
+ win.gBrowser,
BLANK_PAGE,
true // waitForLoad
);
- const { remoteClients, removeMocks } = await createAndMockRemoteSettings({
- languagePairs,
- autoDownloadFromRemoteSettings,
- });
-
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, page);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
@@ -1377,7 +1412,7 @@ async function waitForCloseDialogWindow(dialogWindow) {
// Extracted from https://searchfox.org/mozilla-central/rev/40ef22080910c2e2c27d9e2120642376b1d8b8b2/browser/components/preferences/in-content/tests/head.js#41
function promiseLoadSubDialog(aURL) {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
content.gSubDialog._dialogStack.addEventListener(
"dialogopen",
function dialogopen(aEvent) {
@@ -1444,3 +1479,10 @@ async function loadBlankPage() {
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BLANK_PAGE);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
}
+
+/**
+ * Destroys the Translations Engine process.
+ */
+async function destroyTranslationsEngine() {
+ await EngineProcess.destroyTranslationsEngine();
+}
diff --git a/toolkit/components/translations/tests/browser/translations-test.mjs b/toolkit/components/translations/tests/browser/translations-test.mjs
index a740a2d1cc..3ff107a699 100644
--- a/toolkit/components/translations/tests/browser/translations-test.mjs
+++ b/toolkit/components/translations/tests/browser/translations-test.mjs
@@ -38,21 +38,36 @@ export function getSelectors() {
getHeader() {
return content.document.querySelector("header");
},
- getFirstParagraph() {
- return content.document.querySelector("p:first-of-type");
- },
getLastParagraph() {
return content.document.querySelector("p:last-of-type");
},
- getSpanishParagraph() {
- return content.document.getElementById("spanish-paragraph");
+ getFrenchSection() {
+ return content.document.getElementById("french-section");
},
- getSpanishHyperlink() {
- return content.document.getElementById("spanish-hyperlink");
+ getEnglishSection() {
+ return content.document.getElementById("english-section");
+ },
+ getSpanishSection() {
+ return content.document.getElementById("spanish-section");
+ },
+ getFrenchSentence() {
+ return content.document.getElementById("french-sentence");
+ },
+ getEnglishSentence() {
+ return content.document.getElementById("english-sentence");
+ },
+ getSpanishSentence() {
+ return content.document.getElementById("spanish-sentence");
},
getEnglishHyperlink() {
return content.document.getElementById("english-hyperlink");
},
+ getFrenchHyperlink() {
+ return content.document.getElementById("french-hyperlink");
+ },
+ getSpanishHyperlink() {
+ return content.document.getElementById("spanish-hyperlink");
+ },
};
}
diff --git a/toolkit/components/translations/tests/browser/translations-tester-es.html b/toolkit/components/translations/tests/browser/translations-tester-es.html
index f589df0649..abf2d42c62 100644
--- a/toolkit/components/translations/tests/browser/translations-tester-es.html
+++ b/toolkit/components/translations/tests/browser/translations-tester-es.html
@@ -20,7 +20,7 @@
<header lang="en">The following is an excerpt from Don Quijote de la Mancha, which is in the public domain</header>
<h1>Don Quijote de La Mancha</h1>
<h2>Capítulo VIII.</h2>
- <p id="spanish-paragraph">Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p>
+ <p>Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p>
<p>En esto, descubrieron treinta o cuarenta molinos de viento que hay en aquel campo; y, así como don Quijote los vio, dijo a su escudero:</p>
<p>— La ventura va guiando nuestras cosas mejor de lo que acertáramos a desear, porque ves allí, amigo Sancho Panza, donde se descubren treinta, o pocos más, desaforados gigantes, con quien pienso hacer batalla y quitarles a todos las vidas, con cuyos despojos comenzaremos a enriquecer; que ésta es buena guerra, y es gran servicio de Dios quitar tan mala simiente de sobre la faz de la tierra.</p>
<p>— ¿Qué gigantes? —dijo Sancho Panza.</p>
@@ -32,13 +32,5 @@
<p>Levantóse en esto un poco de viento y las grandes aspas comenzaron a moverse, lo cual visto por don Quijote, dijo:</p>
<p>— Pues, aunque mováis más brazos que los del gigante Briareo, me lo habéis de pagar.</p>
</div>
- <div>
- <header lang="en">The following is a link to another test page in Spanish.</header>
- <p><a id="spanish-hyperlink" href="https://example.org/browser/translations-tester-es.html">Otra pagina en español.</a></p>
- </div>
- <div>
- <header lang="en">The following is a link to another test page in English.</header>
- <p lang="en"><a id="english-hyperlink" href="https://example.org/browser/translations-tester-en.html">Another page in English.</a></p>
- </div>
</body>
</html>
diff --git a/toolkit/components/translations/tests/browser/translations-tester-select.html b/toolkit/components/translations/tests/browser/translations-tester-select.html
new file mode 100644
index 0000000000..034d89a95b
--- /dev/null
+++ b/toolkit/components/translations/tests/browser/translations-tester-select.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+ <meta charset="utf-8" />
+ <title>Select Translations Test</title>
+ <style>
+ div {
+ margin: 10px auto;
+ width: 600px
+ }
+ p {
+ margin: 47px 0;
+ font-size: 21px;
+ line-height: 2;
+ }
+ </style>
+</head>
+<body>
+ <div>
+ <header lang="en">The following is an excerpt from Don Quijote de la Mancha, which is in the public domain</header>
+ <div id="spanish-section">
+ <h1>Don Quijote de La Mancha</h1>
+ <h2>Capítulo VIII.</h2>
+ <p id="spanish-sentence">Del buen suceso que el valeroso don Quijote tuvo en la espantable y jamás imaginada aventura de los molinos de viento, con otros sucesos dignos de felice recordación</p>
+ <p>En esto, descubrieron treinta o cuarenta molinos de viento que hay en aquel campo; y, así como don Quijote los vio, dijo a su escudero:</p>
+ <p>— La ventura va guiando nuestras cosas mejor de lo que acertáramos a desear, porque ves allí, amigo Sancho Panza, donde se descubren treinta, o pocos más, desaforados gigantes, con quien pienso hacer batalla y quitarles a todos las vidas, con cuyos despojos comenzaremos a enriquecer; que ésta es buena guerra, y es gran servicio de Dios quitar tan mala simiente de sobre la faz de la tierra.</p>
+ <p>— ¿Qué gigantes? —dijo Sancho Panza.</p>
+ <p>— Aquellos que allí ves —respondió su amo— de los brazos largos, que los suelen tener algunos de casi dos leguas.</p>
+ <p>— Mire vuestra merced —respondió Sancho— que aquellos que allí se parecen no son gigantes, sino molinos de viento, y lo que en ellos parecen brazos son las aspas, que, volteadas del viento, hacen andar la piedra del molino.</p>
+ <p>— Bien parece —respondió don Quijote— que no estás cursado en esto de las aventuras: ellos son gigantes; y si tienes miedo, quítate de ahí, y ponte en oración en el espacio que yo voy a entrar con ellos en fiera y desigual batalla.</p>
+ <p>Y, diciendo esto, dio de espuelas a su caballo Rocinante, sin atender a las voces que su escudero Sancho le daba, advirtiéndole que, sin duda alguna, eran molinos de viento, y no gigantes, aquellos que iba a acometer. Pero él iba tan puesto en que eran gigantes, que ni oía las voces de su escudero Sancho ni echaba de ver, aunque estaba ya bien cerca, lo que eran; antes, iba diciendo en voces altas:</p>
+ <p>— Non fuyades, cobardes y viles criaturas, que un solo caballero es el que os acomete.</p>
+ <p>Levantóse en esto un poco de viento y las grandes aspas comenzaron a moverse, lo cual visto por don Quijote, dijo:</p>
+ <p>— Pues, aunque mováis más brazos que los del gigante Briareo, me lo habéis de pagar.</p>
+ </div>
+ </div>
+ <div>
+ <header lang="en">The following is an excerpt from Frankenstein, which is in the public domain</header>
+ <div id="english-section">
+ <h1>Frankenstein</h1>
+ <h2>Letter 3</h2>
+ <p>To Mrs. Saville, England.</p>
+ <p>July 7th, 17—.</p>
+ <p>My dear Sister,</p>
+ <p>I write a few lines in haste to say that I am safe—and well advanced on my voyage. This letter will reach England by a merchantman now on its homeward voyage from Archangel; more fortunate than I, who may not see my native land, perhaps, for many years. I am, however, in good spirits: my men are bold and apparently firm of purpose, nor do the floating sheets of ice that continually pass us, indicating the dangers of the region towards which we are advancing, appear to dismay them. We have already reached a very high latitude; but it is the height of summer, and although not so warm as in England, the southern gales, which blow us speedily towards those shores which I so ardently desire to attain, breathe a degree of renovating warmth which I had not expected.</p>
+ <p>No incidents have hitherto befallen us that would make a figure in a letter. One or two stiff gales and the springing of a leak are accidents which experienced navigators scarcely remember to record, and I shall be well content if nothing worse happen to us during our voyage.</p>
+ <p>Adieu, my dear Margaret. Be assured that for my own sake, as well as yours, I will not rashly encounter danger. I will be cool, persevering, and prudent.</p>
+ <p>But success shall crown my endeavours. Wherefore not? Thus far I have gone, tracing a secure way over the pathless seas, the very stars themselves being witnesses and testimonies of my triumph. Why not still proceed over the untamed yet obedient element? What can stop the determined heart and resolved will of man?</p>
+ <p id="english-sentence">My swelling heart involuntarily pours itself out thus. But I must finish. Heaven bless my beloved sister!</p>
+ </div>
+ </div>
+ <div>
+ <header lang="en">The following is an excerpt from Les Misérables, which is in the public domain</header>
+ <div id="french-section">
+ <h1>Les Misérables</h1>
+ <h2>Chapitre II</h2>
+ <p>Monsieur Myriel devient monseigneur Bienvenu</p>
+ <p id="french-sentence">Le palais épiscopal de Digne était attenant à l'hôpital.</p>
+ <p>Le palais épiscopal était un vaste et bel hôtel bâti en pierre au commencement du siècle dernier par monseigneur Henri Puget, docteur en théologie de la faculté de Paris, abbé de Simore, lequel était évêque de Digne en 1712. Ce palais était un vrai logis seigneurial. Tout y avait grand air, les appartements de l'évêque, les salons, les chambres, la cour d'honneur, fort large, avec promenoirs à arcades, selon l'ancienne mode florentine, les jardins plantés de magnifiques arbres. Dans la salle à manger, longue et superbe galerie qui était au rez-de-chaussée et s'ouvrait sur les jardins, monseigneur Henri Puget avait donné à manger en cérémonie le 29 juillet 1714 à messeigneurs Charles Brûlart de Genlis, archevêque-prince d'Embrun, Antoine de Mesgrigny, capucin, évêque de Grasse, Philippe de Vendôme, grand prieur de France, abbé de Saint-Honoré de Lérins, François de Berton de Grillon, évêque-baron de Vence, César de Sabran de Forcalquier, évêque-seigneur de Glandève, et Jean Soanen, prêtre de l'oratoire, prédicateur ordinaire du roi, évêque-seigneur de Senez. Les portraits de ces sept révérends personnages décoraient cette salle, et cette date mémorable, 29 juillet 1714, y était gravée en lettres d'or sur une table de marbre blanc.</p>
+ <p>L'hôpital était une maison étroite et basse à un seul étage avec un petit jardin. Trois jours après son arrivée, l'évêque visita l'hôpital. La visite terminée, il fit prier le directeur de vouloir bien venir jusque chez lui.</p>
+ </div>
+ </div>
+ <div>
+ <header lang="en">The following is a link to another test page in Spanish.</header>
+ <p lang="es"><a id="spanish-hyperlink" href="https://example.org/browser/translations-tester-es.html">Otra pagina en español.</a></p>
+ </div>
+ <div>
+ <header lang="en">The following is a link to another test page in French.</header>
+ <p lang="fr"><a id="french-hyperlink" href="https://example.org/browser/translations-tester-fr.html">Une autre page en français.</a></p>
+ </div>
+ <div>
+ <header lang="en">The following is a link to another test page in English.</header>
+ <p lang="en"><a id="english-hyperlink" href="https://example.org/browser/translations-tester-en.html">Another page in English.</a></p>
+ </div>
+</body>
+</html>
diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts
index cc8d462a9c..9823f6a845 100644
--- a/toolkit/components/translations/translations.d.ts
+++ b/toolkit/components/translations/translations.d.ts
@@ -269,3 +269,10 @@ export interface SupportedLanguages {
}
export type TranslationErrors = "engine-load-error";
+
+export type SelectTranslationsPanelState =
+ | { phase: "closed"; }
+ | { phase: "idle"; fromLanguage: string; toLanguage: string, sourceText: string, }
+ | { phase: "translatable"; fromLanguage: string; toLanguage: string, sourceText: string, }
+ | { phase: "translating"; fromLanguage: string; toLanguage: string, sourceText: string, }
+ | { phase: "translated"; fromLanguage: string; toLanguage: string, sourceText: string, translatedText: string, }