summaryrefslogtreecommitdiffstats
path: root/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs264
1 files changed, 264 insertions, 0 deletions
diff --git a/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
new file mode 100644
index 0000000000..c501c1b0cd
--- /dev/null
+++ b/toolkit/components/translations/actors/AboutTranslationsChild.sys.mjs
@@ -0,0 +1,264 @@
+/* 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/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),
+ })
+ );
+ }
+
+ /**
+ * @returns {TranslationsChild}
+ */
+ #getTranslationsChild() {
+ const child = this.contentWindow.windowGlobalChild.getActor("Translations");
+ if (!child) {
+ throw new Error("Unable to find the TranslationsChild");
+ }
+ return child;
+ }
+
+ /**
+ * 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",
+ ];
+ 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.
+ *
+ * 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);
+ }
+}