commit 4cbcb6be76bc54b6210a261124f3dd63dede0f73
parent 8a4d9fbac8a284bfa430f470f01322d485e28dec
Author: Erik Nordin <enordin@mozilla.com>
Date: Sat, 18 Oct 2025 02:16:51 +0000
Bug 1967758 - Defer Translations logic to page-load race r=translations-reviewers,gregtatum
This patch defers the page-language-identification logic
to run on page load, instead of on DOMContentLoaded,
or, in the worst case, 2 seconds after DOMContentLoaded
if the page has not fully loaded by then.
Differential Revision: https://phabricator.services.mozilla.com/D268282
Diffstat:
3 files changed, 118 insertions(+), 53 deletions(-)
diff --git a/toolkit/components/translations/actors/TranslationsChild.sys.mjs b/toolkit/components/translations/actors/TranslationsChild.sys.mjs
@@ -39,9 +39,11 @@ export class TranslationsChild extends JSWindowActorChild {
}
if (event.type === "DOMContentLoaded") {
- this.sendAsyncMessage("Translations:ReportLangTags", {
- documentElementLang: this.document.documentElement.lang,
+ this.sendAsyncMessage("Translations:DOMContentLoaded", {
+ htmlLangAttribute: this.document.documentElement.lang,
});
+ } else if (event.type === "load") {
+ this.sendAsyncMessage("Translations:Load");
}
}
diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs
@@ -468,6 +468,33 @@ export class TranslationsParent extends JSWindowActorParent {
static #DOC_CONFIDENCE_THRESHOLD = 150;
/**
+ * The maximum time that we will wait to react to the page's
+ * language after observing the DOMContentLoaded event.
+ *
+ * In an ideal scenario, we want to fully wait for the "load"
+ * event before we scrape the page for text, but we also need
+ * mindful that some pages may take a long time to fully load
+ * depending on the complexity of the page and the speed of
+ * the user's connection.
+ *
+ * This is the worst-case scenario where we will start scraping
+ * the page text even if it has not yet fully loaded.
+ */
+ static #REACT_TO_PAGE_LANGUAGE_TIMEOUT = 2000;
+
+ /**
+ * A race that determines when to react to to the page's language tag.
+ *
+ * Ideally, we want to wait for the page to fully load, but we may have
+ * to take action before that.
+ *
+ * @see {TranslationsParent#REACT_TO_PAGE_LANGUAGE_TIMEOUT}
+ *
+ * @type {PromiseWithResolvers<string> | null}
+ */
+ #reactToPageLanguageRace = null;
+
+ /**
* Contains the state that would affect UI. Anytime this state is changed, a dispatch
* event is sent so that UI can react to it. The actor is inside of /toolkit and
* needs a way of notifying /browser code (or other users) of when the state changes.
@@ -1550,58 +1577,92 @@ export class TranslationsParent extends JSWindowActorParent {
return port2;
}
+ /**
+ * Reacts to the page's language tag by considering the HTML lang attribute
+ * and also scraping a sample of the visible text on the page.
+ *
+ * An action is taken based on the agreement between the detected language
+ * and the HTML lang attribute (or lack thereof).
+ *
+ * @param {string} htmlLangAttribute
+ * @param {string} reason
+ */
+ async #reactToPageLanguage(htmlLangAttribute, reason) {
+ lazy.console.debug(`Reacting to page language due to "${reason}".`);
+
+ const detectedLanguages = await this.getDetectedLanguages(
+ htmlLangAttribute
+ ).catch(error => {
+ // Detecting the languages can fail if the page gets destroyed before it
+ // can be completed. This runs on every page that doesn't have a lang tag,
+ // so only report the error if you have Translations logging turned on to
+ // avoid console spam.
+ lazy.console.log("Failed to get the detected languages.", error);
+ });
+
+ if (this.#isDestroyed) {
+ return;
+ }
+
+ if (!detectedLanguages) {
+ // The actor was already destroyed, and the detectedLanguages weren't reported
+ // in time.
+ return;
+ }
+
+ this.languageState.detectedLanguages = detectedLanguages;
+
+ if (await this.shouldAutoTranslate(detectedLanguages)) {
+ if (this.#isDestroyed) {
+ return;
+ }
+
+ this.translate(
+ {
+ sourceLanguage: detectedLanguages.docLangTag,
+ targetLanguage: detectedLanguages.userLangTag,
+ },
+ true // reportAsAutoTranslate
+ );
+ } else {
+ if (this.#isDestroyed) {
+ return;
+ }
+
+ this.maybeOfferTranslations(detectedLanguages).catch(error =>
+ lazy.console.error(error)
+ );
+ }
+ }
+
async receiveMessage({ name, data }) {
if (this.#isDestroyed) {
return undefined;
}
switch (name) {
- case "Translations:ReportLangTags": {
- const { htmlLangAttribute, href } = data;
- const detectedLanguages = await this.getDetectedLanguages(
- htmlLangAttribute,
- href
- ).catch(error => {
- // Detecting the languages can fail if the page gets destroyed before it
- // can be completed. This runs on every page that doesn't have a lang tag,
- // so only report the error if you have Translations logging turned on to
- // avoid console spam.
- lazy.console.log("Failed to get the detected languages.", error);
- });
-
- if (this.#isDestroyed) {
- return undefined;
- }
+ case "Translations:DOMContentLoaded": {
+ const { htmlLangAttribute } = data;
- if (!detectedLanguages) {
- // The actor was already destroyed, and the detectedLanguages weren't reported
- // in time.
- return undefined;
- }
+ this.#reactToPageLanguageRace = Promise.withResolvers();
+ const { promise, resolve } = this.#reactToPageLanguageRace;
- this.languageState.detectedLanguages = detectedLanguages;
-
- if (await this.shouldAutoTranslate(detectedLanguages)) {
+ promise.then(reason => {
if (this.#isDestroyed) {
- return undefined;
+ return;
}
+ this.#reactToPageLanguage(htmlLangAttribute, reason);
+ this.#reactToPageLanguageRace = null;
+ });
- this.translate(
- {
- sourceLanguage: detectedLanguages.docLangTag,
- targetLanguage: detectedLanguages.userLangTag,
- },
- true // reportAsAutoTranslate
- );
- } else {
- if (this.#isDestroyed) {
- return undefined;
- }
+ lazy.setTimeout(() => {
+ resolve("timeout");
+ }, TranslationsParent.#REACT_TO_PAGE_LANGUAGE_TIMEOUT);
- this.maybeOfferTranslations(detectedLanguages).catch(error =>
- lazy.console.error(error)
- );
- }
+ return undefined;
+ }
+ case "Translations:Load": {
+ this.#reactToPageLanguageRace?.resolve("load");
return undefined;
}
case "Translations:RequestPort": {
@@ -3726,11 +3787,10 @@ export class TranslationsParent extends JSWindowActorParent {
* rather than the child to remove the per-content process memory allocation amount.
*
* @param {string} [htmlLangAttribute]
- * @param {string} [href]
* @returns {Promise<LangTags | null>} - Returns null if the actor was destroyed before
* the result could be resolved.
*/
- async getDetectedLanguages(htmlLangAttribute, href) {
+ async getDetectedLanguages(htmlLangAttribute) {
if (this.languageState.detectedLanguages) {
return this.languageState.detectedLanguages;
}
@@ -3739,15 +3799,10 @@ export class TranslationsParent extends JSWindowActorParent {
return null;
}
- if (htmlLangAttribute === undefined) {
- htmlLangAttribute = await this.queryDocumentElementLang();
- if (this.#isDestroyed) {
- return null;
- }
+ if (htmlLangAttribute) {
+ htmlLangAttribute = this.maybeRefineMacroLanguageTag(htmlLangAttribute);
}
- htmlLangAttribute = this.maybeRefineMacroLanguageTag(htmlLangAttribute);
-
let languagePairs = await TranslationsParent.getNonPivotLanguagePairs();
if (this.#isDestroyed) {
return null;
@@ -3827,7 +3882,7 @@ export class TranslationsParent extends JSWindowActorParent {
{ innerWindowId: this.innerWindowId },
message
);
- lazy.console.log(message, href);
+ lazy.console.log(message);
const langTag = await TranslationsParent.getTopPreferredSupportedToLang();
if (this.#isDestroyed) {
@@ -3857,7 +3912,8 @@ export class TranslationsParent extends JSWindowActorParent {
{ innerWindowId: this.innerWindowId },
message
);
- lazy.console.log(message, href);
+ lazy.console.log(message);
+
// The docLangTag will be set, while the userLangTag will be null.
return langTags;
}
diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs
@@ -608,6 +608,13 @@ let JSWINDOWACTORS = {
esModuleURI: "resource://gre/actors/TranslationsChild.sys.mjs",
events: {
DOMContentLoaded: {},
+ load: {
+ // Once the page is loaded, it's important that we react to the page's
+ // language tag as soon as possible in order to give a good response time
+ // for showing the translations panel, or for auto-translating, etc.
+ capture: true,
+ createActor: false,
+ },
},
},
matches: ["http://*/*", "https://*/*", "file:///*", "moz-extension://*"],