Bug 1488232 - add a Google translation backend, r=felipe.
authorFlorian Quèze <florian@queze.net>
Mon, 03 Sep 2018 19:15:56 +0200
changeset 434504 a3eb8e502006
parent 434503 d8ff6c1c6f3a
child 434505 5a1f07f8ca7c
push id107399
push userflorian@queze.net
push dateMon, 03 Sep 2018 17:17:37 +0000
treeherdermozilla-inbound@a3eb8e502006 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs1488232
milestone63.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 1488232 - add a Google translation backend, r=felipe.
browser/app/profile/firefox.js
browser/base/content/test/static/browser_all_files_referenced.js
browser/components/preferences/in-content/main.js
browser/components/translation/GoogleTranslator.jsm
browser/components/translation/Translation.jsm
browser/components/translation/TranslationContentHandler.jsm
browser/components/translation/moz.build
browser/components/translation/test/browser_translation_yandex.js
browser/components/translation/translation-infobar.xml
testing/profiles/unittest/user.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1459,18 +1459,18 @@ pref("media.autoplay.ask-permission", fa
 // 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);
 pref("browser.translation.neverForLanguages", "");
 // Show the translation UI bits, like the info bar, notification icon and preferences.
 pref("browser.translation.ui.show", false);
-// Allows to define the translation engine. Bing is default, Yandex may optionally switched on.
-pref("browser.translation.engine", "bing");
+// Allows to define the translation engine. Google is default, Bing or Yandex are other options.
+pref("browser.translation.engine", "Google");
 
 // Telemetry settings.
 // Determines if Telemetry pings can be archived locally.
 pref("toolkit.telemetry.archive.enabled", true);
 // Enables sending the shutdown ping when Firefox shuts down.
 pref("toolkit.telemetry.shutdownPingSender.enabled", true);
 // Enables sending the shutdown ping using the pingsender from the first session.
 pref("toolkit.telemetry.shutdownPingSender.enabledFirstSession", false);
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -113,16 +113,21 @@ var whitelist = [
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/intl.properties",
    platforms: ["linux", "macosx"]},
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties",
    platforms: ["linux", "macosx"]},
 
   // browser/extensions/pdfjs/content/web/viewer.js#7450
   {file: "resource://pdf.js/web/debugger.js"},
 
