gecko-dev/browser/components/translation/Translation.jsm

425 строки
14 KiB
JavaScript

/* 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",
"TranslationTelemetry",
];
const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage";
ChromeUtils.import("resource://gre/modules/Services.jsm");
var 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 = Services.locale.getAppLocaleAsLangTag()
.split("-")[0];
}
return this._defaultTargetLanguage;
},
documentStateReceived(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.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;
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() {
let attribution = this.supportedEngines[this.translationEngine];
ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
BrowserWindowTracker.getTopWindow().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).includes(engine) ? 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(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() {
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 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});
},
_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() {
this.originalShown = true;
this.showURLBarIcon();
this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal");
TranslationTelemetry.recordShowOriginalContent();
},
showTranslatedContent() {
this.originalShown = false;
this.showURLBarIcon();
this.browser.messageManager.sendAsyncMessage("Translation:ShowTranslation");
},
get notificationBox() {
return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
},
showTranslationInfoBar() {
let notificationBox = this.notificationBox;
let notif = notificationBox.appendNotification("", "translation", null,
notificationBox.PRIORITY_INFO_HIGH);
notif.init(this);
return notif;
},
shouldShowInfoBar(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(",").includes(this.detectedLanguage)) {
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(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() {
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);
},
};
this.TranslationTelemetry.init();