summaryrefslogtreecommitdiffstats
path: root/toolkit/components/translations/content/Translator.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/translations/content/Translator.mjs')
-rw-r--r--toolkit/components/translations/content/Translator.mjs227
1 files changed, 227 insertions, 0 deletions
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;
+ }
+}