/* 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.defineLazyGetter(lazy, "console", () => { return console.createInstance({ maxLogLevelPref: "browser.translations.logLevel", prefix: "Translations", }); }); ChromeUtils.defineESModuleGetters(lazy, { LanguageDetector: "resource://gre/modules/translations/LanguageDetector.sys.mjs", }); /** * @typedef {import("./TranslationsChild.sys.mjs").TranslationsEngine} TranslationsEngine * @typedef {import("./TranslationsChild.sys.mjs").SupportedLanguages} SupportedLanguages * @typedef {import("../translations").LanguagePair} LanguagePair */ /** * 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" ); async handleEvent(event) { if (event.type === "DOMDocElementInserted") { this.#exportFunctions(); } if (event.type === "DOMContentLoaded") { const enabled = await this.sendQuery("AboutTranslations:GetEnabledState"); this.#sendEventToContent({ type: "enabled-state-changed", enabled, }); } } receiveMessage({ name, data }) { switch (name) { case "AboutTranslations:EnabledStateChanged": { const { enabled } = data; this.#sendEventToContent({ type: "enabled-state-changed", enabled, }); break; } case "AboutTranslations:SendTranslationsPort": { const { languagePair, port } = data; const transferables = [port]; this.contentWindow.postMessage( { type: "GetTranslationsPort", languagePair, port, }, "*", transferables ); break; } case "AboutTranslations:RebuildTranslator": { this.#sendEventToContent({ type: "rebuild-translator" }); break; } default: throw new Error("Unknown AboutTranslations message: " + name); } } RPMGetFormatURLPref(formatURL) { return Services.urlFormatter.formatURLPref(formatURL); } /** * @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} promise * @returns {Promise} */ #convertToContentPromise(promise) { return new this.contentWindow.Promise((resolve, reject) => promise.then(resolve, error => { let contentWindow; try { contentWindow = this.contentWindow; } catch { // 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_getAppLocaleAsBCP47", "AT_getSupportedLanguages", "AT_clearSourceText", "AT_enableTranslationsFeature", "AT_isEnabledStateManagedByPolicy", "AT_isTranslationEngineSupported", "AT_isHtmlTranslation", "AT_isInAutomation", "AT_createTranslationsPort", "AT_identifyLanguage", "AT_getDisplayName", "AT_getScriptDirection", "AT_telemetry", "RPMGetFormatURLPref", ]; 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_getAppLocaleAsBCP47() { return Services.locale.appLocaleAsBCP47; } /** * Wire this function to the TranslationsChild. * * @returns {Promise} */ AT_getSupportedLanguages() { return this.#convertToContentPromise( this.sendQuery("AboutTranslations:GetSupportedLanguages").then(data => Cu.cloneInto(data, this.contentWindow) ) ); } /** * Clears the about:translations source textarea with privileged user-input * semantics, rather than setting the value directly to an empty string. * * Clearing the text this way allows the text to be restored via `Ctrl/Cmd + Z`. */ AT_clearSourceText() { const sourceTextArea = this.contentWindow.document.getElementById( "about-translations-source-textarea" ); sourceTextArea.focus(); sourceTextArea.setUserInput(""); } /** * Returns the display name of the given BCP-47 language tag. * * @param {string} language */ AT_getDisplayName(language) { return this.#convertToContentPromise( this.sendQuery("AboutTranslations:GetDisplayName", { language }).then( data => Cu.cloneInto(data, this.contentWindow) ) ); } /** * Does this device support the translation engine? * * @returns {Promise} */ AT_isTranslationEngineSupported() { return this.#convertToContentPromise( this.sendQuery("AboutTranslations:IsTranslationsEngineSupported") ); } /** * Returns true if the enabled state is managed by an enterprise policy, otherwise false. * * When false, the user may freely enable or disable the Translations feature. * When true, the enabled state cannot be changed by the user at run time. * * Note that it is possible for a policy to enforce that the feature is "disabled and immutable," * such that the user cannot turn the feature on, as well as "enabled and immutable," such that * the user cannot turn the feature off. * * @returns {Promise} */ AT_isEnabledStateManagedByPolicy() { return this.#convertToContentPromise( this.sendQuery("AboutTranslations:IsEnabledStateManagedByPolicy") ); } /** * Enables the Translations feature. * * @returns {Promise} */ AT_enableTranslationsFeature() { return this.#convertToContentPromise( this.sendQuery("AboutTranslations:EnableTranslationsFeature") ); } /** * Expose the #isHtmlTranslation property. * * @returns {bool} */ AT_isHtmlTranslation() { return this.#isHtmlTranslation; } /** * Returns true if we are running tests in automation, otherwise false. * * @returns {boolean} */ AT_isInAutomation() { return Cu.isInAutomation; } /** * 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 {LanguagePair} languagePair * @returns {void} */ AT_createTranslationsPort(languagePair) { this.sendAsyncMessage("AboutTranslations:GetTranslationsPort", { languagePair, }); } /** * Attempts to identify the human language in which the message is written. * * @param {string} message * @returns {Promise<{ language: string, confident: boolean }>} */ AT_identifyLanguage(message) { return this.#convertToContentPromise( lazy.LanguageDetector.detectLanguage(message).then(data => Cu.cloneInto(data, this.contentWindow) ) ); } /** * TODO - Remove this when Intl.Locale.prototype.textInfo is available to * content scripts. * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo * https://bugzilla.mozilla.org/show_bug.cgi?id=1693576 * * @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, }); } }