Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
maxLogLevelPref: "browser.translations.logLevel",
prefix: "Translations",
});
});
ChromeUtils.defineESModuleGetters(lazy, {
LanguageDetector:
"resource://gre/modules/translation/LanguageDetector.sys.mjs",
});
/**
* @typedef {import("./TranslationsChild.sys.mjs").TranslationsEngine} TranslationsEngine
* @typedef {import("./TranslationsChild.sys.mjs").SupportedLanguages} SupportedLanguages
*/
/**
* The AboutTranslationsChild is responsible for coordinating what privileged APIs
* are exposed to the un-privileged scope of the about:translations page.
*/
export class AboutTranslationsChild extends JSWindowActorChild {
/**
* The translations engine uses text translations by default in about:translations,
* but it can be changed to translate HTML by setting this pref to true. This is
* useful for manually testing HTML translation behavior, but is not useful to surface
* as a user-facing feature.
*
* @type {bool}
*/
#isHtmlTranslation = Services.prefs.getBoolPref(
"browser.translations.useHTML"
);
handleEvent(event) {
if (event.type === "DOMDocElementInserted") {
this.#exportFunctions();
}
if (
event.type === "DOMContentLoaded" &&
Services.prefs.getBoolPref("browser.translations.enable")
) {
this.#sendEventToContent({ type: "enable" });
}
}
receiveMessage({ name, data }) {
switch (name) {
case "AboutTranslations:SendTranslationsPort": {
const { fromLanguage, toLanguage, port } = data;
const transferables = [port];
this.contentWindow.postMessage(
{
type: "GetTranslationsPort",
fromLanguage,
toLanguage,
port,
},
"*",
transferables
);
break;
}
default:
throw new Error("Unknown AboutTranslations message: " + name);
}
}
/**
* @param {object} detail
*/
#sendEventToContent(detail) {
this.contentWindow.dispatchEvent(
new this.contentWindow.CustomEvent("AboutTranslationsChromeToContent", {
detail: Cu.cloneInto(detail, this.contentWindow),
})
);
}
/**
* A privileged promise can't be used in the content page, so convert a privileged
* promise into a content one.
*
* @param {Promise<any>} promise
* @returns {Promise<any>}
*/
#convertToContentPromise(promise) {
return new this.contentWindow.Promise((resolve, reject) =>
promise.then(resolve, error => {
let contentWindow;
try {
contentWindow = this.contentWindow;
} catch (error) {
// The content window is no longer available.
reject();
return;
}
// Create an error in the content window, if the content window is still around.
let message = "An error occured in the AboutTranslations actor.";
if (typeof error === "string") {
message = error;
}
if (typeof error?.message === "string") {
message = error.message;
}
if (typeof error?.stack === "string") {
message += `\n\nOriginal stack:\n\n${error.stack}\n`;
}
reject(new contentWindow.Error(message));
})
);
}
/**
* Export any of the child functions that start with "AT_" to the unprivileged content
* page. This restricts the security capabilities of the the content page.
*/
#exportFunctions() {
const window = this.contentWindow;
const fns = [
"AT_log",
"AT_logError",
"AT_getAppLocale",
"AT_getSupportedLanguages",
"AT_isTranslationEngineSupported",
"AT_isHtmlTranslation",
"AT_createTranslationsPort",
"AT_identifyLanguage",
"AT_getScriptDirection",
"AT_telemetry",
];
for (const name of fns) {
Cu.exportFunction(this[name].bind(this), window, { defineAs: name });
}
}
/**
* Log messages if "browser.translations.logLevel" is set to "All".
*
* @param {...any} args
*/
AT_log(...args) {
lazy.console.log(...args);
}
/**
* Report an error to the console.
*
* @param {...any} args
*/
AT_logError(...args) {
lazy.console.error(...args);
}
/**
* Returns the app's locale.
*
* @returns {Intl.Locale}
*/
AT_getAppLocale() {
return Services.locale.appLocaleAsBCP47;
}
/**
* Wire this function to the TranslationsChild.
*
* @returns {Promise<SupportedLanguages>}
*/
AT_getSupportedLanguages() {
return this.#convertToContentPromise(
this.sendQuery("AboutTranslations:GetSupportedLanguages").then(data =>
Cu.cloneInto(data, this.contentWindow)
)
);
}
/**
* Does this device support the translation engine?
*
* @returns {Promise<boolean>}
*/
AT_isTranslationEngineSupported() {
return this.#convertToContentPromise(
this.sendQuery("AboutTranslations:IsTranslationsEngineSupported")
);
}
/**
* Expose the #isHtmlTranslation property.
*
* @returns {bool}
*/
AT_isHtmlTranslation() {
return this.#isHtmlTranslation;
}
/**
* Requests a port to the TranslationsEngine process. An engine will be created on
* the fly for translation requests through this port. This port is unique to its
* language pair. In order to translate a different language pair, a new port must be
* created for that pair. The lifecycle of the engine is managed by the
* TranslationsEngine.
*
* @param {string} fromLanguage
* @param {string} toLanguage
* @returns {void}
*/
AT_createTranslationsPort(fromLanguage, toLanguage) {
this.sendAsyncMessage("AboutTranslations:GetTranslationsPort", {
fromLanguage,
toLanguage,
});
}
/**
* Attempts to identify the human language in which the message is written.
*
* @param {string} message
* @returns {Promise<{ langTag: string, confidence: number }>}
*/
AT_identifyLanguage(message) {
return this.#convertToContentPromise(
lazy.LanguageDetector.detectLanguage(message).then(data =>
Cu.cloneInto(
// This language detector reports confidence as a boolean instead of
// a percentage, so we need to map the confidence to 0.0 or 1.0.
{ langTag: data.language, confidence: data.confident ? 1.0 : 0.0 },
this.contentWindow
)
)
);
}
/**
* TODO - Remove this when Intl.Locale.prototype.textInfo is available to
* content scripts.
*
*
* @param {string} locale
* @returns {string}
*/
AT_getScriptDirection(locale) {
return Services.intl.getScriptDirection(locale);
}
/**
* Sends telemetry data to the TranslationsEngine.
*
* @param {string} telemetryFunctionName - The name of the telemetry function.
* @param {object} telemetryData - The data associated with the telemetry event.
*/
AT_telemetry(telemetryFunctionName, telemetryData) {
this.sendAsyncMessage("AboutTranslations:Telemetry", {
telemetryFunctionName,
telemetryData,
});
}
}