Bug 971048 - Run language detection on webpages and display infobar when language is not the current UI locale, r=felipe.
authorFlorian Quèze <florian@queze.net>
Thu, 17 Apr 2014 00:02:33 +0200
changeset 198524 7b4cfd722bcd350a9045863a254d8dcd141b7f6f
parent 198523 8c982eaeef86a3c50a3289a8fd3a599b85e3f765
child 198525 a5cee95d219ec9021d5b59d51dde9404293715c2
push id486
push userasasaki@mozilla.com
push dateMon, 14 Jul 2014 18:39:42 +0000
treeherdermozilla-release@d33428174ff1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs971048
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 971048 - Run language detection on webpages and display infobar when language is not the current UI locale, r=felipe.
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/base/content/content.js
browser/components/translation/Translation.jsm
browser/components/translation/test/browser_translation_infobar.js
browser/components/translation/translation-infobar.xml
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1420,16 +1420,18 @@ pref("ui.key.menuAccessKeyFocuses", true
 
 // Delete HTTP cache v2 data of users that didn't opt-in manually
 pref("browser.cache.auto_delete_cache_version", 1);
 // Play with different values of the decay time and get telemetry,
 // 0 means to randomize (and persist) the experiment value in users' profiles,
 // -1 means no experiment is run and we use the preferred value for frecency (6h)
 pref("browser.cache.frecency_experiment", 0);
 
+pref("browser.translation.detectLanguage", false);
+
 // Telemetry experiments settings.
 pref("experiments.enabled", false);
 pref("experiments.manifest.fetchIntervalSeconds", 86400);
 pref("experiments.manifest.uri", "https://telemetry-experiment.cdn.mozilla.net/manifest/v1/firefox/%VERSION%/%CHANNEL%");
 pref("experiments.manifest.certs.1.commonName", "*.cdn.mozilla.net");
 pref("experiments.manifest.certs.1.issuerName", "CN=Cybertrust Public SureServer SV CA,O=Cybertrust Inc");
 // Whether experiments are supported by the current application profile.
 pref("experiments.supported", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -142,16 +142,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gCustomizationTabPreloader",
   "resource:///modules/CustomizationTabPreloader.jsm", "CustomizationTabPreloader");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Translation",
+  "resource:///modules/translation/Translation.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
   "resource:///modules/SitePermissions.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
   "resource://gre/modules/FxAccounts.jsm");
@@ -1028,16 +1031,17 @@ var gBrowserInit = {
     Services.obs.addObserver(gFormSubmitObserver, "invalidformsubmit", false);
 
     BrowserOffline.init();
     OfflineApps.init();
     IndexedDBPromptHelper.init();
     gFormSubmitObserver.init();
     gRemoteTabsUI.init();
     gPageStyleMenu.init();
+    LanguageDetectionListener.init();
 
     // Initialize the full zoom setting.
     // We do this before the session restore service gets initialized so we can
     // apply full zoom settings to tabs restored by the session restore service.
     FullZoom.init();
     PanelUI.init();
     LightweightThemeListener.init();
     WebrtcIndicator.init();
@@ -5337,16 +5341,25 @@ function stylesheetSwitchAll(contentWind
   gPageStyleMenu.switchStyleSheet(title);
 }
 function setStyleDisabled(disabled) {
   if (disabled)
     gPageStyleMenu.disableStyle();
 }
 
 
+var LanguageDetectionListener = {
+  init: function() {
+    window.messageManager.addMessageListener("LanguageDetection:Result", msg => {
+      Translation.languageDetected(msg.target, msg.data);
+    });
+  }
+};
+
+
 var BrowserOffline = {
   _inited: false,
 
   /////////////////////////////////////////////////////////////////////////////
   // BrowserOffline Public Methods
   init: function ()
   {
     if (!this._uiElement)
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -7,16 +7,18 @@ let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
   "resource:///modules/ContentLinkHandler.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
+  "resource:///modules/translation/LanguageDetector.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
   "resource:///modules/UITour.jsm");
@@ -382,8 +384,54 @@ let PageStyleHandler = {
       result.push({title: currentStyleSheet.title,
                    disabled: currentStyleSheet.disabled});
     }
 
     return result;
   },
 };
 PageStyleHandler.init();
