Bug 1756218 - run android reftest/crashtest as no-fission. r=releng-reviewers,gbrown
[gecko.git] / browser / components / translation / TranslationParent.jsm
blob9cd082ff356cdc7b70b6964ba6aab71871379a8a
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/. */
5 "use strict";
7 var EXPORTED_SYMBOLS = [
8   "Translation",
9   "TranslationParent",
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");
18 var Translation = {
19   STATE_OFFER: 0,
20   STATE_TRANSLATING: 1,
21   STATE_TRANSLATED: 2,
22   STATE_ERROR: 3,
23   STATE_UNAVAILABLE: 4,
25   translationListener: null,
27   serviceUnavailable: false,
29   supportedSourceLanguages: [
30     "bg",
31     "cs",
32     "de",
33     "en",
34     "es",
35     "fr",
36     "ja",
37     "ko",
38     "nl",
39     "no",
40     "pl",
41     "pt",
42     "ru",
43     "tr",
44     "vi",
45     "zh",
46   ],
47   supportedTargetLanguages: [
48     "bg",
49     "cs",
50     "de",
51     "en",
52     "es",
53     "fr",
54     "ja",
55     "ko",
56     "nl",
57     "no",
58     "pl",
59     "pt",
60     "ru",
61     "tr",
62     "vi",
63     "zh",
64   ],
66   setListenerForTests(listener) {
67     this.translationListener = listener;
68   },
70   _defaultTargetLanguage: "",
71   get defaultTargetLanguage() {
72     if (!this._defaultTargetLanguage) {
73       this._defaultTargetLanguage = Services.locale.appLocaleAsBCP47.split(
74         "-"
75       )[0];
76     }
77     return this._defaultTargetLanguage;
78   },
80   openProviderAttribution() {
81     let attribution = this.supportedEngines[this.translationEngine];
82     const { BrowserWindowTracker } = ChromeUtils.import(
83       "resource:///modules/BrowserWindowTracker.jsm"
84     );
85     BrowserWindowTracker.getTopWindow().openWebLinkIn(attribution, "tab");
86   },
88   /**
89    * The list of translation engines and their attributions.
90    */
91   supportedEngines: {
92     Google: "",
93     Bing: "http://aka.ms/MicrosoftTranslatorAttribution",
94     Yandex: "http://translate.yandex.com/",
95   },
97   /**
98    * Fallback engine (currently Google) if the preferences seem confusing.
99    */
100   get defaultEngine() {
101     return Object.keys(this.supportedEngines)[0];
102   },
104   /**
105    * Returns the name of the preferred translation engine.
106    */
107   get translationEngine() {
108     let engine = Services.prefs.getCharPref("browser.translation.engine");
109     return !Object.keys(this.supportedEngines).includes(engine)
110       ? this.defaultEngine
111       : engine;
112   },
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.
127  */
128 class TranslationParent extends JSWindowActorParent {
129   actorCreated() {
130     this._state = 0;
131     this.originalShown = true;
132   }
134   get browser() {
135     return this.browsingContext.top.embedderElement;
136   }
138   receiveMessage(aMessage) {
139     switch (aMessage.name) {
140       case "Translation:DocumentState":
141         this.documentStateReceived(aMessage.data);
142         break;
143     }
144   }
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.
150         return;
151       }
153       if (
154         !Translation.supportedTargetLanguages.includes(aData.detectedLanguage)
155       ) {
156         // Detected language is not part of the supported languages.
157         TranslationTelemetry.recordMissedTranslationOpportunity(
158           aData.detectedLanguage
159         );
160         return;
161       }
163       TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage);
164     }
166     if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
167       return;
168     }
170     // Set all values before showing a new translation infobar.
171     this._state = Translation.serviceUnavailable
172       ? Translation.STATE_UNAVAILABLE
173       : aData.state;
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();
183     }
184   }
186   translate(aFrom, aTo) {
187     if (
188       aFrom == aTo ||
189       (this.state == Translation.STATE_TRANSLATED &&
190         this.translatedFrom == aFrom &&
191         this.translatedTo == aTo)
192     ) {
193       // Nothing to do.
194       return;
195     }
197     if (this.state == Translation.STATE_OFFER) {
198       if (this.detectedLanguage != aFrom) {
199         TranslationTelemetry.recordDetectedLanguageChange(true);
200       }
201     } else {
202       if (this.translatedFrom != aFrom) {
203         TranslationTelemetry.recordDetectedLanguageChange(false);
204       }
205       if (this.translatedTo != aTo) {
206         TranslationTelemetry.recordTargetLanguageChange();
207       }
208     }
210     this.state = Translation.STATE_TRANSLATING;
211     this.translatedFrom = aFrom;
212     this.translatedTo = aTo;
214     this.sendQuery("Translation:TranslateDocument", {
215       from: aFrom,
216       to: aTo,
217     }).then(
218       result => {
219         this.translationFinished(result);
220       },
221       () => {}
222     );
223   }
225   showURLBarIcon() {
226     let chromeWin = this.browser.ownerGlobal;
227     let PopupNotifications = chromeWin.PopupNotifications;
228     let removeId = this.originalShown ? "translated" : "translate";
229     let notification = PopupNotifications.getNotification(
230       removeId,
231       this.browser
232     );
233     if (notification) {
234       PopupNotifications.remove(notification);
235     }
237     let callback = (aTopic, aNewBrowser) => {
238       if (aTopic == "swapping") {
239         let infoBarVisible = this.notificationBox.getNotificationWithValue(
240           "translation"
241         );
242         if (infoBarVisible) {
243           this.showTranslationInfoBar();
244         }
245         return true;
246       }
248       if (aTopic != "showing") {
249         return false;
250       }
251       let translationNotification = this.notificationBox.getNotificationWithValue(
252         "translation"
253       );
254       if (translationNotification) {
255         translationNotification.close();
256       } else {
257         this.showTranslationInfoBar();
258       }
259       return true;
260     };
262     let addId = this.originalShown ? "translate" : "translated";
263     PopupNotifications.show(
264       this.browser,
265       addId,
266       null,
267       addId + "-notification-icon",
268       null,
269       null,
270       { dismissed: true, eventCallback: callback }
271     );
272   }
274   get state() {
275     return this._state;
276   }
278   set state(val) {
279     let notif = this.notificationBox.getNotificationWithValue("translation");
280     if (notif) {
281       notif.state = val;
282     }
283     this._state = val;
284   }
286   showOriginalContent() {
287     this.originalShown = true;
288     this.showURLBarIcon();
289     this.sendAsyncMessage("Translation:ShowOriginal");
290     TranslationTelemetry.recordShowOriginalContent();
291   }
293   showTranslatedContent() {
294     this.originalShown = false;
295     this.showURLBarIcon();
296     this.sendAsyncMessage("Translation:ShowTranslation");
297   }
299   get notificationBox() {
300     return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
301   }
303   showTranslationInfoBar() {
304     let notificationBox = this.notificationBox;
305     let notif = notificationBox.appendNotification("translation", {
306       priority: notificationBox.PRIORITY_INFO_HIGH,
307       notificationIs: "translation-notification",
308     });
309     notif.init(this);
310     return notif;
311   }
313   shouldShowInfoBar(aPrincipal) {
314     // Never show the infobar automatically while the translation
315     // service is temporarily unavailable.
316     if (Translation.serviceUnavailable) {
317       return false;
318     }
320     // Check if we should never show the infobar for this language.
321     let neverForLangs = Services.prefs.getCharPref(
322       "browser.translation.neverForLanguages"
323     );
324     if (neverForLangs.split(",").includes(this.detectedLanguage)) {
325       TranslationTelemetry.recordAutoRejectedTranslationOffer();
326       return false;
327     }
329     // or if we should never show the infobar for this domain.
330     let perms = Services.perms;
331     if (
332       perms.testExactPermissionFromPrincipal(aPrincipal, "translate") ==
333       perms.DENY_ACTION
334     ) {
335       TranslationTelemetry.recordAutoRejectedTranslationOffer();
336       return false;
337     }
339     return true;
340   }
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(
350         result.from,
351         result.to,
352         result.characterCount
353       );
354     } else if (result.unavailable) {
355       Translation.serviceUnavailable = true;
356       this.state = Translation.STATE_UNAVAILABLE;
357     } else {
358       this.state = Translation.STATE_ERROR;
359     }
361     if (Translation.translationListener) {
362       Translation.translationListener();
363     }
364   }
366   infobarClosed() {
367     if (this.state == Translation.STATE_OFFER) {
368       TranslationTelemetry.recordDeniedTranslationOffer();
369     }
370   }
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.
378  */
379 var TranslationTelemetry = {
380   init() {
381     // Constructing histograms.
382     const plain = id => Services.telemetry.getHistogramById(id);
383     const keyed = id => Services.telemetry.getKeyedHistogramById(id);
384     this.HISTOGRAMS = {
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"),
398     };
400     // Capturing the values of flags at the startup.
401     this.recordPreferences();
402   },
404   /**
405    * Record a translation opportunity in the health report.
406    * @param language
407    *        The language of the page.
408    */
409   recordTranslationOpportunity(language) {
410     return this._recordOpportunity(language, true);
411   },
413   /**
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.
417    * @param language
418    *        The language of the page.
419    */
420   recordMissedTranslationOpportunity(language) {
421     return this._recordOpportunity(language, false);
422   },
424   /**
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.
430    *
431    * These translation opportunities should still be recorded in addition to
432    * recording the automatic rejection of the offer.
433    */
434   recordAutoRejectedTranslationOffer() {
435     this.HISTOGRAMS.AUTO_REJECTED().add();
436   },
438   /**
439    * Record a translation in the health report.
440    * @param langFrom
441    *        The language of the page.
442    * @param langTo
443    *        The language translated to
444    * @param numCharacters
445    *        The number of characters that were translated
446    */
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);
451   },
453   /**
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.
457    *
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
463    *        be passed.
464    */
465   recordDetectedLanguageChange(beforeFirstTranslation) {
466     this.HISTOGRAMS.DETECTION_CHANGES().add(beforeFirstTranslation);
467   },
469   /**
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.
473    */
474   recordTargetLanguageChange() {
475     this.HISTOGRAMS.TARGET_CHANGES().add();
476   },
478   /**
479    * Record a denied translation offer.
480    */
481   recordDeniedTranslationOffer() {
482     this.HISTOGRAMS.DENIED().add();
483   },
485   /**
486    * Record a "Show Original" command use.
487    */
488   recordShowOriginalContent() {
489     this.HISTOGRAMS.SHOW_ORIGINAL().add();
490   },
492   /**
493    * Record the state of translation preferences.
494    */
495   recordPreferences() {
496     if (Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
497       this.HISTOGRAMS.SHOW_UI().add(1);
498     }
499     if (Services.prefs.getBoolPref(TRANSLATION_PREF_DETECT_LANG)) {
500       this.HISTOGRAMS.DETECT_LANG().add(1);
501     }
502   },
504   _recordOpportunity(language, success) {
505     this.HISTOGRAMS.OPPORTUNITIES().add(success);
506     this.HISTOGRAMS.OPPORTUNITIES_BY_LANG().add(language, success);
507   },
510 TranslationTelemetry.init();