diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /toolkit/components/translations/actors | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/translations/actors')
-rw-r--r-- | toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs | 3 | ||||
-rw-r--r-- | toolkit/components/translations/actors/TranslationsParent.sys.mjs | 281 |
2 files changed, 198 insertions, 86 deletions
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(); } |