1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TranslationsDocument:
"chrome://global/content/translations/translations-document.sys.mjs",
LRUCache:
"chrome://global/content/translations/translations-document.sys.mjs",
LanguageDetector:
"resource://gre/modules/translation/LanguageDetector.sys.mjs",
});
/**
* This file is extremely sensitive to memory size and performance!
*/
export class TranslationsChild extends JSWindowActorChild {
/**
* @type {TranslationsDocument | null}
*/
#translatedDoc = null;
/**
* 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;
handleEvent(event) {
switch (event.type) {
case "DOMContentLoaded":
this.sendAsyncMessage("Translations:ReportLangTags", {
documentElementLang: this.document.documentElement.lang,
});
break;
}
}
addProfilerMarker(message) {
ChromeUtils.addProfilerMarker(
"TranslationsChild",
{ innerWindowId: this.contentWindow.windowGlobalChild.innerWindowId },
message
);
}
async receiveMessage({ name, data }) {
switch (name) {
case "Translations:TranslatePage": {
if (this.#translatedDoc?.translator.engineStatus === "error") {
this.#translatedDoc.destroy();
this.#translatedDoc = null;
}
if (this.#translatedDoc) {
console.error("This page was already translated.");
return undefined;
}
const { fromLanguage, toLanguage, port, translationsStart } = data;
if (
!TranslationsChild.#translationsCache ||
!TranslationsChild.#translationsCache.matches(
fromLanguage,
toLanguage
)
) {
TranslationsChild.#translationsCache = new lazy.LRUCache(
fromLanguage,
toLanguage
);
}
this.#translatedDoc = new lazy.TranslationsDocument(
this.document,
fromLanguage,
toLanguage,
this.contentWindow.windowGlobalChild.innerWindowId,
port,
() => this.sendAsyncMessage("Translations:RequestPort"),
translationsStart,
() => this.docShell.now(),
TranslationsChild.#translationsCache
);
return undefined;
}
case "Translations:GetDocumentElementLang":
return this.document.documentElement.lang;
case "Translations:IdentifyLanguage": {
// Wait for idle callback as the page will be more settled if it has
// dynamic content, like on a React app.
if (this.contentWindow) {
await new Promise(resolve => {
this.contentWindow.requestIdleCallback(resolve);
});
}
try {
return lazy.LanguageDetector.detectLanguageFromDocument(
this.document
);
} catch (error) {
return null;
}
}
case "Translations:AcquirePort": {
this.addProfilerMarker("Acquired a port, resuming translations");
this.#translatedDoc.translator.acquirePort(data.port);
return undefined;
}
default:
throw new Error("Unknown message.", name);
}
}
}
|