From 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:13:33 +0200 Subject: Merging upstream version 125.0.1. Signed-off-by: Daniel Baumann --- .../translations/TranslationsTelemetry.sys.mjs | 3 + .../actors/AboutTranslationsChild.sys.mjs | 12 +- .../actors/AboutTranslationsParent.sys.mjs | 6 +- .../translations/actors/TranslationsChild.sys.mjs | 1 + .../actors/TranslationsEngineChild.sys.mjs | 6 +- .../actors/TranslationsEngineParent.sys.mjs | 13 +- .../translations/actors/TranslationsParent.sys.mjs | 219 ++++++++------------- .../content/translations-document.sys.mjs | 74 ++++--- .../content/translations-engine.sys.mjs | 8 +- .../translations/content/translations.mjs | 7 +- .../browser_translations_translation_document.js | 100 ++++++++++ .../translations/tests/browser/shared-head.js | 142 ++++++------- .../tests/browser/translations-test.mjs | 2 +- toolkit/components/translations/translations.d.ts | 2 +- 14 files changed, 331 insertions(+), 264 deletions(-) (limited to 'toolkit/components/translations') diff --git a/toolkit/components/translations/TranslationsTelemetry.sys.mjs b/toolkit/components/translations/TranslationsTelemetry.sys.mjs index f01ee1d144..8550a1c1af 100644 --- a/toolkit/components/translations/TranslationsTelemetry.sys.mjs +++ b/toolkit/components/translations/TranslationsTelemetry.sys.mjs @@ -41,6 +41,7 @@ export class TranslationsTelemetry { /** * Telemetry functions for the Translations panel. + * * @returns {Panel} */ static panel() { @@ -49,6 +50,7 @@ export class TranslationsTelemetry { /** * Forces the creation of a new Translations telemetry flowId and returns it. + * * @returns {string} */ static createFlowId() { @@ -60,6 +62,7 @@ export class TranslationsTelemetry { /** * Returns a Translations telemetry flowId by retrieving the cached value * if available, or creating a new one otherwise. + * * @returns {string} */ static getOrCreateFlowId() { diff --git a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs index c501c1b0cd..9d0b27a6a1 100644 --- a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs +++ b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs @@ -83,17 +83,6 @@ export class AboutTranslationsChild extends JSWindowActorChild { ); } - /** - * @returns {TranslationsChild} - */ - #getTranslationsChild() { - const child = this.contentWindow.windowGlobalChild.getActor("Translations"); - if (!child) { - throw new Error("Unable to find the TranslationsChild"); - } - return child; - } - /** * A privileged promise can't be used in the content page, so convert a privileged * promise into a content one. @@ -194,6 +183,7 @@ export class AboutTranslationsChild extends JSWindowActorChild { /** * Does this device support the translation engine? + * * @returns {Promise} */ AT_isTranslationEngineSupported() { diff --git a/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs index 4680dbeef5..236e17bbc3 100644 --- a/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/AboutTranslationsParent.sys.mjs @@ -5,6 +5,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", + EngineProcess: "chrome://global/content/ml/EngineProcess.sys.mjs", }); /** @@ -22,12 +23,13 @@ export class AboutTranslationsParent extends JSWindowActorParent { switch (name) { case "AboutTranslations:GetTranslationsPort": { const { fromLanguage, toLanguage } = data; - const engineProcess = await lazy.TranslationsParent.getEngineProcess(); + const translationsEngineParent = + await lazy.EngineProcess.getTranslationsEngineParent(); if (this.#isDestroyed) { return undefined; } const { port1, port2 } = new MessageChannel(); - engineProcess.actor.startTranslation( + translationsEngineParent.startTranslation( fromLanguage, toLanguage, port1, diff --git a/toolkit/components/translations/actors/TranslationsChild.sys.mjs b/toolkit/components/translations/actors/TranslationsChild.sys.mjs index a3f8d15c85..d318b7284f 100644 --- a/toolkit/components/translations/actors/TranslationsChild.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsChild.sys.mjs @@ -24,6 +24,7 @@ export class TranslationsChild extends JSWindowActorChild { /** * This cache is shared across TranslationsChild instances. This means * that it will be shared across multiple page loads in the same origin. + * * @type {LRUCache | null} */ static #translationsCache = null; diff --git a/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs b/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs index a4ab8e2640..d638858e52 100644 --- a/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsEngineChild.sys.mjs @@ -19,6 +19,7 @@ export class TranslationsEngineChild extends JSWindowActorChild { /** * The resolve function for the Promise returned by the * "TranslationsEngine:ForceShutdown" message. + * * @type {null | () => {}} */ #resolveForceShutdown = null; @@ -130,9 +131,10 @@ export class TranslationsEngineChild extends JSWindowActorChild { } /** - * @param {Object} options + * @param {object} options * @param {number?} options.startTime * @param {string} options.message + * @param {number} options.innerWindowId */ TE_addProfilerMarker({ startTime, message, innerWindowId }) { ChromeUtils.addProfilerMarker( @@ -199,7 +201,7 @@ export class TranslationsEngineChild extends JSWindowActorChild { } /** - * No engines are still alive, destroy the process. + * No engines are still alive, signal that the process can be destroyed. */ TE_destroyEngineProcess() { this.sendAsyncMessage("TranslationsEngine:DestroyEngineProcess"); diff --git a/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs b/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs index 77b16d7ae9..0b35117d7a 100644 --- a/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsEngineParent.sys.mjs @@ -5,6 +5,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", + EngineProcess: "chrome://global/content/ml/EngineProcess.sys.mjs", }); /** @@ -22,12 +23,12 @@ export class TranslationsEngineParent extends JSWindowActorParent { async receiveMessage({ name, data }) { switch (name) { case "TranslationsEngine:Ready": - if (!lazy.TranslationsParent.resolveEngine) { + if (!lazy.EngineProcess.resolveTranslationsEngineParent) { throw new Error( "Unable to find the resolve function for when the translations engine is ready." ); } - lazy.TranslationsParent.resolveEngine(this); + lazy.EngineProcess.resolveTranslationsEngineParent(this); return undefined; case "TranslationsEngine:RequestEnginePayload": { const { fromLanguage, toLanguage } = data; @@ -62,12 +63,7 @@ export class TranslationsEngineParent extends JSWindowActorParent { return undefined; } case "TranslationsEngine:DestroyEngineProcess": - ChromeUtils.addProfilerMarker( - "TranslationsEngine", - {}, - "Loading bergamot wasm array buffer" - ); - lazy.TranslationsParent.destroyEngineProcess().catch(error => + lazy.EngineProcess.destroyTranslationsEngine().catch(error => console.error(error) ); return undefined; @@ -79,7 +75,6 @@ export class TranslationsEngineParent extends JSWindowActorParent { /** * @param {string} fromLanguage * @param {string} toLanguage - * @param {number} innerWindowId * @param {MessagePort} port * @param {number} innerWindowId * @param {TranslationsParent} [translationsParent] diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs index 44b761e6b0..70754d95c4 100644 --- a/toolkit/components/translations/actors/TranslationsParent.sys.mjs +++ b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -58,7 +58,7 @@ ChromeUtils.defineESModuleGetters(lazy, { setTimeout: "resource://gre/modules/Timer.sys.mjs", TranslationsTelemetry: "chrome://global/content/translations/TranslationsTelemetry.sys.mjs", - HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs", + EngineProcess: "chrome://global/content/ml/EngineProcess.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "console", () => { @@ -142,11 +142,11 @@ const VERIFY_SIGNATURES_FROM_FS = false; */ /** - * @typedef {Object} TranslationPair - * @prop {string} fromLanguage - * @prop {string} toLanguage - * @prop {string} [fromDisplayLanguage] - * @prop {string} [toDisplayLanguage] + * @typedef {object} TranslationPair + * @property {string} fromLanguage + * @property {string} toLanguage + * @property {string} [fromDisplayLanguage] + * @property {string} [toDisplayLanguage] */ /** @@ -332,6 +332,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Telemetry functions for Translations + * * @returns {TranslationsTelemetry} */ static telemetry() { @@ -348,109 +349,10 @@ export class TranslationsParent extends JSWindowActorParent { ); } - /** - * @type {Promise<{ hiddenFrame: HiddenFrame, actor: TranslationsEngineParent }> | null} - */ - static #engine = null; - - static async getEngineProcess() { - if (!TranslationsParent.#engine) { - TranslationsParent.#engine = TranslationsParent.#getEngineProcessImpl(); - } - const enginePromise = TranslationsParent.#engine; - - // Determine if the actor was destroyed, or if there was an error. In this case - // attempt to rebuild the process. - let needsRebuilding = true; - try { - const { actor } = await enginePromise; - needsRebuilding = actor.isDestroyed; - } catch {} - - if ( - TranslationsParent.#engine && - enginePromise !== TranslationsParent.#engine - ) { - // This call lost the race, something else updated the engine promise, return that. - return TranslationsParent.#engine; - } - - if (needsRebuilding) { - // The engine was destroyed, attempt to re-create the engine process. - const rebuild = TranslationsParent.destroyEngineProcess().then(() => - TranslationsParent.#getEngineProcessImpl() - ); - TranslationsParent.#engine = rebuild; - return rebuild; - } - - return enginePromise; - } - - static destroyEngineProcess() { - const enginePromise = this.#engine; - this.#engine = null; - if (enginePromise) { - ChromeUtils.addProfilerMarker( - "TranslationsParent", - {}, - "Destroying the translations engine process" - ); - return enginePromise.then(({ actor, hiddenFrame }) => - actor - .forceShutdown() - .catch(error => { - lazy.console.error( - "There was an error shutting down the engine.", - error - ); - }) - .then(() => { - hiddenFrame.destroy(); - }) - ); - } - return Promise.resolve(); - } - - /** - * @type {Promise<{ hiddenFrame: HiddenFrame, actor: TranslationsEngineParent }> | null} - */ - static async #getEngineProcessImpl() { - ChromeUtils.addProfilerMarker( - "TranslationsParent", - {}, - "Creating the translations engine process" - ); - - // Manages the hidden ChromeWindow. - const hiddenFrame = new lazy.HiddenFrame(); - const chromeWindow = await hiddenFrame.get(); - const doc = chromeWindow.document; - - const actorPromise = new Promise(resolve => { - this.resolveEngine = resolve; - }); - - const browser = doc.createXULElement("browser"); - browser.setAttribute("remote", "true"); - browser.setAttribute("remoteType", "web"); - browser.setAttribute("disableglobalhistory", "true"); - browser.setAttribute("type", "content"); - browser.setAttribute( - "src", - "chrome://global/content/translations/translations-engine.html" - ); - doc.documentElement.appendChild(browser); - - const actor = await actorPromise; - this.resolveEngine = null; - return { hiddenFrame, browser, actor }; - } - /** * Offer translations (for instance by automatically opening the popup panel) whenever * languages are detected, but only do it once per host per session. + * * @param {LangTags} detectedLanguages */ maybeOfferTranslations(detectedLanguages) { @@ -559,10 +461,6 @@ export class TranslationsParent extends JSWindowActorParent { detectedLanguages ); - TranslationsParent.getEngineProcess().catch(error => - console.error(error) - ); - browser.dispatchEvent( new CustomEvent("TranslationsParent:OfferTranslation", { bubbles: true, @@ -616,7 +514,7 @@ export class TranslationsParent extends JSWindowActorParent { * about:* pages will not be translated. Keep this logic up to date with the "matches" * array in the `toolkit/modules/ActorManagerParent.sys.mjs` definition. * - * @param {string} scheme - The URI spec + * @param {object} gBrowser * @returns {boolean} */ static isRestrictedPage(gBrowser) { @@ -657,6 +555,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Provide a way for tests to override the system locales. + * * @type {null | string[]} */ static mockedSystemLocales = null; @@ -814,9 +713,9 @@ export class TranslationsParent extends JSWindowActorParent { return undefined; } - let engineProcess; + let actor; try { - engineProcess = await TranslationsParent.getEngineProcess(); + actor = await lazy.EngineProcess.getTranslationsEngineParent(); } catch (error) { console.error("Failed to get the translation engine process", error); return undefined; @@ -836,7 +735,7 @@ export class TranslationsParent extends JSWindowActorParent { // The MessageChannel will be used for communicating directly between the content // process and the engine's process. const { port1, port2 } = new MessageChannel(); - engineProcess.actor.startTranslation( + actor.startTranslation( requestedTranslationPair.fromLanguage, requestedTranslationPair.toLanguage, port1, @@ -952,6 +851,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * The cached language pairs. + * * @type {Promise> | null} */ static #languagePairs = null; @@ -1041,8 +941,8 @@ export class TranslationsParent extends JSWindowActorParent { /** * Create a unique list of languages, sorted by the display name. * - * @param {Object} supportedLanguages - * @returns {Array<{ langTag: string, displayName: string}} + * @param {object} supportedLanguages + * @returns {Array<{ langTag: string, displayName: string}>} */ static getLanguageList(supportedLanguages) { const displayNames = new Map(); @@ -1070,8 +970,8 @@ export class TranslationsParent extends JSWindowActorParent { } /** - * @param {Object} event - * @param {Object} event.data + * @param {object} event + * @param {object} event.data * @param {TranslationModelRecord[]} event.data.created * @param {TranslationModelRecord[]} event.data.updated * @param {TranslationModelRecord[]} event.data.deleted @@ -1147,12 +1047,13 @@ export class TranslationsParent extends JSWindowActorParent { * then only the 1.1-version record will be returned in the resulting collection. * * @param {RemoteSettingsClient} remoteSettingsClient - * @param {Object} [options] - * @param {Object} [options.filters={}] + * @param {object} [options] + * @param {object} [options.filters={}] * The filters to apply when retrieving the records from RemoteSettings. * Filters should correspond to properties on the RemoteSettings records themselves. * For example, A filter to retrieve only records with a `fromLang` value of "en" and a `toLang` value of "es": * { filters: { fromLang: "en", toLang: "es" } } + * @param {number} options.majorVersion * @param {Function} [options.lookupKey=(record => record.name)] * The function to use to extract a lookup key from each record. * This function should take a record as input and return a string that represents the lookup key for the record. @@ -1537,7 +1438,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Deletes language files that match a language. * - * @param {string} requestedLanguage The BCP 47 language tag. + * @param {string} language The BCP 47 language tag. */ static async deleteLanguageFiles(language) { const client = TranslationsParent.#getTranslationModelsRemoteClient(); @@ -1558,7 +1459,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Download language files that match a language. * - * @param {string} requestedLanguage The BCP 47 language tag. + * @param {string} language The BCP 47 language tag. */ static async downloadLanguageFiles(language) { const client = TranslationsParent.#getTranslationModelsRemoteClient(); @@ -1607,6 +1508,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Delete all language model files. + * * @returns {Promise} A list of record IDs. */ static async deleteAllLanguageFiles() { @@ -1867,8 +1769,10 @@ export class TranslationsParent extends JSWindowActorParent { * @param {string} fromLanguage * @param {string} toLanguage * @param {boolean} withQualityEstimation - * @returns {Promise<{downloadSize: long, modelFound: boolean}> Download size is the size in bytes of the estimated download for display purposes. Model found indicates a model was found. - * e.g., a result of {size: 0, modelFound: false} indicates no bytes to download, because a model wasn't located. + * @returns {Promise<{downloadSize: long, modelFound: boolean}>} Download size is the + * size in bytes of the estimated download for display purposes. Model found indicates + * a model was found. e.g., a result of {size: 0, modelFound: false} indicates no + * bytes to download, because a model wasn't located. */ static async #getModelDownloadSize( fromLanguage, @@ -1964,6 +1868,7 @@ export class TranslationsParent extends JSWindowActorParent { /** * Report an error. Having this as a method allows tests to check that an error * was properly reported. + * * @param {Error} error - Providing an Error object makes sure the stack is properly * reported. * @param {any[]} args - Any args to pass on to console.error. @@ -2001,9 +1906,9 @@ export class TranslationsParent extends JSWindowActorParent { } else { const { docLangTag } = this.languageState.detectedLanguages; - let engineProcess; + let actor; try { - engineProcess = await TranslationsParent.getEngineProcess(); + actor = await lazy.EngineProcess.getTranslationsEngineParent(); } catch (error) { console.error("Failed to get the translation engine process", error); return; @@ -2018,7 +1923,7 @@ export class TranslationsParent extends JSWindowActorParent { // The MessageChannel will be used for communicating directly between the content // process and the engine's process. const { port1, port2 } = new MessageChannel(); - engineProcess.actor.startTranslation( + actor.startTranslation( fromLanguage, toLanguage, port1, @@ -2151,6 +2056,46 @@ export class TranslationsParent extends JSWindowActorParent { return false; } + /** + * Checks if a given language tag is supported for translation + * when translating from this language into other languages. + * + * @param {string} langTag - A BCP-47 language tag. + * @returns {Promise} + */ + static async isSupportedAsFromLang(langTag) { + if (!langTag) { + return false; + } + let languagePairs = await TranslationsParent.getLanguagePairs(); + return Boolean(languagePairs.find(({ fromLang }) => fromLang === langTag)); + } + + /** + * Checks if a given language tag is supported for translation + * when translating from other languages into this language. + * + * @param {string} langTag - A BCP-47 language tag. + * @returns {Promise} + */ + static async isSupportedAsToLang(langTag) { + if (!langTag) { + return false; + } + let languagePairs = await TranslationsParent.getLanguagePairs(); + return Boolean(languagePairs.find(({ fromLang }) => fromLang === langTag)); + } + + /** + * Retrieves the top preferred user language for which translation + * is supported when translating to that language. + */ + static async getTopPreferredSupportedToLang() { + return TranslationsParent.getPreferredLanguages().find( + async langTag => await TranslationsParent.isSupportedAsToLang(langTag) + ); + } + /** * Returns the lang tags that should be offered for translation. This is in the parent * rather than the child to remove the per-content process memory allocation amount. @@ -2584,15 +2529,15 @@ export class TranslationsParent extends JSWindowActorParent { * are misbehaving. */ #ensureTranslationsDiscarded() { - if (!TranslationsParent.#engine) { + if (!lazy.EngineProcess.translationsEngineParent) { return; } - TranslationsParent.#engine + lazy.EngineProcess.translationsEngineParent // If the engine fails to load, ignore it since we are ending translations. .catch(() => null) - .then(engineProcess => { - if (engineProcess && this.languageState.requestedTranslationPair) { - engineProcess.actor.discardTranslations(this.innerWindowId); + .then(actor => { + if (actor && this.languageState.requestedTranslationPair) { + actor.discardTranslations(this.innerWindowId); } }) // This error will be one from the endTranslation code, which we need to @@ -2804,11 +2749,11 @@ class TranslationsLanguageState { } /** - * @typedef {Object} QueueItem - * @prop {Function} download - * @prop {Function} [onSuccess] - * @prop {Function} [onFailure] - * @prop {number} [retriesLeft] + * @typedef {object} QueueItem + * @property {Function} download + * @property {Function} [onSuccess] + * @property {Function} [onFailure] + * @property {number} [retriesLeft] */ /** diff --git a/toolkit/components/translations/content/translations-document.sys.mjs b/toolkit/components/translations/content/translations-document.sys.mjs index 7f436575d8..ed75fe9ec6 100644 --- a/toolkit/components/translations/content/translations-document.sys.mjs +++ b/toolkit/components/translations/content/translations-document.sys.mjs @@ -254,7 +254,7 @@ export class TranslationsDocument { /** * The BCP 47 language tag that is used on the page. * - * @type {string} */ + @type {string} */ documentLanguage; /** @@ -292,7 +292,7 @@ export class TranslationsDocument { * The list of nodes that need updating with the translated HTML. These are batched * into an update. * - * @type {Set<{ node: Node, translatedHTML: string }} + * @type {Set<{ node: Node, translatedHTML: string }>} */ #nodesWithTranslatedHTML = new Set(); @@ -300,7 +300,7 @@ export class TranslationsDocument { * The list of nodes that need updating with the translated Attribute HTML. These are batched * into an update. * - * @type {Set<{ node: Node, translation: string, attribute: string }} + * @type {Set<{ node: Node, translation: string, attribute: string }>} */ #nodesWithTranslatedAttributes = new Set(); @@ -475,8 +475,9 @@ export class TranslationsDocument { /** * Queue a node for translation of attributes. + * * @param {Node} node - * @param {Array} + * @param {Array} attributeList */ queueAttributeNodeForTranslation(node, attributeList) { /** @type {NodeVisibility} */ @@ -522,6 +523,7 @@ export class TranslationsDocument { /** * Helper function for adding a new root to the mutation * observer. + * * @param {Node} root */ observeNewRoot(root) { @@ -689,6 +691,7 @@ export class TranslationsDocument { * Get all the nodes which have selected attributes * from the node/document and queue them. * Call the translate function on these nodes + * * @param {Node} node * @returns {Array> | null} */ @@ -773,7 +776,7 @@ export class TranslationsDocument { * Runs `determineTranslationStatus`, but only on unprocessed nodes. * * @param {Node} node - * @return {number} - One of the NodeStatus values. + * @returns {number} - One of the NodeStatus values. */ determineTranslationStatusForUnprocessedNodes = node => { if (this.#processedNodes.has(node)) { @@ -840,6 +843,7 @@ export class TranslationsDocument { /** * Queue a node for translation. + * * @param {Node} node */ queueNodeForTranslation(node) { @@ -856,6 +860,7 @@ export class TranslationsDocument { /** * Submit the translations giving priority to nodes in the viewport. + * * @returns {Array> | null} */ dispatchQueuedTranslations() { @@ -905,6 +910,7 @@ export class TranslationsDocument { /** * Submit the Attribute translations giving priority to nodes in the viewport. + * * @returns {Array> | null} */ dispatchQueuedAttributeTranslations() { @@ -1124,9 +1130,10 @@ export class TranslationsDocument { /** * A single function to update pendingTranslationsCount while * calling the translate function + * * @param {Node} node * @param {string} text - * @prop {boolean} isHTML + * @property {boolean} isHTML * @returns {Promise} */ async maybeTranslate(node, text, isHTML) { @@ -1223,6 +1230,7 @@ export class TranslationsDocument { /** * Stop the mutations so that the updates of the translations * in the nodes won't trigger observations. + * * @param {Function} run The function to update translations */ pauseMutationObserverAndRun(run) { @@ -1286,7 +1294,8 @@ export class TranslationsDocument { /** * Get the list of attributes that need to be translated * in a given node. - * @returns Array + * + * @returns {Array} */ function getTranslatableAttributes(node) { if (node.nodeType !== Node.ELEMENT_NODE) { @@ -1342,7 +1351,7 @@ function langTagsMatch(knownLanguage, otherLanguage) { * style of node. * * @param {Node} node - * @returns {HTMLElement} */ + @returns {HTMLElement} */ function getElementForStyle(node) { if (node.nodeType != Node.TEXT_NODE) { return node; @@ -1424,6 +1433,7 @@ function updateElement(translationsDocument, element) { /** * The Set of translation IDs for nodes that have been cloned. + * * @type {Set} */ const clonedNodes = new Set(); @@ -1649,6 +1659,7 @@ function removeTextNodes(node) { * - `

test

`: yes * - `

`: no * - `

test

`: no + * * @param {Node} node * @returns {boolean} */ @@ -1723,18 +1734,21 @@ function isNodeQueued(node, queuedNodes) { } /** - * Reads the elements computed style and determines if the element is inline or not. + * Reads the elements computed style and determines if the element is a block-like + * element or not. Every element that lays out like a block should be sent in as one + * cohesive unit to be translated. * * @param {Element} element */ -function getIsInline(element) { +function getIsBlockLike(element) { const win = element.ownerGlobal; if (element.namespaceURI === "http://www.w3.org/2000/svg") { // SVG elements will report as inline, but there is no block layout in SVG. // Treat every SVG element as being block so that every node will be subdivided. - return false; + return true; } - return win.getComputedStyle(element).display === "inline"; + const { display } = win.getComputedStyle(element); + return display !== "inline" && display !== "none"; } /** @@ -1751,7 +1765,8 @@ function nodeNeedsSubdividing(node) { return false; } - if (getIsInline(node)) { + if (!getIsBlockLike(node)) { + // This element is inline, or not displayed. return false; } @@ -1761,12 +1776,12 @@ function nodeNeedsSubdividing(node) { // Keep checking for more inline or text nodes. continue; case Node.ELEMENT_NODE: { - if (getIsInline(child)) { - // Keep checking for more inline or text nodes. - continue; + if (getIsBlockLike(child)) { + // This node is a block node, so it needs further subdividing. + return true; } - // A child element is not inline, so subdivide this node further. - return true; + // Keep checking for more inline or text nodes. + continue; } default: return true; @@ -1795,12 +1810,12 @@ function* getAncestorsIterator(node) { /** * This contains all of the information needed to perform a translation request. * - * @typedef {Object} TranslationRequest - * @prop {Node} node - * @prop {string} sourceText - * @prop {boolean} isHTML - * @prop {Function} resolve - * @prop {Function} reject + * @typedef {object} TranslationRequest + * @property {Node} node + * @property {string} sourceText + * @property {boolean} isHTML + * @property {Function} resolve + * @property {Function} reject */ /** @@ -1827,13 +1842,15 @@ class QueuedTranslator { /** * Tie together a message id to a resolved response. - * @type {Map} */ #requests = new Map(); /** * If the translations are paused, they are queued here. This Map is ordered by * from oldest to newest requests with stale requests being removed. + * * @type {Map} */ #queue = new Map(); @@ -1845,7 +1862,6 @@ class QueuedTranslator { /** * @param {MessagePort} port - * @param {Document} document * @param {() => void} actorRequestNewPort */ constructor(port, actorRequestNewPort) { @@ -1902,6 +1918,7 @@ class QueuedTranslator { /** * Request a new port. The port will come in via `acquirePort`, and then resolved * through the `this.#portRequest.resolve`. + * * @returns {Promise} */ #requestNewPort() { @@ -1956,7 +1973,7 @@ class QueuedTranslator { * then the request is stale. A rejection means there was an error in the translation. * This request may be queued. * - * @param {node} Node + * @param {Node} node * @param {string} sourceText * @param {boolean} isHTML */ @@ -1997,7 +2014,7 @@ class QueuedTranslator { * @param {Node} node * @param {string} sourceText * @param {boolean} isHTML - * @return {{ translateText: TranslationFunction, translateHTML: TranslationFunction}} + * @returns {{ translateText: TranslationFunction, translateHTML: TranslationFunction}} */ #postTranslationRequest(node, sourceText, isHTML) { return new Promise((resolve, reject) => { @@ -2049,6 +2066,7 @@ class QueuedTranslator { /** * Acquires a port, checks on the engine status, and then starts or resumes * translations. + * * @param {MessagePort} port */ acquirePort(port) { diff --git a/toolkit/components/translations/content/translations-engine.sys.mjs b/toolkit/components/translations/content/translations-engine.sys.mjs index e9aeb8076b..72b5757e21 100644 --- a/toolkit/components/translations/content/translations-engine.sys.mjs +++ b/toolkit/components/translations/content/translations-engine.sys.mjs @@ -150,6 +150,7 @@ export class TranslationsEngine { /** * Removes the engine, and if it's the last, call the process to destroy itself. + * * @param {string} languagePairKey * @param {boolean} force - On forced shutdowns, it's not necessary to notify the * parent process. @@ -207,6 +208,7 @@ export class TranslationsEngine { /** * Terminates the engine and its worker after a timeout. + * * @param {boolean} force */ terminate = (force = false) => { @@ -419,7 +421,8 @@ function getLanguagePairKey(fromLanguage, toLanguage) { /** * Maps the innerWindowId to the port. - * @type {Map} */ const ports = new Map(); @@ -427,6 +430,7 @@ const ports = new Map(); * Listen to the port to the content process for incoming messages, and pass * them to the TranslationsEngine manager. The other end of the port is held * in the content process by the TranslationsDocument. + * * @param {string} fromLanguage * @param {string} toLanguage * @param {number} innerWindowId @@ -511,7 +515,7 @@ function listenForPortMessages(fromLanguage, toLanguage, innerWindowId, port) { /** * Discards the queue and removes the port. * - * @param {innerWindowId} number + * @param {number} innerWindowId */ function discardTranslations(innerWindowId) { TE_log("Discarding translations, innerWindowId:", innerWindowId); diff --git a/toolkit/components/translations/content/translations.mjs b/toolkit/components/translations/content/translations.mjs index 0ec8b2d475..478f854bb5 100644 --- a/toolkit/components/translations/content/translations.mjs +++ b/toolkit/components/translations/content/translations.mjs @@ -57,6 +57,7 @@ class TranslationsState { /** * Only send one translation in at a time to the worker. + * * @type {Promise} */ translationRequest = Promise.resolve([]); @@ -75,6 +76,7 @@ class TranslationsState { constructor(isSupported) { /** * Is the engine supported by the device? + * * @type {boolean} */ this.isTranslationEngineSupported = isSupported; @@ -607,7 +609,7 @@ window.addEventListener("AboutTranslationsChromeToContent", ({ detail }) => { * Debounce a function so that it is only called after some wait time with no activity. * This is good for grouping text entry via keyboard. * - * @param {Object} settings + * @param {object} settings * @param {Function} settings.onDebounce * @param {Function} settings.doEveryTime * @returns {Function} @@ -671,7 +673,8 @@ class Translator { /** * Tie together a message id to a resolved response. - * @type {Map} */ #requests = new Map(); diff --git a/toolkit/components/translations/tests/browser/browser_translations_translation_document.js b/toolkit/components/translations/tests/browser/browser_translations_translation_document.js index 9a00da9ccf..d3d56fd387 100644 --- a/toolkit/components/translations/tests/browser/browser_translations_translation_document.js +++ b/toolkit/components/translations/tests/browser/browser_translations_translation_document.js @@ -51,6 +51,7 @@ async function createDoc(html, options) { /** * Test utility to check that the document matches the expected markup * + * @param {string} message * @param {string} html */ async function htmlMatches(message, html) { @@ -720,6 +721,105 @@ add_task(async function test_presumed_inlines3() { cleanup(); }); +/** + * Test the display "none" properties properly subdivide in block elements. + */ +add_task(async function test_display_none() { + const { translate, htmlMatches, cleanup } = await createDoc( + /* html */ ` +

+ This is some text. + It has inline elements + +

+ `, + { mockedTranslatorPort: createBatchedMockedTranslatorPort() } + ); + + translate(); + + // Note: The bergamot translator does not translate style elements, while our fake + // translator does translate the inside of style elements. That is why in the assertion + // here the style element is blank rather than containing style. + await htmlMatches( + "Display none", + /* html */ ` +

+ aaaa aa aaaa aaaa. + + aa aaa aaaaaa aaaaaaaa + + +

+ ` + ); + + cleanup(); +}); + +/** + * Test the display "none" properties properly subdivide in block elements. + * + * TODO - See Bug 1885235 + * + * This assertion is wrong, as our test suite doesn't properly compute the style for + * elements. The div with "display; none;" is still block, not "none". + */ +add_task(async function test_display_none_div() { + const { translate, htmlMatches, cleanup } = await createDoc( + /* html */ ` +
+ + Start of inline text + +
+ hidden portion of +
+ + rest of inline text. + +
+ `, + { mockedTranslatorPort: createBatchedMockedTranslatorPort() } + ); + + translate(); + + // eslint-disable-next-line no-unused-vars + const _realExpectedResults = /* html */ ` +
+ + aaaaa aa aaaaaa aaaa + +
+ aaaaaa aaaaaaa aa +
+ + aaaa aa aaaaaa aaaa. + +
+ `; + + const currentResults = /* html */ ` +
+ + aaaaa aa aaaaaa aaaa + +
+ bbbbbb bbbbbbb bb +
+ + cccc cc cccccc cccc. + +
+ `; + + await htmlMatches("Display none", currentResults); + + cleanup(); +}); + add_task(async function test_chunking_large_text() { const { translate, htmlMatches, cleanup } = await createDoc( /* html */ ` diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js index bad8e48a1b..82b3e783a7 100644 --- a/toolkit/components/translations/tests/browser/shared-head.js +++ b/toolkit/components/translations/tests/browser/shared-head.js @@ -3,6 +3,13 @@ "use strict"; +/** + * @type {import("../../../ml/content/EngineProcess.sys.mjs")} + */ +const { EngineProcess } = ChromeUtils.importESModule( + "chrome://global/content/ml/EngineProcess.sys.mjs" +); + // Avoid about:blank's non-standard behavior. const BLANK_PAGE = "data:text/html;charset=utf-8,BlankBlank page"; @@ -134,7 +141,7 @@ async function openAboutTranslations({ BrowserTestUtils.removeTab(tab); await removeMocks(); - await TranslationsParent.destroyEngineProcess(); + await EngineProcess.destroyTranslationsEngine(); await SpecialPowers.popPrefEnv(); } @@ -142,6 +149,7 @@ async function openAboutTranslations({ /** * Naively prettify's html based on the opening and closing tags. This is not robust * for general usage, but should be adequate for these tests. + * * @param {string} html * @returns {string} */ @@ -340,11 +348,23 @@ function getTranslationsParent() { } /** - * Closes the context menu if it is open. + * Closes all open panels and menu popups related to Translations. */ -function closeContextMenuIfOpen() { - return waitForCondition(async () => { - const contextMenu = document.getElementById("contentAreaContextMenu"); +async function closeAllOpenPanelsAndMenus() { + await closeSettingsMenuIfOpen(); + await closeFullPageTranslationsPanelIfOpen(); + await closeSelectTranslationsPanelIfOpen(); + await closeContextMenuIfOpen(); +} + +/** + * Closes the popup element with the given Id if it is open. + * + * @param {string} popupElementId + */ +async function closePopupIfOpen(popupElementId) { + await waitForCondition(async () => { + const contextMenu = document.getElementById(popupElementId); if (!contextMenu) { return true; } @@ -361,51 +381,32 @@ function closeContextMenuIfOpen() { }); } +/** + * Closes the context menu if it is open. + */ +async function closeContextMenuIfOpen() { + await closePopupIfOpen("contentAreaContextMenu"); +} + /** * Closes the translations panel settings menu if it is open. */ -function closeSettingsMenuIfOpen() { - return waitForCondition(async () => { - const settings = document.getElementById( - "translations-panel-settings-menupopup" - ); - if (!settings) { - return true; - } - if (settings.state === "closed") { - return true; - } - let popuphiddenPromise = BrowserTestUtils.waitForEvent( - settings, - "popuphidden" - ); - PanelMultiView.hidePopup(settings); - await popuphiddenPromise; - return false; - }); +async function closeSettingsMenuIfOpen() { + await closePopupIfOpen("full-page-translations-panel-settings-menupopup"); } /** * Closes the translations panel if it is open. */ -async function closeTranslationsPanelIfOpen() { - await closeSettingsMenuIfOpen(); - return waitForCondition(async () => { - const panel = document.getElementById("translations-panel"); - if (!panel) { - return true; - } - if (panel.state === "closed") { - return true; - } - let popuphiddenPromise = BrowserTestUtils.waitForEvent( - panel, - "popuphidden" - ); - PanelMultiView.hidePopup(panel); - await popuphiddenPromise; - return false; - }); +async function closeFullPageTranslationsPanelIfOpen() { + await closePopupIfOpen("full-page-translations-panel"); +} + +/** + * Closes the translations panel if it is open. + */ +async function closeSelectTranslationsPanelIfOpen() { + await closePopupIfOpen("select-translations-panel"); } /** @@ -442,10 +443,9 @@ async function setupActorTest({ actor, remoteClients, async cleanup() { + await closeAllOpenPanelsAndMenus(); await loadBlankPage(); - await TranslationsParent.destroyEngineProcess(); - await closeTranslationsPanelIfOpen(); - await closeContextMenuIfOpen(); + await EngineProcess.destroyTranslationsEngine(); BrowserTestUtils.removeTab(tab); await removeMocks(); TestTranslationsTelemetry.reset(); @@ -500,7 +500,7 @@ async function loadTestPage({ }) { info(`Loading test page starting at url: ${page}`); // Ensure no engine is being carried over from a previous test. - await TranslationsParent.destroyEngineProcess(); + await EngineProcess.destroyTranslationsEngine(); Services.fog.testResetFOG(); await SpecialPowers.pushPrefEnv({ set: [ @@ -552,7 +552,7 @@ async function loadTestPage({ if (autoOffer && TranslationsParent.shouldAlwaysOfferTranslations()) { info("Waiting for the popup to be automatically shown."); await waitForCondition(() => { - const panel = document.getElementById("translations-panel"); + const panel = document.getElementById("full-page-translations-panel"); return panel && panel.state === "open"; }); } @@ -585,10 +585,9 @@ async function loadTestPage({ * @returns {Promise} */ async cleanup() { + await closeAllOpenPanelsAndMenus(); await loadBlankPage(); - await TranslationsParent.destroyEngineProcess(); - await closeTranslationsPanelIfOpen(); - await closeContextMenuIfOpen(); + await EngineProcess.destroyTranslationsEngine(); await removeMocks(); Services.fog.testResetFOG(); TranslationsParent.testAutomaticPopup = false; @@ -658,7 +657,8 @@ async function captureTranslationsError(callback) { /** * Load a test page and run - * @param {Object} options - The options for `loadTestPage` plus a `runInPage` function. + * + * @param {object} options - The options for `loadTestPage` plus a `runInPage` function. */ async function autoTranslatePage(options) { const { prefs, languagePairs, ...otherOptions } = options; @@ -675,6 +675,10 @@ async function autoTranslatePage(options) { await cleanup(); } +/** + * @typedef {ReturnType} AttachmentMock + */ + /** * @param {RemoteSettingsClient} client * @param {string} mockedCollectionName - The name of the mocked collection without @@ -826,7 +830,7 @@ let _remoteSettingsMockId = 0; * Creates a local RemoteSettingsClient for use within tests. * * @param {boolean} autoDownloadFromRemoteSettings - * @param {Object[]} langPairs + * @param {object[]} langPairs * @returns {RemoteSettingsClient} */ async function createTranslationModelsRemoteClient( @@ -980,7 +984,7 @@ function hitEnterKey(button, message) { * * @see assertVisibility * - * @param {Object} options + * @param {object} options * @param {string} options.message * @param {Record} options.visible * @param {Record} options.hidden @@ -1011,7 +1015,7 @@ async function ensureVisibility({ message = null, visible = {}, hidden = {} }) { /** * Asserts that the provided elements are either visible or hidden. * - * @param {Object} options + * @param {object} options * @param {string} options.message * @param {Record} options.visible * @param {Record} options.hidden @@ -1066,10 +1070,9 @@ async function setupAboutPreferences( const elements = await selectAboutPreferencesElements(); async function cleanup() { + await closeAllOpenPanelsAndMenus(); await loadBlankPage(); - await TranslationsParent.destroyEngineProcess(); - await closeTranslationsPanelIfOpen(); - await closeContextMenuIfOpen(); + await EngineProcess.destroyTranslationsEngine(); BrowserTestUtils.removeTab(tab); await removeMocks(); await SpecialPowers.popPrefEnv(); @@ -1137,8 +1140,8 @@ class TestTranslationsTelemetry { * Asserts qualities about a counter telemetry metric. * * @param {string} name - The name of the metric. - * @param {Object} counter - The Glean counter object. - * @param {Object} expectedCount - The expected value of the counter. + * @param {object} counter - The Glean counter object. + * @param {object} expectedCount - The expected value of the counter. */ static async assertCounter(name, counter, expectedCount) { // Ensures that glean metrics are collected from all child processes @@ -1155,16 +1158,16 @@ class TestTranslationsTelemetry { /** * Asserts qualities about an event telemetry metric. * - * @param {string} name - The name of the metric. - * @param {Object} event - The Glean event object. - * @param {Object} expectations - The test expectations. + * @param {object} event - The Glean event object. + * @param {object} expectations - The test expectations. * @param {number} expectations.expectedEventCount - The expected count of events. * @param {boolean} expectations.expectNewFlowId + * @param {boolean} [expectations.expectFirstInteraction] * - Expects the flowId to be different than the previous flowId if true, * and expects it to be the same if false. - * @param {Array} [expectations.allValuePredicates=[]] + * @param {Array} [expectations.allValuePredicates=[]] * - An array of function predicates to assert for all event values. - * @param {Array} [expectations.finalValuePredicates=[]] + * @param {Array} [expectations.finalValuePredicates=[]] * - An array of function predicates to assert for only the final event value. */ static async assertEvent( @@ -1264,8 +1267,8 @@ class TestTranslationsTelemetry { * Asserts qualities about a rate telemetry metric. * * @param {string} name - The name of the metric. - * @param {Object} rate - The Glean rate object. - * @param {Object} expectations - The test expectations. + * @param {object} rate - The Glean rate object. + * @param {object} expectations - The test expectations. * @param {number} expectations.expectedNumerator - The expected value of the numerator. * @param {number} expectations.expectedDenominator - The expected value of the denominator. */ @@ -1295,7 +1298,7 @@ class TestTranslationsTelemetry { * Provide longer defaults for the waitForCondition. * * @param {Function} callback - * @param {string} messages + * @param {string} message */ function waitForCondition(callback, message) { const interval = 100; @@ -1346,9 +1349,10 @@ function getNeverTranslateSitesFromPerms() { /** * Opens a dialog window for about:preferences + * * @param {string} dialogUrl - The URL of the dialog window * @param {Function} callback - The function to open the dialog via UI - * @returns {Object} The dialog window object + * @returns {object} The dialog window object */ async function waitForOpenDialogWindow(dialogUrl, callback) { const dialogLoaded = promiseLoadSubDialog(dialogUrl); @@ -1360,7 +1364,7 @@ async function waitForOpenDialogWindow(dialogUrl, callback) { /** * Closes an open dialog window and waits for it to close. * - * @param {Object} dialogWindow + * @param {object} dialogWindow */ async function waitForCloseDialogWindow(dialogWindow) { const closePromise = BrowserTestUtils.waitForEvent( diff --git a/toolkit/components/translations/tests/browser/translations-test.mjs b/toolkit/components/translations/tests/browser/translations-test.mjs index 3e16be57e9..a740a2d1cc 100644 --- a/toolkit/components/translations/tests/browser/translations-test.mjs +++ b/toolkit/components/translations/tests/browser/translations-test.mjs @@ -60,7 +60,7 @@ export function getSelectors() { * Provide longer defaults for the waitForCondition. * * @param {Function} callback - * @param {string} messages + * @param {string} message */ function waitForCondition(callback, message) { const interval = 100; diff --git a/toolkit/components/translations/translations.d.ts b/toolkit/components/translations/translations.d.ts index ec1e899af4..cc8d462a9c 100644 --- a/toolkit/components/translations/translations.d.ts +++ b/toolkit/components/translations/translations.d.ts @@ -187,7 +187,7 @@ export namespace Bergamot { /** * The client to interact with RemoteSettings. - * See services/settings/RemoteSettingsClient.jsm + * See services/settings/RemoteSettingsClient.sys.mjs */ interface RemoteSettingsClient { on: Function, -- cgit v1.2.3