summaryrefslogtreecommitdiffstats
path: root/browser/components/translation/Translation.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/translation/Translation.jsm')
-rw-r--r--browser/components/translation/Translation.jsm446
1 files changed, 446 insertions, 0 deletions
diff --git a/browser/components/translation/Translation.jsm b/browser/components/translation/Translation.jsm
new file mode 100644
index 000000000..15a847c13
--- /dev/null
+++ b/browser/components/translation/Translation.jsm
@@ -0,0 +1,446 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+ "Translation",
+ "TranslationTelemetry",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
+const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+this.Translation = {
+ STATE_OFFER: 0,
+ STATE_TRANSLATING: 1,
+ STATE_TRANSLATED: 2,
+ STATE_ERROR: 3,
+ STATE_UNAVAILABLE: 4,
+
+ 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"],
+
+ _defaultTargetLanguage: "",
+ get defaultTargetLanguage() {
+ if (!this._defaultTargetLanguage) {
+ this._defaultTargetLanguage = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global")
+ .split("-")[0];
+ }
+ return this._defaultTargetLanguage;
+ },
+
+ documentStateReceived: function(aBrowser, aData) {
+ if (aData.state == this.STATE_OFFER) {
+ if (aData.detectedLanguage == this.defaultTargetLanguage) {
+ // Detected language is the same as the user's locale.
+ return;
+ }
+
+ if (this.supportedSourceLanguages.indexOf(aData.detectedLanguage) == -1) {
+ // 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;
+
+ if (!aBrowser.translationUI)
+ aBrowser.translationUI = new TranslationUI(aBrowser);
+ let trUI = aBrowser.translationUI;
+
+ // Set all values before showing a new translation infobar.
+ trUI._state = Translation.serviceUnavailable ? Translation.STATE_UNAVAILABLE
+ : aData.state;
+ trUI.detectedLanguage = aData.detectedLanguage;
+ trUI.translatedFrom = aData.translatedFrom;
+ trUI.translatedTo = aData.translatedTo;
+ trUI.originalShown = aData.originalShown;
+
+ trUI.showURLBarIcon();
+
+ if (trUI.shouldShowInfoBar(aBrowser.currentURI))
+ trUI.showTranslationInfoBar();
+ },
+
+ openProviderAttribution: function() {
+ let attribution = this.supportedEngines[this.translationEngine];
+ Cu.import("resource:///modules/RecentWindow.jsm");
+ RecentWindow.getMostRecentBrowserWindow().openUILinkIn(attribution, "tab");
+ },
+
+ /**
+ * The list of translation engines and their attributions.
+ */
+ supportedEngines: {
+ "bing" : "http://aka.ms/MicrosoftTranslatorAttribution",
+ "yandex" : "http://translate.yandex.com/"
+ },
+
+ /**
+ * Fallback engine (currently Bing Translator) if the preferences seem
+ * confusing.
+ */
+ get defaultEngine() {
+ return this.supportedEngines.keys[0];
+ },
+
+ /**
+ * Returns the name of the preferred translation engine.
+ */
+ get translationEngine() {
+ let engine = Services.prefs.getCharPref("browser.translation.engine");
+ return Object.keys(this.supportedEngines).indexOf(engine) == -1 ? this.defaultEngine : engine;
+ },
+};
+
+/* TranslationUI objects keep the information related to translation for
+ * a specific browser. This object is passed to the translation
+ * infobar so that it can initialize itself. 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.
+ */
+function TranslationUI(aBrowser) {
+ this.browser = aBrowser;
+}
+
+TranslationUI.prototype = {
+ get browser() {
+ return this._browser;
+ },
+ set browser(aBrowser) {
+ if (this._browser)
+ this._browser.messageManager.removeMessageListener("Translation:Finished", this);
+ aBrowser.messageManager.addMessageListener("Translation:Finished", this);
+ this._browser = aBrowser;
+ },
+ translate: function(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.browser.messageManager.sendAsyncMessage(
+ "Translation:TranslateDocument",
+ { from: aFrom, to: aTo }
+ );
+ },
+
+ showURLBarIcon: function() {
+ 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");
+ aNewBrowser.translationUI = this;
+ this.browser = aNewBrowser;
+ if (infoBarVisible)
+ this.showTranslationInfoBar();
+ return true;
+ }
+
+ if (aTopic != "showing")
+ return false;
+ let notification = this.notificationBox.getNotificationWithValue("translation");
+ if (notification)
+ notification.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});
+ },
+
+ _state: 0,
+ get state() {
+ return this._state;
+ },
+ set state(val) {
+ let notif = this.notificationBox.getNotificationWithValue("translation");
+ if (notif)
+ notif.state = val;
+ this._state = val;
+ },
+
+ originalShown: true,
+ showOriginalContent: function() {
+ this.originalShown = true;
+ this.showURLBarIcon();
+ this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal");
+ TranslationTelemetry.recordShowOriginalContent();
+ },
+
+ showTranslatedContent: function() {
+ this.originalShown = false;
+ this.showURLBarIcon();
+ this.browser.messageManager.sendAsyncMessage("Translation:ShowTranslation");
+ },
+
+ get notificationBox() {
+ return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
+ },
+
+ showTranslationInfoBar: function() {
+ let notificationBox = this.notificationBox;
+ let notif = notificationBox.appendNotification("", "translation", null,
+ notificationBox.PRIORITY_INFO_HIGH);
+ notif.init(this);
+ return notif;
+ },
+
+ shouldShowInfoBar: function(aURI) {
+ // 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(",").indexOf(this.detectedLanguage) != -1) {
+ TranslationTelemetry.recordAutoRejectedTranslationOffer();
+ return false;
+ }
+
+ // or if we should never show the infobar for this domain.
+ let perms = Services.perms;
+ if (perms.testExactPermission(aURI, "translate") == perms.DENY_ACTION) {
+ TranslationTelemetry.recordAutoRejectedTranslationOffer();
+ return false;
+ }
+
+ return true;
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Translation:Finished":
+ if (msg.data.success) {
+ this.originalShown = false;
+ this.state = Translation.STATE_TRANSLATED;
+ this.showURLBarIcon();
+
+ // Record the number of characters translated.
+ TranslationTelemetry.recordTranslation(msg.data.from, msg.data.to,
+ msg.data.characterCount);
+ } else if (msg.data.unavailable) {
+ Translation.serviceUnavailable = true;
+ this.state = Translation.STATE_UNAVAILABLE;
+ } else {
+ this.state = Translation.STATE_ERROR;
+ }
+ break;
+ }
+ },
+
+ infobarClosed: function() {
+ 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.
+ */
+this.TranslationTelemetry = {
+
+ init: function () {
+ // 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: function (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: function (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: function () {
+ if (!this._canRecord) return;
+ 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: function (langFrom, langTo, numCharacters) {
+ if (!this._canRecord) return;
+ 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: function (beforeFirstTranslation) {
+ if (!this._canRecord) return;
+ 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: function () {
+ if (!this._canRecord) return;
+ this.HISTOGRAMS.TARGET_CHANGES().add();
+ },
+
+ /**
+ * Record a denied translation offer.
+ */
+ recordDeniedTranslationOffer: function () {
+ if (!this._canRecord) return;
+ this.HISTOGRAMS.DENIED().add();
+ },
+
+ /**
+ * Record a "Show Original" command use.
+ */
+ recordShowOriginalContent: function () {
+ if (!this._canRecord) return;
+ this.HISTOGRAMS.SHOW_ORIGINAL().add();
+ },
+
+ /**
+ * Record the state of translation preferences.
+ */
+ recordPreferences: function () {
+ if (!this._canRecord) return;
+ 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: function(language, success) {
+ if (!this._canRecord) return;
+ this.HISTOGRAMS.OPPORTUNITIES().add(success);
+ this.HISTOGRAMS.OPPORTUNITIES_BY_LANG().add(language, success);
+ },
+
+ /**
+ * A shortcut for reading the telemetry preference.
+ *
+ */
+ _canRecord: function () {
+ return Services.prefs.getBoolPref("toolkit.telemetry.enabled");
+ }
+};
+
+this.TranslationTelemetry.init();