+
+let TranslationHandler = {
+  init: function() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+  },
+
+  /* nsIWebProgressListener implementation */
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+    if (!aWebProgress.isTopLevel ||
+        !(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP))
+      return;
+
+    let url = aRequest.name;
+    if (!url.startsWith("http://") && !url.startsWith("https://"))
+      return;
+
+    // Grab a 60k sample of text from the page.
+    let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
+                    .createInstance(Ci.nsIDocumentEncoder);
+    encoder.init(content.document, "text/plain", encoder.SkipInvisibleContent);
+    let string = encoder.encodeToStringWithMaxLength(60 * 1024);
+
+    // Language detection isn't reliable on very short strings.
+    if (string.length < 100)
+      return;
+
+    LanguageDetector.detectLanguage(string).then(result => {
+      if (result.confident)
+        sendAsyncMessage("LanguageDetection:Result", result.language);
+    });
+  },
+
+  // Unused methods.
+  onProgressChange: function() {},
+  onLocationChange: function() {},
+  onStatusChange:   function() {},
+  onSecurityChange: function() {},
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
+
+if (Services.prefs.getBoolPref("browser.translation.detectLanguage"))
+  TranslationHandler.init();
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -6,63 +6,69 @@
 
 this.EXPORTED_SYMBOLS = ["Translation"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
-  "resource:///modules/translation/LanguageDetector.jsm");
+
+this.Translation = {
+  supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
+  supportedTargetLanguages: ["en", "pl", "tr", "vi"],
 
-/* Create an object keeping the information related to translation for
+  _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;
+  },
+
+  languageDetected: function(aBrowser, aDetectedLanguage) {
+    if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) != -1 &&
+        aDetectedLanguage != this.defaultTargetLanguage) {
+      if (!aBrowser.translationUI)
+        aBrowser.translationUI = new TranslationUI(aBrowser);
+
+      aBrowser.translationUI.showTranslationUI(aDetectedLanguage);
+    }
+  }
+};
+
+/* 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:
- * - supportedSourceLanguages, array of supported source language codes
- * - supportedTargetLanguages, array of supported target language codes
  * - detectedLanguage, code of the language detected on the web page.
- * - defaultTargetLanguage, code of the language to use by default for
- *   translation.
  * - state, the state in which the infobar should be displayed
  * - STATE_{OFFER,TRANSLATING,TRANSLATED,ERROR} constants.
  * - 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.
  */
-this.Translation = function(aBrowser) {
+function TranslationUI(aBrowser) {
   this.browser = aBrowser;
-};
+}
 
-this.Translation.prototype = {
-  supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
-  supportedTargetLanguages: ["en", "pl", "tr", "vi"],
-
+TranslationUI.prototype = {
   STATE_OFFER: 0,
   STATE_TRANSLATING: 1,
   STATE_TRANSLATED: 2,
   STATE_ERROR: 3,
 
-  _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;
-  },
-
   get doc() this.browser.contentDocument,
 
   translate: function(aFrom, aTo) {
     this.state = this.STATE_TRANSLATING;
     this.translatedFrom = aFrom;
     this.translatedTo = aTo;
   },
 
--- a/browser/components/translation/test/browser_translation_infobar.js
+++ b/browser/components/translation/test/browser_translation_infobar.js
@@ -24,18 +24,16 @@ function waitForCondition(condition, nex
       moveOn();
     }
     tries++;
   }, 100);
   var moveOn = function() { clearInterval(interval); nextTest(); };
 }
 
 var TranslationStub = {
-  __proto__: Translation.prototype,
-
   translate: function(aFrom, aTo) {
     this.state = this.STATE_TRANSLATING;
     this.translatedFrom = aFrom;
     this.translatedTo = aTo;
   },
 
   _reset: function() {
     this.translatedFrom = "";
@@ -49,16 +47,24 @@ var TranslationStub = {
 
   finishTranslation: function() {
     this.showTranslatedContent();
     this.state = this.STATE_TRANSLATED;
     this._reset();
   }
 };
 
+function showTranslationUI(aDetectedLanguage) {
+  let browser = gBrowser.selectedBrowser;
+  Translation.languageDetected(browser, aDetectedLanguage);
+  let ui = browser.translationUI;
+  for (let name of ["translate", "_reset", "failTranslation", "finishTranslation"])
+    ui[name] = TranslationStub[name];
+  return ui.notificationBox.getNotificationWithValue("translation");
+}
 
 function test() {
   waitForExplicitFinish();
 
   let tab = gBrowser.addTab();
   gBrowser.selectedTab = tab;
   tab.linkedBrowser.addEventListener("load", function onload() {
     tab.linkedBrowser.removeEventListener("load", onload, true);
@@ -78,45 +84,45 @@ function checkURLBarIcon(aExpectTranslat
   is(!PopupNotifications.getNotification("translate"), aExpectTranslated,
      "translate icon " + (aExpectTranslated ? "not " : "") + "shown");
   is(!!PopupNotifications.getNotification("translated"), aExpectTranslated,
      "translated icon " + (aExpectTranslated ? "" : "not ") + "shown");
 }
 
 function run_tests(aFinishCallback) {
   info("Show an info bar saying the current page is in French");
-  let notif = TranslationStub.showTranslationUI("fr");
-  is(notif.state, TranslationStub.STATE_OFFER, "the infobar is offering translation");
+  let notif = showTranslationUI("fr");
+  is(notif.state, notif.translation.STATE_OFFER, "the infobar is offering translation");
   is(notif._getAnonElt("detectedLanguage").value, "fr", "The detected language is displayed");
   checkURLBarIcon();
 
   info("Click the 'Translate' button");
   notif._getAnonElt("translate").click();
-  is(notif.state, TranslationStub.STATE_TRANSLATING, "the infobar is in the translating state");
-  ok(!!TranslationStub.translatedFrom, "Translation.translate has been called");
-  is(TranslationStub.translatedFrom, "fr", "from language correct");
-  is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "from language correct");
+  is(notif.state, notif.translation.STATE_TRANSLATING, "the infobar is in the translating state");
+  ok(!!notif.translation.translatedFrom, "Translation.translate has been called");
+  is(notif.translation.translatedFrom, "fr", "from language correct");
+  is(notif.translation.translatedTo, Translation.defaultTargetLanguage, "from language correct");
   checkURLBarIcon();
 
   info("Make the translation fail and check we are in the error state.");
-  TranslationStub.failTranslation();
-  is(notif.state, TranslationStub.STATE_ERROR, "infobar in the error state");
+  notif.translation.failTranslation();
+  is(notif.state, notif.translation.STATE_ERROR, "infobar in the error state");
   checkURLBarIcon();
 
   info("Click the try again button");
   notif._getAnonElt("tryAgain").click();
-  is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state");
-  ok(!!TranslationStub.translatedFrom, "Translation.translate has been called");
-  is(TranslationStub.translatedFrom, "fr", "from language correct");
-  is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "from language correct");
+  is(notif.state, notif.translation.STATE_TRANSLATING, "infobar in the translating state");
+  ok(!!notif.translation.translatedFrom, "Translation.translate has been called");
+  is(notif.translation.translatedFrom, "fr", "from language correct");
+  is(notif.translation.translatedTo, Translation.defaultTargetLanguage, "from language correct");
   checkURLBarIcon();
 
   info("Make the translation succeed and check we are in the 'translated' state.");
-  TranslationStub.finishTranslation();
-  is(notif.state, TranslationStub.STATE_TRANSLATED, "infobar in the translated state");
+  notif.translation.finishTranslation();
+  is(notif.state, notif.translation.STATE_TRANSLATED, "infobar in the translated state");
   checkURLBarIcon(true);
 
   info("Test 'Show original' / 'Show Translation' buttons.");
   // First check 'Show Original' is visible and 'Show Translation' is hidden.
   ok(!notif._getAnonElt("showOriginal").hidden, "'Show Original' button visible");
   ok(notif._getAnonElt("showTranslation").hidden, "'Show Translation' button hidden");
   // Click the button.
   notif._getAnonElt("showOriginal").click();
@@ -132,55 +138,55 @@ function run_tests(aFinishCallback) {
   // Check that the 'Show Original' button is visible again.
   ok(!notif._getAnonElt("showOriginal").hidden, "'Show Original' button visible");
   ok(notif._getAnonElt("showTranslation").hidden, "'Show Translation' button hidden");
 
   info("Check that changing the source language causes a re-translation");
   let from = notif._getAnonElt("fromLanguage");
   from.value = "es";
   from.doCommand();
-  is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state");
-  ok(!!TranslationStub.translatedFrom, "Translation.translate has been called");
-  is(TranslationStub.translatedFrom, "es", "from language correct");
-  is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "to language correct");
+  is(notif.state, notif.translation.STATE_TRANSLATING, "infobar in the translating state");
+  ok(!!notif.translation.translatedFrom, "Translation.translate has been called");
+  is(notif.translation.translatedFrom, "es", "from language correct");
+  is(notif.translation.translatedTo, Translation.defaultTargetLanguage, "to language correct");
   // We want to show the 'translated' icon while re-translating,
   // because we are still displaying the previous translation.
   checkURLBarIcon(true);
-  TranslationStub.finishTranslation();
+  notif.translation.finishTranslation();
   checkURLBarIcon(true);
 
   info("Check that changing the target language causes a re-translation");
   let to = notif._getAnonElt("toLanguage");
   to.value = "pl";
   to.doCommand();
-  is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state");
-  ok(!!TranslationStub.translatedFrom, "Translation.translate has been called");
-  is(TranslationStub.translatedFrom, "es", "from language correct");
-  is(TranslationStub.translatedTo, "pl", "to language correct");
+  is(notif.state, notif.translation.STATE_TRANSLATING, "infobar in the translating state");
+  ok(!!notif.translation.translatedFrom, "Translation.translate has been called");
+  is(notif.translation.translatedFrom, "es", "from language correct");
+  is(notif.translation.translatedTo, "pl", "to language correct");
   checkURLBarIcon(true);
-  TranslationStub.finishTranslation();
+  notif.translation.finishTranslation();
   checkURLBarIcon(true);
 
   // Cleanup.
   notif.close();
 
   info("Reopen the info bar to check that it's possible to override the detected language.");
-  notif = TranslationStub.showTranslationUI("fr");
-  is(notif.state, TranslationStub.STATE_OFFER, "the infobar is offering translation");
+  notif = showTranslationUI("fr");
+  is(notif.state, notif.translation.STATE_OFFER, "the infobar is offering translation");
   is(notif._getAnonElt("detectedLanguage").value, "fr", "The detected language is displayed");
   // Change the language and click 'Translate'
   notif._getAnonElt("detectedLanguage").value = "ja";
   notif._getAnonElt("translate").click();
-  is(notif.state, TranslationStub.STATE_TRANSLATING, "the infobar is in the translating state");
-  ok(!!TranslationStub.translatedFrom, "Translation.translate has been called");
-  is(TranslationStub.translatedFrom, "ja", "from language correct");
+  is(notif.state, notif.translation.STATE_TRANSLATING, "the infobar is in the translating state");
+  ok(!!notif.translation.translatedFrom, "Translation.translate has been called");
+  is(notif.translation.translatedFrom, "ja", "from language correct");
   notif.close();
 
   info("Reopen to check the 'Not Now' button closes the notification.");
-  notif = TranslationStub.showTranslationUI("fr");
+  notif = showTranslationUI("fr");
   let notificationBox = gBrowser.getNotificationBox();
   ok(!!notificationBox.getNotificationWithValue("translation"), "there's a 'translate' notification");
   notif._getAnonElt("notNow").click();
   ok(!notificationBox.getNotificationWithValue("translation"), "no 'translate' notification after clicking 'not now'");
 
   info("Check that clicking the url bar icon reopens the info bar");
   checkURLBarIcon();
   // Clicking the anchor element causes a 'showing' event to be sent
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -97,30 +97,30 @@
             this.translation = aTranslation;
             let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
                            .getService(Ci.nsIStringBundleService)
                            .createBundle("chrome://global/locale/languageNames.properties");
 
             // Fill the lists of supported source languages.
             let detectedLanguage = this._getAnonElt("detectedLanguage");
             let fromLanguage = this._getAnonElt("fromLanguage");
-            for (let code of this.translation.supportedSourceLanguages) {
+            for (let code of Translation.supportedSourceLanguages) {
               let name = bundle.GetStringFromName(code);
               detectedLanguage.appendItem(name, code);
               fromLanguage.appendItem(name, code);
             }
             detectedLanguage.value = this.translation.detectedLanguage;
 
             // translatedFrom is only set if we have already translated this page.
             if (aTranslation.translatedFrom)
               fromLanguage.value = aTranslation.translatedFrom;
 
             // Fill the list of supporter target languages.
             let toLanguage = this._getAnonElt("toLanguage");
-            for (let code of this.translation.supportedTargetLanguages)
+            for (let code of Translation.supportedTargetLanguages)
               toLanguage.appendItem(bundle.GetStringFromName(code), code);
 
             if (aTranslation.translatedTo)
               toLanguage.value = aTranslation.translatedTo;
 
             if (aTranslation.state)
               this.state = aTranslation.state;
 
@@ -138,17 +138,17 @@
 
       <method name="translate">
         <body>
           <![CDATA[
             if (this.state == this.translation.STATE_OFFER) {
               this._getAnonElt("fromLanguage").value =
                 this._getAnonElt("detectedLanguage").value;
               this._getAnonElt("toLanguage").value =
-                this.translation.defaultTargetLanguage;
+                Translation.defaultTargetLanguage;
             }
 
             this._handleButtonHiding(false);
             this.translation.translate(this._getAnonElt("fromLanguage").value,
                                        this._getAnonElt("toLanguage").value);
           ]]>
         </body>
       </method>