summaryrefslogtreecommitdiffstats
path: root/browser/components/translation/TranslationParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/translation/TranslationParent.jsm')
-rw-r--r--browser/components/translation/TranslationParent.jsm508
1 files changed, 508 insertions, 0 deletions
diff --git a/browser/components/translation/TranslationParent.jsm b/browser/components/translation/TranslationParent.jsm
new file mode 100644
index 0000000000..d47f0dfde6
--- /dev/null
+++ b/browser/components/translation/TranslationParent.jsm
@@ -0,0 +1,508 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "Translation",
+ "TranslationParent",
+ "TranslationTelemetry",
+];
+
+const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
+const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage";
+
+var Translation = {
+ STATE_OFFER: 0,
+ STATE_TRANSLATING: 1,
+ STATE_TRANSLATED: 2,
+ STATE_ERROR: 3,
+ STATE_UNAVAILABLE: 4,
+
+ translationListener: null,
+
+ serviceUnavailable: false,
+
+ supportedSourceLanguages: [
+ "bg",
+ "cs",
+ "de",
+ "en",
+ "es",
+ "fr",
+ "ja",
+ "ko",
+ "nl",
+ "no",
+ "pl",
+ "pt",
+ "ru",
+ "tr",
+ "vi",
+ "zh",
+ ],
+ supportedTargetLanguages: [
+ "bg",
+ "cs",
+ "de",
+ "en",
+ "es",
+ "fr",
+ "ja",
+ "ko",
+ "nl",
+ "no",
+ "pl",
+ "pt",
+ "ru",
+ "tr",
+ "vi",
+ "zh",
+ ],
+
+ setListenerForTests(listener) {
+ this.translationListener = listener;
+ },
+
+ _defaultTargetLanguage: "",
+ get defaultTargetLanguage() {
+ if (!this._defaultTargetLanguage) {
+ this._defaultTargetLanguage = Services.locale.appLocaleAsBCP47.split(
+ "-"
+ )[0];
+ }
+ return this._defaultTargetLanguage;
+ },
+
+ openProviderAttribution() {
+ let attribution = this.supportedEngines[this.translationEngine];
+ const { BrowserWindowTracker } = ChromeUtils.import(
+ "resource:///modules/BrowserWindowTracker.jsm"
+ );
+ BrowserWindowTracker.getTopWindow().openWebLinkIn(attribution, "tab");
+ },
+
+ /**
+ * The list of translation engines and their attributions.
+ */
+ supportedEngines: {
+ Google: "",
+ Bing: "http://aka.ms/MicrosoftTranslatorAttribution",
+ Yandex: "http://translate.yandex.com/",
+ },
+
+ /**
+ * Fallback engine (currently Google) if the preferences seem confusing.
+ */
+ get defaultEngine() {
+ return Object.keys(this.supportedEngines)[0];
+ },
+
+ /**
+ * Returns the name of the preferred translation engine.
+ */
+ get translationEngine() {
+ let engine = Services.prefs.getCharPref("browser.translation.engine");
+ return !Object.keys(this.supportedEngines).includes(engine)
+ ? this.defaultEngine
+ : engine;
+ },
+};
+
+/* Translation objects keep the information related to translation for
+ * a specific browser. The properties exposed to the infobar are:
+ * - detectedLanguage, code of the language detected on the web page.
+ * - state, the state in which the infobar should be displayed
+ * - translatedFrom, if already translated, source language code.
+ * - translatedTo, if already translated, target language code.
+ * - translate, method starting the translation of the current page.
+ * - showOriginalContent, method showing the original page content.
+ * - showTranslatedContent, method showing the translation for an
+ * already translated page whose original content is shown.
+ * - originalShown, boolean indicating if the original or translated
+ * version of the page is shown.
+ */
+class TranslationParent extends JSWindowActorParent {
+ actorCreated() {
+ this._state = 0;
+ this.originalShown = true;
+ }
+
+ get browser() {
+ return this.browsingContext.top.embedderElement;
+ }
+
+ receiveMessage(aMessage) {
+ switch (aMessage.name) {
+ case "Translation:DocumentState":
+ this.documentStateReceived(aMessage.data);
+ break;
+ }
+ }
+
+ documentStateReceived(aData) {
+ if (aData.state == Translation.STATE_OFFER) {
+ if (aData.detectedLanguage == Translation.defaultTargetLanguage) {
+ // Detected language is the same as the user's locale.
+ return;
+ }
+
+ if (
+ !Translation.supportedTargetLanguages.includes(aData.detectedLanguage)
+ ) {
+ // Detected language is not part of the supported languages.
+ TranslationTelemetry.recordMissedTranslationOpportunity(
+ aData.detectedLanguage
+ );
+ return;
+ }
+
+ TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage);
+ }
+
+ if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
+ return;
+ }
+
+ // Set all values before showing a new translation infobar.
+ this._state = Translation.serviceUnavailable
+ ? Translation.STATE_UNAVAILABLE
+ : aData.state;
+ this.detectedLanguage = aData.detectedLanguage;
+ this.translatedFrom = aData.translatedFrom;
+ this.translatedTo = aData.translatedTo;
+ this.originalShown = aData.originalShown;
+
+ this.showURLBarIcon();
+
+ if (this.shouldShowInfoBar(this.browser.contentPrincipal)) {
+ this.showTranslationInfoBar();
+ }
+ }
+
+ translate(aFrom, aTo) {
+ if (
+ aFrom == aTo ||
+ (this.state == Translation.STATE_TRANSLATED &&
+ this.translatedFrom == aFrom &&
+ this.translatedTo == aTo)
+ ) {
+ // Nothing to do.
+ return;
+ }
+
+ if (this.state == Translation.STATE_OFFER) {
+ if (this.detectedLanguage != aFrom) {
+ TranslationTelemetry.recordDetectedLanguageChange(true);
+ }
+ } else {
+ if (this.translatedFrom != aFrom) {
+ TranslationTelemetry.recordDetectedLanguageChange(false);
+ }
+ if (this.translatedTo != aTo) {
+ TranslationTelemetry.recordTargetLanguageChange();
+ }
+ }
+
+ this.state = Translation.STATE_TRANSLATING;
+ this.translatedFrom = aFrom;
+ this.translatedTo = aTo;
+
+ this.sendQuery("Translation:TranslateDocument", {
+ from: aFrom,
+ to: aTo,
+ }).then(
+ result => {
+ this.translationFinished(result);
+ },
+ () => {}
+ );
+ }
+
+ showURLBarIcon() {
+ let chromeWin = this.browser.ownerGlobal;
+ let PopupNotifications = chromeWin.PopupNotifications;
+ let removeId = this.originalShown ? "translated" : "translate";
+ let notification = PopupNotifications.getNotification(
+ removeId,
+ this.browser
+ );
+ if (notification) {
+ PopupNotifications.remove(notification);
+ }
+
+ let callback = (aTopic, aNewBrowser) => {
+ if (aTopic == "swapping") {
+ let infoBarVisible = this.notificationBox.getNotificationWithValue(
+ "translation"
+ );
+ if (infoBarVisible) {
+ this.showTranslationInfoBar();
+ }
+ return true;
+ }
+
+ if (aTopic != "showing") {
+ return false;
+ }
+ let translationNotification = this.notificationBox.getNotificationWithValue(
+ "translation"
+ );
+ if (translationNotification) {
+ translationNotification.close();
+ } else {
+ this.showTranslationInfoBar();
+ }
+ return true;
+ };
+
+ let addId = this.originalShown ? "translate" : "translated";
+ PopupNotifications.show(
+ this.browser,
+ addId,
+ null,
+ addId + "-notification-icon",
+ null,
+ null,
+ { dismissed: true, eventCallback: callback }
+ );
+ }
+
+ get state() {
+ return this._state;
+ }
+
+ set state(val) {
+ let notif = this.notificationBox.getNotificationWithValue("translation");
+ if (notif) {
+ notif.state = val;
+ }
+ this._state = val;
+ }
+
+ showOriginalContent() {
+ this.originalShown = true;
+ this.showURLBarIcon();
+ this.sendAsyncMessage("Translation:ShowOriginal");
+ TranslationTelemetry.recordShowOriginalContent();
+ }
+
+ showTranslatedContent() {
+ this.originalShown = false;
+ this.showURLBarIcon();
+ this.sendAsyncMessage("Translation:ShowTranslation");
+ }
+
+ get notificationBox() {
+ return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
+ }
+
+ showTranslationInfoBar() {
+ let notificationBox = this.notificationBox;
+ let notif = notificationBox.appendNotification("translation", {
+ priority: notificationBox.PRIORITY_INFO_HIGH,
+ notificationIs: "translation-notification",
+ });
+ notif.init(this);
+ return notif;
+ }
+
+ shouldShowInfoBar(aPrincipal) {
+ // Never show the infobar automatically while the translation
+ // service is temporarily unavailable.
+ if (Translation.serviceUnavailable) {
+ return false;
+ }
+
+ // Check if we should never show the infobar for this language.
+ let neverForLangs = Services.prefs.getCharPref(
+ "browser.translation.neverForLanguages"
+ );
+ if (neverForLangs.split(",").includes(this.detectedLanguage)) {
+ TranslationTelemetry.recordAutoRejectedTranslationOffer();
+ return false;
+ }
+
+ // or if we should never show the infobar for this domain.
+ let perms = Services.perms;
+ if (
+ perms.testExactPermissionFromPrincipal(aPrincipal, "translate") ==
+ perms.DENY_ACTION
+ ) {
+ TranslationTelemetry.recordAutoRejectedTranslationOffer();
+ return false;
+ }
+
+ return true;
+ }
+
+ translationFinished(result) {
+ if (result.success) {
+ this.originalShown = false;
+ this.state = Translation.STATE_TRANSLATED;
+ this.showURLBarIcon();
+
+ // Record the number of characters translated.
+ TranslationTelemetry.recordTranslation(
+ result.from,
+ result.to,
+ result.characterCount
+ );
+ } else if (result.unavailable) {
+ Translation.serviceUnavailable = true;
+ this.state = Translation.STATE_UNAVAILABLE;
+ } else {
+ this.state = Translation.STATE_ERROR;
+ }
+
+ if (Translation.translationListener) {
+ Translation.translationListener();
+ }
+ }
+
+ infobarClosed() {
+ if (this.state == Translation.STATE_OFFER) {
+ TranslationTelemetry.recordDeniedTranslationOffer();
+ }
+ }
+}
+
+/**
+ * Uses telemetry histograms for collecting statistics on the usage of the
+ * translation component.
+ *
+ * NOTE: Metrics are only recorded if the user enabled the telemetry option.
+ */
+var TranslationTelemetry = {
+ init() {
+ // Constructing histograms.
+ const plain = id => Services.telemetry.getHistogramById(id);
+ const keyed = id => Services.telemetry.getKeyedHistogramById(id);
+ this.HISTOGRAMS = {
+ OPPORTUNITIES: () => plain("TRANSLATION_OPPORTUNITIES"),
+ OPPORTUNITIES_BY_LANG: () =>
+ keyed("TRANSLATION_OPPORTUNITIES_BY_LANGUAGE"),
+ PAGES: () => plain("TRANSLATED_PAGES"),
+ PAGES_BY_LANG: () => keyed("TRANSLATED_PAGES_BY_LANGUAGE"),
+ CHARACTERS: () => plain("TRANSLATED_CHARACTERS"),
+ DENIED: () => plain("DENIED_TRANSLATION_OFFERS"),
+ AUTO_REJECTED: () => plain("AUTO_REJECTED_TRANSLATION_OFFERS"),
+ SHOW_ORIGINAL: () => plain("REQUESTS_OF_ORIGINAL_CONTENT"),
+ TARGET_CHANGES: () => plain("CHANGES_OF_TARGET_LANGUAGE"),
+ DETECTION_CHANGES: () => plain("CHANGES_OF_DETECTED_LANGUAGE"),
+ SHOW_UI: () => plain("SHOULD_TRANSLATION_UI_APPEAR"),
+ DETECT_LANG: () => plain("SHOULD_AUTO_DETECT_LANGUAGE"),
+ };
+
+ // Capturing the values of flags at the startup.
+ this.recordPreferences();
+ },
+
+ /**
+ * Record a translation opportunity in the health report.
+ * @param language
+ * The language of the page.
+ */
+ recordTranslationOpportunity(language) {
+ return this._recordOpportunity(language, true);
+ },
+
+ /**
+ * Record a missed translation opportunity in the health report.
+ * A missed opportunity is when the language detected is not part
+ * of the supported languages.
+ * @param language
+ * The language of the page.
+ */
+ recordMissedTranslationOpportunity(language) {
+ return this._recordOpportunity(language, false);
+ },
+
+ /**
+ * Record an automatically rejected translation offer in the health
+ * report. A translation offer is automatically rejected when a user
+ * has previously clicked "Never translate this language" or "Never
+ * translate this site", which results in the infobar not being shown for
+ * the translation opportunity.
+ *
+ * These translation opportunities should still be recorded in addition to
+ * recording the automatic rejection of the offer.
+ */
+ recordAutoRejectedTranslationOffer() {
+ this.HISTOGRAMS.AUTO_REJECTED().add();
+ },
+
+ /**
+ * Record a translation in the health report.
+ * @param langFrom
+ * The language of the page.
+ * @param langTo
+ * The language translated to
+ * @param numCharacters
+ * The number of characters that were translated
+ */
+ recordTranslation(langFrom, langTo, numCharacters) {
+ this.HISTOGRAMS.PAGES().add();
+ this.HISTOGRAMS.PAGES_BY_LANG().add(langFrom + " -> " + langTo);
+ this.HISTOGRAMS.CHARACTERS().add(numCharacters);
+ },
+
+ /**
+ * Record a change of the detected language in the health report. This should
+ * only be called when actually executing a translation, not every time the
+ * user changes in the language in the UI.
+ *
+ * @param beforeFirstTranslation
+ * A boolean indicating if we are recording a change of detected
+ * language before translating the page for the first time. If we
+ * have already translated the page from the detected language and
+ * the user has manually adjusted the detected language false should
+ * be passed.
+ */
+ recordDetectedLanguageChange(beforeFirstTranslation) {
+ this.HISTOGRAMS.DETECTION_CHANGES().add(beforeFirstTranslation);
+ },
+
+ /**
+ * Record a change of the target language in the health report. This should
+ * only be called when actually executing a translation, not every time the
+ * user changes in the language in the UI.
+ */
+ recordTargetLanguageChange() {
+ this.HISTOGRAMS.TARGET_CHANGES().add();
+ },
+
+ /**
+ * Record a denied translation offer.
+ */
+ recordDeniedTranslationOffer() {
+ this.HISTOGRAMS.DENIED().add();
+ },
+
+ /**
+ * Record a "Show Original" command use.
+ */
+ recordShowOriginalContent() {
+ this.HISTOGRAMS.SHOW_ORIGINAL().add();
+ },
+
+ /**
+ * Record the state of translation preferences.
+ */
+ recordPreferences() {
+ if (Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
+ this.HISTOGRAMS.SHOW_UI().add(1);
+ }
+ if (Services.prefs.getBoolPref(TRANSLATION_PREF_DETECT_LANG)) {
+ this.HISTOGRAMS.DETECT_LANG().add(1);
+ }
+ },
+
+ _recordOpportunity(language, success) {
+ this.HISTOGRAMS.OPPORTUNITIES().add(success);
+ this.HISTOGRAMS.OPPORTUNITIES_BY_LANG().add(language, success);
+ },
+};
+
+TranslationTelemetry.init();