1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = [
10 "TranslationTelemetry",
13 const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
14 const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage";
16 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
25 translationListener: null,
27 serviceUnavailable: false,
29 supportedSourceLanguages: [
47 supportedTargetLanguages: [
66 setListenerForTests(listener) {
67 this.translationListener = listener;
70 _defaultTargetLanguage: "",
71 get defaultTargetLanguage() {
72 if (!this._defaultTargetLanguage) {
73 this._defaultTargetLanguage = Services.locale.appLocaleAsBCP47.split(
77 return this._defaultTargetLanguage;
80 openProviderAttribution() {
81 let attribution = this.supportedEngines[this.translationEngine];
82 const { BrowserWindowTracker } = ChromeUtils.import(
83 "resource:///modules/BrowserWindowTracker.jsm"
85 BrowserWindowTracker.getTopWindow().openWebLinkIn(attribution, "tab");
89 * The list of translation engines and their attributions.
93 Bing: "http://aka.ms/MicrosoftTranslatorAttribution",
94 Yandex: "http://translate.yandex.com/",
98 * Fallback engine (currently Google) if the preferences seem confusing.
100 get defaultEngine() {
101 return Object.keys(this.supportedEngines)[0];
105 * Returns the name of the preferred translation engine.
107 get translationEngine() {
108 let engine = Services.prefs.getCharPref("browser.translation.engine");
109 return !Object.keys(this.supportedEngines).includes(engine)
115 /* Translation objects keep the information related to translation for
116 * a specific browser. The properties exposed to the infobar are:
117 * - detectedLanguage, code of the language detected on the web page.
118 * - state, the state in which the infobar should be displayed
119 * - translatedFrom, if already translated, source language code.
120 * - translatedTo, if already translated, target language code.
121 * - translate, method starting the translation of the current page.
122 * - showOriginalContent, method showing the original page content.
123 * - showTranslatedContent, method showing the translation for an
124 * already translated page whose original content is shown.
125 * - originalShown, boolean indicating if the original or translated
126 * version of the page is shown.
128 class TranslationParent extends JSWindowActorParent {
131 this.originalShown = true;
135 return this.browsingContext.top.embedderElement;
138 receiveMessage(aMessage) {
139 switch (aMessage.name) {
140 case "Translation:DocumentState":
141 this.documentStateReceived(aMessage.data);
146 documentStateReceived(aData) {
147 if (aData.state == Translation.STATE_OFFER) {
148 if (aData.detectedLanguage == Translation.defaultTargetLanguage) {
149 // Detected language is the same as the user's locale.
154 !Translation.supportedTargetLanguages.includes(aData.detectedLanguage)
156 // Detected language is not part of the supported languages.
157 TranslationTelemetry.recordMissedTranslationOpportunity(
158 aData.detectedLanguage
163 TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage);
166 if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
170 // Set all values before showing a new translation infobar.
171 this._state = Translation.serviceUnavailable
172 ? Translation.STATE_UNAVAILABLE
174 this.detectedLanguage = aData.detectedLanguage;
175 this.translatedFrom = aData.translatedFrom;
176 this.translatedTo = aData.translatedTo;
177 this.originalShown = aData.originalShown;
179 this.showURLBarIcon();
181 if (this.shouldShowInfoBar(this.browser.contentPrincipal)) {
182 this.showTranslationInfoBar();
186 translate(aFrom, aTo) {
189 (this.state == Translation.STATE_TRANSLATED &&
190 this.translatedFrom == aFrom &&
191 this.translatedTo == aTo)
197 if (this.state == Translation.STATE_OFFER) {
198 if (this.detectedLanguage != aFrom) {
199 TranslationTelemetry.recordDetectedLanguageChange(true);
202 if (this.translatedFrom != aFrom) {
203 TranslationTelemetry.recordDetectedLanguageChange(false);
205 if (this.translatedTo != aTo) {
206 TranslationTelemetry.recordTargetLanguageChange();
210 this.state = Translation.STATE_TRANSLATING;
211 this.translatedFrom = aFrom;
212 this.translatedTo = aTo;
214 this.sendQuery("Translation:TranslateDocument", {
219 this.translationFinished(result);
226 let chromeWin = this.browser.ownerGlobal;
227 let PopupNotifications = chromeWin.PopupNotifications;
228 let removeId = this.originalShown ? "translated" : "translate";
229 let notification = PopupNotifications.getNotification(
234 PopupNotifications.remove(notification);
237 let callback = (aTopic, aNewBrowser) => {
238 if (aTopic == "swapping") {
239 let infoBarVisible = this.notificationBox.getNotificationWithValue(
242 if (infoBarVisible) {
243 this.showTranslationInfoBar();
248 if (aTopic != "showing") {
251 let translationNotification = this.notificationBox.getNotificationWithValue(
254 if (translationNotification) {
255 translationNotification.close();
257 this.showTranslationInfoBar();
262 let addId = this.originalShown ? "translate" : "translated";
263 PopupNotifications.show(
267 addId + "-notification-icon",
270 { dismissed: true, eventCallback: callback }
279 let notif = this.notificationBox.getNotificationWithValue("translation");
286 showOriginalContent() {
287 this.originalShown = true;
288 this.showURLBarIcon();
289 this.sendAsyncMessage("Translation:ShowOriginal");
290 TranslationTelemetry.recordShowOriginalContent();
293 showTranslatedContent() {
294 this.originalShown = false;
295 this.showURLBarIcon();
296 this.sendAsyncMessage("Translation:ShowTranslation");
299 get notificationBox() {
300 return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
303 showTranslationInfoBar() {
304 let notificationBox = this.notificationBox;
305 let notif = notificationBox.appendNotification("translation", {
306 priority: notificationBox.PRIORITY_INFO_HIGH,
307 notificationIs: "translation-notification",
313 shouldShowInfoBar(aPrincipal) {
314 // Never show the infobar automatically while the translation
315 // service is temporarily unavailable.
316 if (Translation.serviceUnavailable) {
320 // Check if we should never show the infobar for this language.
321 let neverForLangs = Services.prefs.getCharPref(
322 "browser.translation.neverForLanguages"
324 if (neverForLangs.split(",").includes(this.detectedLanguage)) {
325 TranslationTelemetry.recordAutoRejectedTranslationOffer();
329 // or if we should never show the infobar for this domain.
330 let perms = Services.perms;
332 perms.testExactPermissionFromPrincipal(aPrincipal, "translate") ==
335 TranslationTelemetry.recordAutoRejectedTranslationOffer();
342 translationFinished(result) {
343 if (result.success) {
344 this.originalShown = false;
345 this.state = Translation.STATE_TRANSLATED;
346 this.showURLBarIcon();
348 // Record the number of characters translated.
349 TranslationTelemetry.recordTranslation(
352 result.characterCount
354 } else if (result.unavailable) {
355 Translation.serviceUnavailable = true;
356 this.state = Translation.STATE_UNAVAILABLE;
358 this.state = Translation.STATE_ERROR;
361 if (Translation.translationListener) {
362 Translation.translationListener();
367 if (this.state == Translation.STATE_OFFER) {
368 TranslationTelemetry.recordDeniedTranslationOffer();
374 * Uses telemetry histograms for collecting statistics on the usage of the
375 * translation component.
377 * NOTE: Metrics are only recorded if the user enabled the telemetry option.
379 var TranslationTelemetry = {
381 // Constructing histograms.
382 const plain = id => Services.telemetry.getHistogramById(id);
383 const keyed = id => Services.telemetry.getKeyedHistogramById(id);
385 OPPORTUNITIES: () => plain("TRANSLATION_OPPORTUNITIES"),
386 OPPORTUNITIES_BY_LANG: () =>
387 keyed("TRANSLATION_OPPORTUNITIES_BY_LANGUAGE"),
388 PAGES: () => plain("TRANSLATED_PAGES"),
389 PAGES_BY_LANG: () => keyed("TRANSLATED_PAGES_BY_LANGUAGE"),
390 CHARACTERS: () => plain("TRANSLATED_CHARACTERS"),
391 DENIED: () => plain("DENIED_TRANSLATION_OFFERS"),
392 AUTO_REJECTED: () => plain("AUTO_REJECTED_TRANSLATION_OFFERS"),
393 SHOW_ORIGINAL: () => plain("REQUESTS_OF_ORIGINAL_CONTENT"),
394 TARGET_CHANGES: () => plain("CHANGES_OF_TARGET_LANGUAGE"),
395 DETECTION_CHANGES: () => plain("CHANGES_OF_DETECTED_LANGUAGE"),
396 SHOW_UI: () => plain("SHOULD_TRANSLATION_UI_APPEAR"),
397 DETECT_LANG: () => plain("SHOULD_AUTO_DETECT_LANGUAGE"),
400 // Capturing the values of flags at the startup.
401 this.recordPreferences();
405 * Record a translation opportunity in the health report.
407 * The language of the page.
409 recordTranslationOpportunity(language) {
410 return this._recordOpportunity(language, true);
414 * Record a missed translation opportunity in the health report.
415 * A missed opportunity is when the language detected is not part
416 * of the supported languages.
418 * The language of the page.
420 recordMissedTranslationOpportunity(language) {
421 return this._recordOpportunity(language, false);
425 * Record an automatically rejected translation offer in the health
426 * report. A translation offer is automatically rejected when a user
427 * has previously clicked "Never translate this language" or "Never
428 * translate this site", which results in the infobar not being shown for
429 * the translation opportunity.
431 * These translation opportunities should still be recorded in addition to
432 * recording the automatic rejection of the offer.
434 recordAutoRejectedTranslationOffer() {
435 this.HISTOGRAMS.AUTO_REJECTED().add();
439 * Record a translation in the health report.
441 * The language of the page.
443 * The language translated to
444 * @param numCharacters
445 * The number of characters that were translated
447 recordTranslation(langFrom, langTo, numCharacters) {
448 this.HISTOGRAMS.PAGES().add();
449 this.HISTOGRAMS.PAGES_BY_LANG().add(langFrom + " -> " + langTo);
450 this.HISTOGRAMS.CHARACTERS().add(numCharacters);
454 * Record a change of the detected language in the health report. This should
455 * only be called when actually executing a translation, not every time the
456 * user changes in the language in the UI.
458 * @param beforeFirstTranslation
459 * A boolean indicating if we are recording a change of detected
460 * language before translating the page for the first time. If we
461 * have already translated the page from the detected language and
462 * the user has manually adjusted the detected language false should
465 recordDetectedLanguageChange(beforeFirstTranslation) {
466 this.HISTOGRAMS.DETECTION_CHANGES().add(beforeFirstTranslation);
470 * Record a change of the target language in the health report. This should
471 * only be called when actually executing a translation, not every time the
472 * user changes in the language in the UI.
474 recordTargetLanguageChange() {
475 this.HISTOGRAMS.TARGET_CHANGES().add();
479 * Record a denied translation offer.
481 recordDeniedTranslationOffer() {
482 this.HISTOGRAMS.DENIED().add();
486 * Record a "Show Original" command use.
488 recordShowOriginalContent() {
489 this.HISTOGRAMS.SHOW_ORIGINAL().add();
493 * Record the state of translation preferences.
495 recordPreferences() {
496 if (Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
497 this.HISTOGRAMS.SHOW_UI().add(1);
499 if (Services.prefs.getBoolPref(TRANSLATION_PREF_DETECT_LANG)) {
500 this.HISTOGRAMS.DETECT_LANG().add(1);
504 _recordOpportunity(language, success) {
505 this.HISTOGRAMS.OPPORTUNITIES().add(success);
506 this.HISTOGRAMS.OPPORTUNITIES_BY_LANG().add(language, success);
510 TranslationTelemetry.init();