From da4c7e7ed675c3bf405668739c3012d140856109 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:42 +0200 Subject: Adding upstream version 126.0. Signed-off-by: Daniel Baumann --- .../components/translations/content/Translator.mjs | 227 +++++++++++++++++++++ .../translations/content/translations.mjs | 201 ++++-------------- 2 files changed, 268 insertions(+), 160 deletions(-) create mode 100644 toolkit/components/translations/content/Translator.mjs (limited to 'toolkit/components/translations/content') 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} + */ + #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} + */ + #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} 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} + */ + 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} - 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} + */ + 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} + * @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} - */ - #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} - */ - 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} - */ - 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(); - } -} -- cgit v1.2.3