+  // resource://app/modules/translation/TranslationContentHandler.jsm
+  {file: "resource://app/modules/translation/BingTranslator.jsm"},
+  {file: "resource://app/modules/translation/GoogleTranslator.jsm"},
+  {file: "resource://app/modules/translation/YandexTranslator.jsm"},
+
   // Starting from here, files in the whitelist are bugs that need fixing.
   // Bug 1339424 (wontfix?)
   {file: "chrome://browser/locale/taskbar.properties",
    platforms: ["linux", "macosx"]},
   // Bug 1356031 (only used by devtools)
   {file: "chrome://global/skin/icons/error-16.png"},
   // Bug 1348362
   {file: "chrome://global/skin/icons/warning-64.png", platforms: ["linux"]},
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -423,17 +423,17 @@ var gMainPane = {
 
     // Show translation preferences if we may:
     const prefName = "browser.translation.ui.show";
     if (Services.prefs.getBoolPref(prefName)) {
       let row = document.getElementById("translationBox");
       row.removeAttribute("hidden");
       // Showing attribution only for Bing Translator.
       ChromeUtils.import("resource:///modules/translation/Translation.jsm");
-      if (Translation.translationEngine == "bing") {
+      if (Translation.translationEngine == "Bing") {
         document.getElementById("bingAttribution").removeAttribute("hidden");
       }
     }
 
     if (AppConstants.MOZ_DEV_EDITION) {
       let uAppData = OS.Constants.Path.userApplicationDataDir;
       let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
 
copy from browser/components/translation/BingTranslator.jsm
copy to browser/components/translation/GoogleTranslator.jsm
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/GoogleTranslator.jsm
@@ -1,149 +1,128 @@
 /* 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 = [ "BingTranslator" ];
+var EXPORTED_SYMBOLS = [ "GoogleTranslator" ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
-ChromeUtils.import("resource://services-common/async.js");
 ChromeUtils.import("resource://gre/modules/Http.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
+XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser"]);
 
-// The maximum amount of net data allowed per request on Bing's API.
-const MAX_REQUEST_DATA = 5000; // Documentation says 10000 but anywhere
-                               // close to that is refused by the service.
+// The maximum amount of net data allowed per request on Google's API.
+const MAX_REQUEST_DATA = 5000; // XXX This is the Bing value
 
 // The maximum number of chunks allowed to be translated in a single
 // request.
-const MAX_REQUEST_CHUNKS = 1000; // Documentation says 2000.
+const MAX_REQUEST_CHUNKS = 128; // Undocumented, but the de facto upper limit.
 
 // Self-imposed limit of 15 requests. This means that a page that would need
 // to be broken in more than 15 requests won't be fully translated.
 // The maximum amount of data that we will translate for a single page
 // is MAX_REQUESTS * MAX_REQUEST_DATA.
 const MAX_REQUESTS = 15;
 
+const URL = "https://translation.googleapis.com/language/translate/v2";
+
 /**
- * Translates a webpage using Bing's Translation API.
+ * Translates a webpage using Google's Translation API.
  *
  * @param translationDocument  The TranslationDocument object that represents
  *                             the webpage to be translated
  * @param sourceLanguage       The source language of the document
  * @param targetLanguage       The target language for the translation
  *
  * @returns {Promise}          A promise that will resolve when the translation
  *                             task is finished.
  */
-var BingTranslator = function(translationDocument, sourceLanguage, targetLanguage) {
+var GoogleTranslator = function(translationDocument, sourceLanguage, targetLanguage) {
   this.translationDocument = translationDocument;
   this.sourceLanguage = sourceLanguage;
   this.targetLanguage = targetLanguage;
   this._pendingRequests = 0;
   this._partialSuccess = false;
-  this._serviceUnavailable = false;
   this._translatedCharacterCount = 0;
 };
 
-this.BingTranslator.prototype = {
+this.GoogleTranslator.prototype = {
   /**
    * Performs the translation, splitting the document into several chunks
    * respecting the data limits of the API.
    *
    * @returns {Promise}          A promise that will resolve when the translation
    *                             task is finished.
    */
-  translate() {
-    return (async () => {
-      let currentIndex = 0;
-      this._onFinishedDeferred = PromiseUtils.defer();
+  async translate() {
+    let currentIndex = 0;
+    this._onFinishedDeferred = PromiseUtils.defer();
 
-      // Let's split the document into various requests to be sent to
-      // Bing's Translation API.
-      for (let requestCount = 0; requestCount < MAX_REQUESTS; requestCount++) {
-        // Generating the text for each request can be expensive, so
-        // let's take the opportunity of the chunkification process to
-        // allow for the event loop to attend other pending events
-        // before we continue.
-        await Async.promiseYield();
+    // Let's split the document into various requests to be sent to
+    // Google's Translation API.
+    for (let requestCount = 0; requestCount < MAX_REQUESTS; requestCount++) {
+      // Generating the text for each request can be expensive, so
+      // let's take the opportunity of the chunkification process to
+      // allow for the event loop to attend other pending events
+      // before we continue.
+      await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
 
-        // Determine the data for the next request.
-        let request = this._generateNextTranslationRequest(currentIndex);
+      // Determine the data for the next request.
+      let request = this._generateNextTranslationRequest(currentIndex);
 
-        // Create a real request to the server, and put it on the
-        // pending requests list.
-        let bingRequest = new BingRequest(request.data,
-                                          this.sourceLanguage,
-                                          this.targetLanguage);
-        this._pendingRequests++;
-        bingRequest.fireRequest().then(this._chunkCompleted.bind(this),
+      // Create a real request to the server, and put it on the
+      // pending requests list.
+      let googleRequest = new GoogleRequest(request.data,
+                                            this.sourceLanguage,
+                                            this.targetLanguage);
+      this._pendingRequests++;
+      googleRequest.fireRequest().then(this._chunkCompleted.bind(this),
                                        this._chunkFailed.bind(this));
 
-        currentIndex = request.lastIndex;
-        if (request.finished) {
-          break;
-        }
+      currentIndex = request.lastIndex;
+      if (request.finished) {
+        break;
       }
+    }
 
-      return this._onFinishedDeferred.promise;
-    })();
-  },
-
-  /**
-   * Resets the expiration time of the current token, in order to
-   * force the token manager to ask for a new token during the next request.
-   */
-  _resetToken() {
-    // Force the token manager to get update token
-    BingTokenManager._currentExpiryTime = 0;
+    return this._onFinishedDeferred.promise;
   },
 
   /**
    * Function called when a request sent to the server completed successfully.
    * This function handles calling the function to parse the result and the
    * function to resolve the promise returned by the public `translate()`
    * method when there's no pending request left.
    *
-   * @param   request   The BingRequest sent to the server.
+   * @param   request   The GoogleRequest sent to the server.
    */
-  _chunkCompleted(bingRequest) {
-    if (this._parseChunkResult(bingRequest)) {
+  _chunkCompleted(googleRequest) {
+    if (this._parseChunkResult(googleRequest)) {
       this._partialSuccess = true;
       // Count the number of characters successfully translated.
-      this._translatedCharacterCount += bingRequest.characterCount;
+      this._translatedCharacterCount += googleRequest.characterCount;
     }
 
     this._checkIfFinished();
   },
 
   /**
    * Function called when a request sent to the server has failed.
    * This function handles deciding if the error is transient or means the
    * service is unavailable (zero balance on the key or request credentials are
    * not in an active state) and calling the function to resolve the promise
    * returned by the public `translate()` method when there's no pending.
    * request left.
    *
    * @param   aError   [optional] The XHR object of the request that failed.
    */
   _chunkFailed(aError) {
-    if (aError instanceof XMLHttpRequest &&
-        [400, 401].includes(aError.status)) {
-      let body = aError.responseText;
-      if (body && body.includes("TranslateApiException") &&
-          (body.includes("balance") || body.includes("active state")))
-        this._serviceUnavailable = true;
-    }
-
     this._checkIfFinished();
   },
 
   /**
    * Function called when a request sent to the server has completed.
    * This function handles resolving the promise
    * returned by the public `translate()` method when all chunks are completed.
    */
@@ -155,61 +134,58 @@ this.BingTranslator.prototype = {
     // display the "Success" state for the infobar. Otherwise,
     // the "Error" state will appear.
     if (--this._pendingRequests == 0) {
       if (this._partialSuccess) {
         this._onFinishedDeferred.resolve({
           characterCount: this._translatedCharacterCount,
         });
       } else {
-        let error = this._serviceUnavailable ? "unavailable" : "failure";
-        this._onFinishedDeferred.reject(error);
+        this._onFinishedDeferred.reject("failure");
       }
     }
   },
 
   /**
    * This function parses the result returned by Bing's Http.svc API,
    * which is a XML file that contains a number of elements. To our
    * particular interest, the only part of the response that matters
    * are the <TranslatedText> nodes, which contains the resulting
    * items that were sent to be translated.
    *
    * @param   request      The request sent to the server.
    * @returns boolean      True if parsing of this chunk was successful.
    */
-  _parseChunkResult(bingRequest) {
+  _parseChunkResult(googleRequest) {
     let results;
     try {
-      let doc = bingRequest.networkRequest.responseXML;
-      results = doc.querySelectorAll("TranslatedText");
+      let response = googleRequest.networkRequest.response;
+      results = JSON.parse(response).data.translations;
     } catch (e) {
       return false;
     }
-
     let len = results.length;
-    if (len != bingRequest.translationData.length) {
+    if (len != googleRequest.translationData.length) {
       // This should never happen, but if the service returns a different number
       // of items (from the number of items submitted), we can't use this chunk
       // because all items would be paired incorrectly.
       return false;
     }
 
     let error = false;
     for (let i = 0; i < len; i++) {
       try {
-        let result = results[i].firstChild.nodeValue;
-        let root = bingRequest.translationData[i][0];
-
-        if (root.isSimpleRoot) {
-          // Workaround for Bing's service problem in which "&" chars in
-          // plain-text TranslationItems are double-escaped.
-          result = result.replace(/&amp;/g, "&");
+        let result = results[i].translatedText;
+        let root = googleRequest.translationData[i][0];
+        if (root.isSimpleRoot && result.includes("&")) {
+          // If the result contains HTML entities, we need to convert them as
+          // simple roots expect a plain text result.
+          let doc = (new DOMParser()).parseFromString(result, "text/html");
+          result = doc.body.firstChild.nodeValue;
         }
-
         root.parseResult(result);
       } catch (e) { error = true; }
     }
 
     return !error;
   },
 
   /**
@@ -227,17 +203,16 @@ this.BingTranslator.prototype = {
 
     for (let i = startIndex; i < rootsList.length; i++) {
       let root = rootsList[i];
       let text = this.translationDocument.generateTextForItem(root);
       if (!text) {
         continue;
       }
 
-      text = escapeXML(text);
       let newCurSize = currentDataSize + text.length;
       let newChunks = currentChunks + 1;
 
       if (newCurSize > MAX_REQUEST_DATA ||
           newChunks > MAX_REQUEST_CHUNKS) {
 
         // If we've reached the API limits, let's stop accumulating data
         // for this request and return. We return information useful for
@@ -259,189 +234,64 @@ this.BingTranslator.prototype = {
       data: output,
       finished: true,
       lastIndex: 0,
     };
   },
 };
 
 /**
- * Represents a request (for 1 chunk) sent off to Bing's service.
+ * Represents a request (for 1 chunk) sent off to Google's service.
  *
  * @params translationData  The data to be used for this translation,
  *                          generated by the generateNextTranslationRequest...
  *                          function.
  * @param sourceLanguage    The source language of the document.
  * @param targetLanguage    The target language for the translation.
  *
  */
-function BingRequest(translationData, sourceLanguage, targetLanguage) {
+function GoogleRequest(translationData, sourceLanguage, targetLanguage) {
   this.translationData = translationData;
   this.sourceLanguage = sourceLanguage;
   this.targetLanguage = targetLanguage;
   this.characterCount = 0;
 }
 
-BingRequest.prototype = {
+GoogleRequest.prototype = {
   /**
    * Initiates the request
    */
   fireRequest() {
-    return (async () => {
-      // Prepare authentication.
-      let token = await BingTokenManager.getToken();
-      let auth = "Bearer " + token;
-
-      // Prepare URL.
-      let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
-                            "browser.translation.bing.translateArrayURL");
-
-      // Prepare request headers.
-      let headers = [["Content-type", "text/xml"], ["Authorization", auth]];
-
-      // Prepare the request body.
-      let requestString =
-        "<TranslateArrayRequest>" +
-          "<AppId/>" +
-          "<From>" + this.sourceLanguage + "</From>" +
-          "<Options>" +
-            '<ContentType xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2">text/html</ContentType>' +
-            '<ReservedFlags xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" />' +
-          "</Options>" +
-          '<Texts xmlns:s="http://schemas.microsoft.com/2003/10/Serialization/Arrays">';
-
-      for (let [, text] of this.translationData) {
-        requestString += "<s:string>" + text + "</s:string>";
-        this.characterCount += text.length;
-      }
-
-      requestString += "</Texts>" +
-          "<To>" + this.targetLanguage + "</To>" +
-        "</TranslateArrayRequest>";
+    let key = Services.cpmm.sharedData.get("translationKey") ||
+              Services.prefs.getStringPref("browser.translation.google.apiKey", "");
+    if (!key) {
+      return Promise.reject("no API key");
+    }
 
-      // Set up request options.
-      return new Promise((resolve, reject) => {
-        let options = {
-          onLoad: (responseText, xhr) => {
-            resolve(this);
-          },
-          onError(e, responseText, xhr) {
-            reject(xhr);
-          },
-          postData: requestString,
-          headers,
-        };
-
-        // Fire the request.
-        let request = httpRequest(url, options);
+    // Prepare the request body.
+    let postData = [
+      ["key", key],
+      ["source", this.sourceLanguage],
+      ["target", this.targetLanguage],
+    ];
 
-        // Override the response MIME type.
-        request.overrideMimeType("text/xml");
-        this.networkRequest = request;
-      });
-    })();
-  },
-};
-
-/**
- * Authentication Token manager for the API
- */
-var BingTokenManager = {
-  _currentToken: null,
-  _currentExpiryTime: 0,
-  _pendingRequest: null,
-
-  /**
-   * Get a valid, non-expired token to be used for the API calls.
-   *
-   * @returns {Promise}  A promise that resolves with the token
-   *                     string once it is obtained. The token returned
-   *                     can be the same one used in the past if it is still
-   *                     valid.
-   */
-  getToken() {
-    if (this._pendingRequest) {
-      return this._pendingRequest;
+    for (let [, text] of this.translationData) {
+      postData.push(["q", text]);
+      this.characterCount += text.length;
     }
 
-    let remainingMs = this._currentExpiryTime - new Date();
-    // Our existing token is still good for more than a minute, let's use it.
-    if (remainingMs > 60 * 1000) {
-      return Promise.resolve(this._currentToken);
-    }
-
-    return this._getNewToken();
-  },
-
-  /**
-   * Generates a new token from the server.
-   *
-   * @returns {Promise}  A promise that resolves with the token
-   *                     string once it is obtained.
-   */
-  _getNewToken() {
-    let url = getUrlParam("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13",
-                          "browser.translation.bing.authURL");
-    let params = [
-      ["grant_type", "client_credentials"],
-      ["scope", "http://api.microsofttranslator.com"],
-      ["client_id",
-      getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride")],
-      ["client_secret",
-      getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")],
-    ];
-
-    this._pendingRequest = new Promise((resolve, reject) => {
+    // Set up request options.
+    return new Promise((resolve, reject) => {
       let options = {
-        onLoad(responseText, xhr) {
-          BingTokenManager._pendingRequest = null;
-          try {
-            let json = JSON.parse(responseText);
-
-            if (json.error) {
-              reject(json.error);
-              return;
-            }
-
-            let token = json.access_token;
-            let expires_in = json.expires_in;
-            BingTokenManager._currentToken = token;
-            BingTokenManager._currentExpiryTime = new Date(Date.now() + expires_in * 1000);
-            resolve(token);
-          } catch (e) {
-            reject(e);
-          }
+        onLoad: (responseText, xhr) => {
+          resolve(this);
         },
         onError(e, responseText, xhr) {
-          BingTokenManager._pendingRequest = null;
-          reject(e);
+          reject(xhr);
         },
-        postData: params,
+        postData,
       };
 
-      httpRequest(url, options);
+      // Fire the request.
+      this.networkRequest = httpRequest(URL, options);
     });
-    return this._pendingRequest;
   },
 };
-
-/**
- * Escape a string to be valid XML content.
- */
-function escapeXML(aStr) {
-  return aStr.toString()
-             .replace(/&/g, "&amp;")
-             .replace(/\"/g, "&quot;")
-             .replace(/\'/g, "&apos;")
-             .replace(/</g, "&lt;")
-             .replace(/>/g, "&gt;");
-}
-
-/**
- * Fetch an auth token (clientID or client secret), which may be overridden by
- * a pref if it's set.
- */
-function getUrlParam(paramValue, prefName) {
-  if (Services.prefs.getPrefType(prefName))
-    paramValue = Services.prefs.getCharPref(prefName);
-  paramValue = Services.urlFormatter.formatURL(paramValue);
-  return paramValue;
-}
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -77,26 +77,26 @@ var Translation = {
     ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
     BrowserWindowTracker.getTopWindow().openWebLinkIn(attribution, "tab");
   },
 
   /**
    * The list of translation engines and their attributions.
    */
   supportedEngines: {
-    "bing": "http://aka.ms/MicrosoftTranslatorAttribution",
-    "yandex": "http://translate.yandex.com/",
+    "Google": "",
+    "Bing": "http://aka.ms/MicrosoftTranslatorAttribution",
+    "Yandex": "http://translate.yandex.com/",
   },
 
   /**
-   * Fallback engine (currently Bing Translator) if the preferences seem
-   * confusing.
+   * Fallback engine (currently Google) if the preferences seem confusing.
    */
   get defaultEngine() {
-    return this.supportedEngines.keys[0];
+    return Object.keys(this.supportedEngines)[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;
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -59,19 +59,24 @@ TranslationContentHandler.prototype = {
 
   /* nsIWebProgressListener implementation */
   onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
     if (!aWebProgress.isTopLevel ||
         !(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
         !this.global.content)
       return;
 
-    let url = aRequest.name;
-    if (!url.startsWith("http://") && !url.startsWith("https://"))
+    try {
+      let url = aRequest.name;
+      if (!url.startsWith("http://") && !url.startsWith("https://"))
+        return;
+    } catch (e) {
+      // nsIRequest.name throws NS_ERROR_NOT_IMPLEMENTED for view-source: tabs.
       return;
+    }
 
     let content = this.global.content;
     if (content.detectedLanguage)
       return;
 
     // Grab a 60k sample of text from the page.
     let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
                     .createInstance(Ci.nsIDocumentEncoder);
@@ -115,29 +120,22 @@ TranslationContentHandler.prototype = {
 
         // If a TranslationDocument already exists for this document, it should
         // be used instead of creating a new one so that we can use the original
         // content of the page for the new translation instead of the newly
         // translated text.
         let translationDocument = this.global.content.translationDocument ||
                                   new TranslationDocument(this.global.content.document);
 
-        let preferredEngine = Services.prefs.getCharPref("browser.translation.engine");
-        let translator = null;
-        if (preferredEngine == "yandex") {
-          ChromeUtils.import("resource:///modules/translation/YandexTranslator.jsm");
-          translator = new YandexTranslator(translationDocument,
-                                            msg.data.from,
-                                            msg.data.to);
-        } else {
-          ChromeUtils.import("resource:///modules/translation/BingTranslator.jsm");
-          translator = new BingTranslator(translationDocument,
-                                          msg.data.from,
-                                          msg.data.to);
-        }
+        let engine = Services.prefs.getCharPref("browser.translation.engine");
+        let importScope =
+          ChromeUtils.import(`resource:///modules/translation/${engine}Translator.jsm`, {});
+        let translator = new importScope[engine + "Translator"](translationDocument,
+                                                                msg.data.from,
+                                                                msg.data.to);
 
         this.global.content.translationDocument = translationDocument;
         translationDocument.translatedFrom = msg.data.from;
         translationDocument.translatedTo = msg.data.to;
         translationDocument.translationError = false;
 
         translator.translate().then(
           result => {
--- a/browser/components/translation/moz.build
+++ b/browser/components/translation/moz.build
@@ -4,16 +4,17 @@
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Translation")
 
 EXTRA_JS_MODULES.translation = [
     'BingTranslator.jsm',
     'cld2/cld-worker.js',
     'cld2/cld-worker.js.mem',
+    'GoogleTranslator.jsm',
     'LanguageDetector.jsm',
     'Translation.jsm',
     'TranslationContentHandler.jsm',
     'TranslationDocument.jsm',
     'YandexTranslator.jsm'
 ]
 
 JAR_MANIFESTS += ['jar.mn']
--- a/browser/components/translation/test/browser_translation_yandex.js
+++ b/browser/components/translation/test/browser_translation_yandex.js
@@ -16,17 +16,17 @@ PromiseTestUtils.whitelistRejectionsGlob
 
 const kEnginePref = "browser.translation.engine";
 const kApiKeyPref = "browser.translation.yandex.apiKeyOverride";
 const kShowUIPref = "browser.translation.ui.show";
 
 const {Translation} = ChromeUtils.import("resource:///modules/translation/Translation.jsm", {});
 
 add_task(async function setup() {
-  Services.prefs.setCharPref(kEnginePref, "yandex");
+  Services.prefs.setCharPref(kEnginePref, "Yandex");
   Services.prefs.setCharPref(kApiKeyPref, "yandexValidKey");
   Services.prefs.setBoolPref(kShowUIPref, true);
 
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref(kEnginePref);
     Services.prefs.clearUserPref(kApiKeyPref);
     Services.prefs.clearUserPref(kShowUIPref);
   });
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -212,18 +212,32 @@
               toLanguage.value = aTranslation.translatedTo;
 
             if (aTranslation.state)
               this.state = aTranslation.state;
 
             // Show attribution for the preferred translator.
             let engineIndex = Object.keys(Translation.supportedEngines)
               .indexOf(Translation.translationEngine);
+            // We currently only have attribution for the Bing and Yandex engines.
+            if (engineIndex >= 0) {
+              --engineIndex;
+            }
+            let attributionNode = this._getAnonElt("translationEngine");
             if (engineIndex != -1) {
-              this._getAnonElt("translationEngine").selectedIndex = engineIndex;
+              attributionNode.selectedIndex = engineIndex;
+            } else {
+              // Hide the attribution menuitem
+              let footer = attributionNode.parentNode;
+              footer.hidden = true;
+              // Make the 'Translation preferences' item the new footer.
+              footer = footer.previousSibling;
+              footer.setAttribute("class", "subviewbutton panel-subview-footer");
+              // And hide the menuseparator.
+              footer.previousSibling.hidden = true;
             }
 
             const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
             if (Services.prefs.prefHasUserValue(kWelcomePref) ||
                 this.translation.browser != gBrowser.selectedBrowser)
               return;
 
             this.addEventListener("transitionend", function() {
--- a/testing/profiles/unittest/user.js
+++ b/testing/profiles/unittest/user.js
@@ -58,17 +58,17 @@ user_pref("browser.tabs.delayHidingAudio
 // Don't allow background tabs to be zombified, otherwise for tests that
 // open additional tabs, the test harness tab itself might get unloaded.
 user_pref("browser.tabs.disableBackgroundZombification", true);
 // Don't use auto-enabled e10s
 user_pref("browser.tabs.remote.autostart", false);
 // Make sure Translation won't hit the network.
 user_pref("browser.translation.bing.authURL", "http://{server}/browser/browser/components/translation/test/bing.sjs");
 user_pref("browser.translation.bing.translateArrayURL", "http://{server}/browser/browser/components/translation/test/bing.sjs");
-user_pref("browser.translation.engine", "bing");
+user_pref("browser.translation.engine", "Bing");
 user_pref("browser.translation.yandex.translateURLOverride", "http://{server}/browser/browser/components/translation/test/yandex.sjs");
 user_pref("browser.ui.layout.tablet", 0); // force tablet UI off
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://{server}/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://{server}/uitour-dummy/tour");
 user_pref("browser.urlbar.speculativeConnect.enabled", false);
 // Turn off search suggestions in the location bar so as not to trigger network
 // connections.