merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 04 Jun 2014 15:35:43 +0200
changeset 206796 49b4b6cf5de8bc582925b8aaa0a12b6beb75f906
parent 206789 4faf95ee362021845d3045269d9875283007caed (current diff)
parent 206795 875df781fa8bf83c4f6b056706269ec75daf258a (diff)
child 206821 c7fdd7e755cdea42b483ea570cf4c77b7c40b2a9
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone32.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
merge fx-team to mozilla-central a=merge
browser/components/translation/test/test_cld2.js
browser/components/translation/test/xpcshell.ini
browser/devtools/projecteditor/lib/plugins/app-manager/lib/app-project-editor.js
browser/devtools/projecteditor/lib/plugins/app-manager/lib/plugin.js
browser/devtools/projecteditor/lib/plugins/delete/lib/delete.js
browser/devtools/projecteditor/lib/plugins/dirty/lib/dirty.js
browser/devtools/projecteditor/lib/plugins/image-view/lib/image-editor.js
browser/devtools/projecteditor/lib/plugins/image-view/lib/plugin.js
browser/devtools/projecteditor/lib/plugins/logging/lib/logging.js
browser/devtools/projecteditor/lib/plugins/new/lib/new.js
browser/devtools/projecteditor/lib/plugins/save/lib/save.js
browser/devtools/projecteditor/lib/plugins/status-bar/lib/plugin.js
--- a/browser/components/migration/content/migration.js
+++ b/browser/components/migration/content/migration.js
@@ -132,17 +132,17 @@ var MigrationWizard = {
       if (this._autoMigrate)
         this._wiz.currentPage.next = "homePageImport";
       else
         this._wiz.currentPage.next = "importItems";
 
       if (sourceProfiles && sourceProfiles.length == 1)
         this._selectedProfile = sourceProfiles[0];
       else
-        this._selectedProfile = "";
+        this._selectedProfile = null;
     }
   },
   
   // 2 - [Profile Selection]
   onSelectProfilePageShow: function ()
   {
     // Disabling this for now, since we ask about import sources in automigration
     // too and don't want to disable the back button
@@ -152,38 +152,43 @@ var MigrationWizard = {
     var profiles = document.getElementById("profiles");
     while (profiles.hasChildNodes()) 
       profiles.removeChild(profiles.firstChild);
     
     // Note that this block is still reached even if the user chose 'From File'
     // and we canceled the dialog.  When that happens, _migrator will be null.
     if (this._migrator) {
       var sourceProfiles = this._migrator.sourceProfiles;
-      for (var i = 0; i < sourceProfiles.length; ++i) {
+
+      for (let profile of sourceProfiles) {
         var item = document.createElement("radio");
-        item.id = sourceProfiles[i];
-        item.setAttribute("label", sourceProfiles[i]);
+        item.id = profile.id;
+        item.setAttribute("label", profile.name);
         profiles.appendChild(item);
       }
     }
     
-    profiles.selectedItem = this._selectedProfile ? document.getElementById(this._selectedProfile) : profiles.firstChild;
+    profiles.selectedItem = this._selectedProfile ? document.getElementById(this._selectedProfile.id) : profiles.firstChild;
   },
   
   onSelectProfilePageRewound: function ()
   {
     var profiles = document.getElementById("profiles");
-    this._selectedProfile = profiles.selectedItem.id;
+    this._selectedProfile = this._migrator.sourceProfiles.find(
+      profile => profile.id == profiles.selectedItem.id
+    ) || null;
   },
   
   onSelectProfilePageAdvanced: function ()
   {
     var profiles = document.getElementById("profiles");
-    this._selectedProfile = profiles.selectedItem.id;
-    
+    this._selectedProfile = this._migrator.sourceProfiles.find(
+      profile => profile.id == profiles.selectedItem.id
+    ) || null;
+
     // If we're automigrating or just doing bookmarks don't show the item selection page
     if (this._autoMigrate)
       this._wiz.currentPage.next = "homePageImport";
   },
   
   // 3 - ImportItems
   onImportItemsPageShow: function ()
   {
--- a/browser/components/migration/public/nsIBrowserProfileMigrator.idl
+++ b/browser/components/migration/public/nsIBrowserProfileMigrator.idl
@@ -3,17 +3,17 @@
  * 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/. */
 
 #include "nsISupports.idl"
 
 interface nsIArray;
 interface nsIProfileStartup;
 
-[scriptable, uuid(44993E0E-74E8-4BEC-9D66-AD8156E0A274)]
+[scriptable, uuid(30e5a7ec-f71e-4f41-9dbd-7429c02132ec)]
 interface nsIBrowserProfileMigrator : nsISupports
 {
   /**
    * profile items to migrate. use with migrate().
    */
   const unsigned short ALL         = 0x0000;
   const unsigned short SETTINGS    = 0x0001;
   const unsigned short COOKIES     = 0x0002;
@@ -25,28 +25,28 @@ interface nsIBrowserProfileMigrator : ns
   const unsigned short SESSION     = 0x0080;
 
   /**
    * Copy user profile information to the current active profile.
    * @param aItems   list of data items to migrate. see above for values.
    * @param aStartup helper interface which is non-null if called during startup. 
    * @param aProfile profile to migrate from, if there is more than one.
    */
-  void migrate(in unsigned short aItems, in nsIProfileStartup aStartup, in wstring aProfile);
+  void migrate(in unsigned short aItems, in nsIProfileStartup aStartup, in jsval aProfile);
 
   /**
    * A bit field containing profile items that this migrator
    * offers for import. 
    * @param   aProfile the profile that we are looking for available data
    *          to import
    * @param   aDoingStartup "true" if the profile is not currently being used.
    * @return  bit field containing profile items (see above)
    * @note    a return value of 0 represents no items rather than ALL.
    */
-  unsigned short getMigrateData(in wstring aProfile, in boolean aDoingStartup);
+  unsigned short getMigrateData(in jsval aProfile, in boolean aDoingStartup);
 
   /** 
    * Whether or not there is any data that can be imported from this 
    * browser (i.e. whether or not it is installed, and there exists
    * a user profile)
    */
   readonly attribute boolean          sourceExists;
 
--- a/browser/components/migration/src/ChromeProfileMigrator.js
+++ b/browser/components/migration/src/ChromeProfileMigrator.js
@@ -88,17 +88,17 @@ function ChromeProfileMigrator() {
 }
 
 ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
 
 ChromeProfileMigrator.prototype.getResources =
   function Chrome_getResources(aProfile) {
     if (this._chromeUserDataFolder) {
       let profileFolder = this._chromeUserDataFolder.clone();
-      profileFolder.append(aProfile);
+      profileFolder.append(aProfile.id);
       if (profileFolder.exists()) {
         let possibleResources = [GetBookmarksResource(profileFolder),
                                  GetHistoryResource(profileFolder),
                                  GetCookiesResource(profileFolder)];
         return [r for each (r in possibleResources) if (r != null)];
       }
     }
     return [];
@@ -107,45 +107,55 @@ ChromeProfileMigrator.prototype.getResou
 Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
   get: function Chrome_sourceProfiles() {
     if ("__sourceProfiles" in this)
       return this.__sourceProfiles;
 
     if (!this._chromeUserDataFolder)
       return [];
 
-    let profiles;
+    let profiles = [];
     try {
       // Local State is a JSON file that contains profile info.
       let localState = this._chromeUserDataFolder.clone();
       localState.append("Local State");
       if (!localState.exists())
         throw new Error("Chrome's 'Local State' file does not exist.");
       if (!localState.isReadable())
         throw new Error("Chrome's 'Local State' file could not be read.");
 
       let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
       fstream.init(localState, -1, 0, 0);
       let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
                                                         { charset: "UTF-8" });
       let info_cache = JSON.parse(inputStream).profile.info_cache;
-      if (info_cache)
-        profiles = Object.keys(info_cache);
+      for (let profileFolderName in info_cache) {
+        let profileFolder = this._chromeUserDataFolder.clone();
+        profileFolder.append(profileFolderName);
+        profiles.push({
+          id: profileFolderName,
+          name: info_cache[profileFolderName].name || profileFolderName,
+        });
+      }
     } catch (e) {
       Cu.reportError("Error detecting Chrome profiles: " + e);
       // If we weren't able to detect any profiles above, fallback to the Default profile.
       let defaultProfileFolder = this._chromeUserDataFolder.clone();
       defaultProfileFolder.append("Default");
-      if (defaultProfileFolder.exists())
-        profiles = ["Default"];
+      if (defaultProfileFolder.exists()) {
+        profiles = [{
+          id: "Default",
+          name: "Default",
+        }];
+      }
     }
 
     // Only list profiles from which any data can be imported
-    return this.__sourceProfiles = profiles.filter(function(profileName) {
-      let resources = this.getResources(profileName);
+    return this.__sourceProfiles = profiles.filter(function(profile) {
+      let resources = this.getResources(profile);
       return resources && resources.length > 0;
     }, this);
   }
 });
 
 Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
   get: function Chrome_sourceHomePageURL() {
     let prefsFile = this._chromeUserDataFolder.clone();
--- a/browser/components/migration/src/MigrationUtils.jsm
+++ b/browser/components/migration/src/MigrationUtils.jsm
@@ -87,17 +87,20 @@ function getMigratorKeyForDefaultBrowser
  * 7. For startup-only migrators, override |startupOnlyMigrator|.
  */
 this.MigratorPrototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserProfileMigrator]),
 
   /**
    * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
    *
-   * Returns array of profiles (by names) from which data may be imported.
+   * Returns array of profile objects from which data may be imported. The object
+   * should have the following keys:
+   *   id - a unique string identifier for the profile
+   *   name - a pretty name to display to the user in the UI
    *
    * Only profiles from which data can be imported should be listed.  Otherwise
    * the behavior of the migration wizard isn't well-defined.
    *
    * For a single-profile source (e.g. safari, ie), this returns null,
    * and not an empty array.  That is the default implementation.
    */
   get sourceProfiles() null,
@@ -308,24 +311,25 @@ this.MigratorPrototype = {
     catch(ex) {
       Cu.reportError(ex);
     }
     return exists;
   },
 
   /*** PRIVATE STUFF - DO NOT OVERRIDE ***/
   _getMaybeCachedResources: function PMB__getMaybeCachedResources(aProfile) {
+    let profileKey = aProfile ? aProfile.id : "";
     if (this._resourcesByProfile) {
-      if (aProfile in this._resourcesByProfile)
-        return this._resourcesByProfile[aProfile];
+      if (profileKey in this._resourcesByProfile)
+        return this._resourcesByProfile[profileKey];
     }
     else {
       this._resourcesByProfile = { };
     }
-    return this._resourcesByProfile[aProfile] = this.getResources(aProfile);
+    return this._resourcesByProfile[profileKey] = this.getResources(aProfile);
   }
 };
 
 this.MigrationUtils = Object.freeze({
   resourceTypes: {
     SETTINGS:   Ci.nsIBrowserProfileMigrator.SETTINGS,
     COOKIES:    Ci.nsIBrowserProfileMigrator.COOKIES,
     HISTORY:    Ci.nsIBrowserProfileMigrator.HISTORY,
--- a/browser/components/migration/tests/unit/test_IE_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -6,17 +6,17 @@ function run_test() {
   do_test_pending();
 
   let migrator = MigrationUtils.getMigrator("ie");
 
   // Sanity check for the source.
   do_check_true(migrator.sourceExists);
 
   // Ensure bookmarks migration is available.
-  let availableSources = migrator.getMigrateData("FieldOfFlowers", false);
+  let availableSources = migrator.getMigrateData(null, false);
   do_check_true((availableSources & MigrationUtils.resourceTypes.BOOKMARKS) > 0);
 
   // Wait for the imported bookmarks.  Check that "From Internet Explorer"
   // folders are created in the menu and on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameIE");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
@@ -47,10 +47,10 @@ function run_test() {
 
     // Check the bookmarks have been imported to all the expected parents.
     do_check_eq(expectedParents.length, 0);
 
     do_test_finished();
   }, "Migration:Ended", false);
 
   migrator.migrate(MigrationUtils.resourceTypes.BOOKMARKS, null,
-                   "FieldOfFlowers");
+                   null);
 }
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -1,21 +1,31 @@
 /* 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/. */
+ * 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";
 
-this.EXPORTED_SYMBOLS = ["Translation"];
+this.EXPORTED_SYMBOLS = [
+  "Translation",
+  "TranslationProvider",
+];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+
 
 this.Translation = {
   supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
   supportedTargetLanguages: ["en", "pl", "tr", "vi"],
 
   _defaultTargetLanguage: "",
   get defaultTargetLanguage() {
     if (!this._defaultTargetLanguage) {
@@ -23,26 +33,30 @@ this.Translation = {
                                       .getService(Ci.nsIXULChromeRegistry)
                                       .getSelectedLocale("global")
                                       .split("-")[0];
     }
     return this._defaultTargetLanguage;
   },
 
   languageDetected: function(aBrowser, aDetectedLanguage) {
+    if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) == -1 ||
+        aDetectedLanguage == this.defaultTargetLanguage)
+      return;
+
+    TranslationHealthReport.recordTranslationOpportunity(aDetectedLanguage);
+
     if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI))
       return;
 
-    if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) != -1 &&
-        aDetectedLanguage != this.defaultTargetLanguage) {
-      if (!aBrowser.translationUI)
-        aBrowser.translationUI = new TranslationUI(aBrowser);
+    if (!aBrowser.translationUI)
+      aBrowser.translationUI = new TranslationUI(aBrowser);
 
-      aBrowser.translationUI.showTranslationUI(aDetectedLanguage);
-    }
+
+    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:
  * - detectedLanguage, code of the language detected on the web page.
@@ -182,8 +196,233 @@ TranslationUI.prototype = {
           this.originalShown = false;
         } else {
           this.state = this.STATE_ERROR;
         }
         break;
     }
   }
 };
+
+/**
+ * Helper methods for recording translation data in FHR.
+ */
+let TranslationHealthReport = {
+  /**
+   * Record a translation opportunity in the health report.
+   * @param language
+   *        The language of the page.
+   */
+  recordTranslationOpportunity: function (language) {
+    this._withProvider(provider => provider.recordTranslationOpportunity(language));
+   },
+
+   /**
+   * 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: function (langFrom, langTo, numCharacters) {
+    this._withProvider(provider => provider.recordTranslation(langFrom, langTo, 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.
+   */
+  recordLanguageChange: function (beforeFirstTranslation) {
+    this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation));
+  },
+
+  /**
+   * Retrieve the translation provider and pass it to the given function.
+   *
+   * @param callback
+   *        The function that will be passed the translation provider.
+   */
+  _withProvider: function (callback) {
+    try {
+      let reporter = Cc["@mozilla.org/datareporting/service;1"]
+                        .getService().wrappedJSObject.healthReporter;
+
+      if (reporter) {
+        reporter.onInit().then(function () {
+          callback(reporter.getProvider("org.mozilla.translation"));
+        }, Cu.reportError);
+      } else {
+        callback(null);
+      }
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+  }
+};
+
+/**
+ * Holds usage data about the Translation feature.
+ *
+ * This is a special telemetry measurement that is transmitted in the FHR
+ * payload. Data will only be recorded/transmitted when both telemetry and
+ * FHR are enabled. Additionally, if telemetry was previously enabled but
+ * is currently disabled, old recorded data will not be transmitted.
+ */
+function TranslationMeasurement1() {
+  Metrics.Measurement.call(this);
+
+  this._serializers[this.SERIALIZE_JSON].singular =
+    this._wrapJSONSerializer(this._serializers[this.SERIALIZE_JSON].singular);
+
+  this._serializers[this.SERIALIZE_JSON].daily =
+    this._wrapJSONSerializer(this._serializers[this.SERIALIZE_JSON].daily);
+}
+
+TranslationMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "translation",
+  version: 1,
+
+  fields: {
+    translationOpportunityCount: DAILY_COUNTER_FIELD,
+    pageTranslatedCount: DAILY_COUNTER_FIELD,
+    charactersTranslatedCount: DAILY_COUNTER_FIELD,
+    translationOpportunityCountsByLanguage: DAILY_LAST_TEXT_FIELD,
+    pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD,
+    detectedLanguageChangedBefore: DAILY_COUNTER_FIELD,
+    detectedLanguageChangedAfter: DAILY_COUNTER_FIELD,
+  },
+
+  shouldIncludeField: function (field) {
+    if (!Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
+      // This measurement should only be included when telemetry is
+      // enabled, so we will not include any fields.
+      return false;
+    }
+
+    return field in this._fields;
+  },
+
+  _getDailyLastTextFieldAsJSON: function(name, date) {
+    let id = this.fieldID(name);
+
+    return this.storage.getDailyLastTextFromFieldID(id, date).then((data) => {
+      if (data.hasDay(date)) {
+        data = JSON.parse(data.getDay(date));
+      } else {
+        data = {};
+      }
+
+      return data;
+    });
+},
+
+  _wrapJSONSerializer: function (serializer) {
+    let _parseInPlace = function(o, k) {
+      if (k in o) {
+        o[k] = JSON.parse(o[k]);
+      }
+    };
+
+    return function (data) {
+      let result = serializer(data);
+
+      // Special case the serialization of these fields so that
+      // they are sent as objects, not stringified objects.
+      _parseInPlace(result, "translationOpportunityCountsByLanguage");
+      _parseInPlace(result, "pageTranslatedCountsByLanguage");
+
+      return result;
+    }
+  }
+});
+
+this.TranslationProvider = function () {
+  Metrics.Provider.call(this);
+}
+
+TranslationProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.translation",
+
+  measurementTypes: [
+    TranslationMeasurement1,
+  ],
+
+  recordTranslationOpportunity: function (language, date=new Date()) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      yield m.incrementDailyCounter("translationOpportunityCount", date);
+
+      let langCounts = yield m._getDailyLastTextFieldAsJSON(
+        "translationOpportunityCountsByLanguage", date);
+
+      langCounts[language] = (langCounts[language] || 0) + 1;
+      langCounts = JSON.stringify(langCounts);
+
+      yield m.setDailyLastText("translationOpportunityCountsByLanguage",
+                               langCounts, date);
+
+    }.bind(this));
+  },
+
+  recordTranslation: function (langFrom, langTo, numCharacters, date=new Date()) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      yield m.incrementDailyCounter("pageTranslatedCount", date);
+      yield m.incrementDailyCounter("charactersTranslatedCount", date,
+                                    numCharacters);
+
+      let langCounts = yield m._getDailyLastTextFieldAsJSON(
+        "pageTranslatedCountsByLanguage", date);
+
+      let counts = langCounts[langFrom] || {};
+      counts["total"] = (counts["total"] || 0) + 1;
+      counts[langTo] = (counts[langTo] || 0) + 1;
+      langCounts[langFrom] = counts;
+      langCounts = JSON.stringify(langCounts);
+
+      yield m.setDailyLastText("pageTranslatedCountsByLanguage",
+                               langCounts, date);
+    }.bind(this));
+  },
+
+  recordLanguageChange: function (beforeFirstTranslation) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      if (beforeFirstTranslation) {
+          yield m.incrementDailyCounter("detectedLanguageChangedBefore");
+        } else {
+          yield m.incrementDailyCounter("detectedLanguageChangedAfter");
+        }
+    }.bind(this));
+  },
+
+  _enqueueTelemetryStorageTask: function (task) {
+    if (!Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
+      // This measurement should only be included when telemetry is
+      // enabled, so don't record any data.
+      return Promise.resolve(null);
+    }
+
+    return this.enqueueStorageOperation(() => {
+      return Task.spawn(task);
+    });
+  }
+});
--- a/browser/components/translation/moz.build
+++ b/browser/components/translation/moz.build
@@ -16,10 +16,14 @@ EXTRA_JS_MODULES = [
 
 JAR_MANIFESTS += ['jar.mn']
 
 BROWSER_CHROME_MANIFESTS += [
     'test/browser.ini'
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
-    'test/xpcshell.ini'
+    'test/unit/xpcshell.ini'
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'translation.manifest',
+]
--- a/browser/components/translation/test/browser_translation_exceptions.js
+++ b/browser/components/translation/test/browser_translation_exceptions.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
 // tests the translation infobar, using a fake 'Translation' implementation.
 
-Components.utils.import("resource:///modules/translation/Translation.jsm");
+let tmp = {};
+Cu.import("resource:///modules/translation/Translation.jsm", tmp);
+let {Translation} = tmp;
 
 const kLanguagesPref = "browser.translation.neverForLanguages";
 const kShowUIPref = "browser.translation.ui.show";
 
 function test() {
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref(kShowUIPref, true);
--- a/browser/components/translation/test/browser_translation_infobar.js
+++ b/browser/components/translation/test/browser_translation_infobar.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
 // tests the translation infobar, using a fake 'Translation' implementation.
 
-Components.utils.import("resource:///modules/translation/Translation.jsm");
+let tmp = {};
+Cu.import("resource:///modules/translation/Translation.jsm", tmp);
+let {Translation} = tmp;
 
 const kShowUIPref = "browser.translation.ui.show";
 
 function waitForCondition(condition, nextTest, errorMsg) {
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= 30) {
       ok(false, errorMsg);
rename from browser/components/translation/test/test_cld2.js
rename to browser/components/translation/test/unit/test_cld2.js
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/unit/test_healthreport.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource:///modules/translation/Translation.jsm", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+// At end of test, restore original state.
+const ORIGINAL_TELEMETRY_ENABLED = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function setup() {
+  do_get_profile();
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
+
+  run_next_test();
+});
+
+do_register_cleanup(function() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled",
+                             ORIGINAL_TELEMETRY_ENABLED);
+});
+
+add_task(function test_constructor() {
+  let provider = new TranslationProvider();
+});
+
+// Provider can initialize and de-initialize properly.
+add_task(function* test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording translation opportunities.
+add_task(function* test_translation_opportunity() {
+  let storage = yield Metrics.Storage("opportunity");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+
+  // Initially nothing should be configured.
+  let now = new Date();
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 0);
+  Assert.ok(!values.days.hasDay(now));
+
+  // Record an opportunity.
+  yield provider.recordTranslationOpportunity("fr", now);
+
+  values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("translationOpportunityCount"));
+  Assert.equal(day.get("translationOpportunityCount"), 1);
+
+  Assert.ok(day.has("translationOpportunityCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("translationOpportunityCountsByLanguage"));
+  Assert.equal(countsByLanguage["fr"], 1);
+
+  // Record more opportunities.
+  yield provider.recordTranslationOpportunity("fr", now);
+  yield provider.recordTranslationOpportunity("fr", now);
+  yield provider.recordTranslationOpportunity("es", now);
+
+  values = yield m.getValues();
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("translationOpportunityCount"));
+  Assert.equal(day.get("translationOpportunityCount"), 4);
+
+  Assert.ok(day.has("translationOpportunityCountsByLanguage"));
+  countsByLanguage = JSON.parse(day.get("translationOpportunityCountsByLanguage"));
+  Assert.equal(countsByLanguage["fr"], 3);
+  Assert.equal(countsByLanguage["es"], 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording a translation.
+add_task(function* test_record_translation() {
+  let storage = yield Metrics.Storage("translation");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  let now = new Date();
+
+  // Record a translation.
+  yield provider.recordTranslation("fr", "es", 1000, now);
+
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("pageTranslatedCount"));
+  Assert.equal(day.get("pageTranslatedCount"), 1);
+  Assert.ok(day.has("charactersTranslatedCount"));
+  Assert.equal(day.get("charactersTranslatedCount"), 1000);
+
+  Assert.ok(day.has("pageTranslatedCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("pageTranslatedCountsByLanguage"));
+  Assert.ok("fr" in countsByLanguage);
+  Assert.equal(countsByLanguage["fr"]["total"], 1);
+  Assert.equal(countsByLanguage["fr"]["es"], 1);
+
+  // Record more translations.
+  yield provider.recordTranslation("fr", "es", 1, now);
+  yield provider.recordTranslation("fr", "en", 2, now);
+  yield provider.recordTranslation("es", "en", 4, now);
+
+  values = yield m.getValues();
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("pageTranslatedCount"));
+  Assert.equal(day.get("pageTranslatedCount"), 4);
+  Assert.ok(day.has("charactersTranslatedCount"));
+  Assert.equal(day.get("charactersTranslatedCount"), 1007);
+
+  Assert.ok(day.has("pageTranslatedCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("pageTranslatedCountsByLanguage"));
+  Assert.ok("fr" in countsByLanguage);
+  Assert.equal(countsByLanguage["fr"]["total"], 3);
+  Assert.equal(countsByLanguage["fr"]["es"], 2);
+  Assert.equal(countsByLanguage["fr"]["en"], 1);
+  Assert.ok("es" in countsByLanguage);
+  Assert.equal(countsByLanguage["es"]["total"], 1);
+  Assert.equal(countsByLanguage["es"]["en"], 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording changing languages.
+add_task(function* test_record_translation() {
+  let storage = yield Metrics.Storage("translation");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  let now = new Date();
+
+  // Record a language change before translation.
+  yield provider.recordLanguageChange(true);
+
+  // Record two language changes after translation.
+  yield provider.recordLanguageChange(false);
+  yield provider.recordLanguageChange(false);
+
+
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+
+  Assert.ok(day.has("detectedLanguageChangedBefore"));
+  Assert.equal(day.get("detectedLanguageChangedBefore"), 1);
+  Assert.ok(day.has("detectedLanguageChangedAfter"));
+  Assert.equal(day.get("detectedLanguageChangedAfter"), 2);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test the payload after recording with telemetry enabled.
+add_task(function* test_healthreporter_json() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
+
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    let now = new Date();
+    let provider = new TranslationProvider();
+    yield reporter._providerManager.registerProvider(provider);
+
+    yield provider.recordTranslationOpportunity("fr", now);
+    yield provider.recordLanguageChange(true);
+    yield provider.recordTranslation("fr", "en", 1000, now);
+    yield provider.recordLanguageChange(false);
+
+    yield provider.recordTranslationOpportunity("es", now);
+    yield provider.recordTranslation("es", "en", 1000, now);
+
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.translation.translation" in day);
+
+    let translations = day["org.mozilla.translation.translation"];
+
+    Assert.equal(translations["translationOpportunityCount"], 2);
+    Assert.equal(translations["pageTranslatedCount"], 2);
+    Assert.equal(translations["charactersTranslatedCount"], 2000);
+
+    Assert.ok("translationOpportunityCountsByLanguage" in translations);
+    Assert.equal(translations["translationOpportunityCountsByLanguage"]["fr"], 1);
+    Assert.equal(translations["translationOpportunityCountsByLanguage"]["es"], 1);
+
+    Assert.ok("pageTranslatedCountsByLanguage" in translations);
+    Assert.ok("fr" in translations["pageTranslatedCountsByLanguage"]);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["fr"]["total"], 1);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["fr"]["en"], 1);
+
+    Assert.ok("es" in translations["pageTranslatedCountsByLanguage"]);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["es"]["total"], 1);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["es"]["en"], 1);
+
+    Assert.ok("detectedLanguageChangedBefore" in translations);
+    Assert.equal(translations["detectedLanguageChangedBefore"], 1);
+    Assert.ok("detectedLanguageChangedAfter" in translations);
+    Assert.equal(translations["detectedLanguageChangedAfter"], 1);
+  } finally {
+    reporter._shutdown();
+  }
+});
+
+// Test the payload after recording with telemetry disabled.
+add_task(function* test_healthreporter_json() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", false);
+
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    let now = new Date();
+    let provider = new TranslationProvider();
+    yield reporter._providerManager.registerProvider(provider);
+
+    yield provider.recordTranslationOpportunity("fr", now);
+    yield provider.recordLanguageChange(true);
+    yield provider.recordTranslation("fr", "en", 1000, now);
+    yield provider.recordLanguageChange(false);
+
+    yield provider.recordTranslationOpportunity("es", now);
+    yield provider.recordTranslation("es", "en", 1000, now);
+
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.translation.translation" in day);
+
+    let translations = day["org.mozilla.translation.translation"];
+
+    Assert.ok(!("translationOpportunityCount" in translations));
+    Assert.ok(!("pageTranslatedCount" in translations));
+    Assert.ok(!("charactersTranslatedCount" in translations));
+    Assert.ok(!("translationOpportunityCountsByLanguage" in translations));
+    Assert.ok(!("pageTranslatedCountsByLanguage" in translations));
+    Assert.ok(!("detectedLanguageChangedBefore" in translations));
+    Assert.ok(!("detectedLanguageChangedAfter" in translations));
+  } finally {
+    reporter._shutdown();
+  }
+});
rename from browser/components/translation/test/xpcshell.ini
rename to browser/components/translation/test/unit/xpcshell.ini
--- a/browser/components/translation/test/xpcshell.ini
+++ b/browser/components/translation/test/unit/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head = 
 tail = 
 firefox-appdir = browser
 
 [test_cld2.js]
+[test_healthreport.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/translation.manifest
@@ -0,0 +1,3 @@
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default TranslationProvider resource:///modules/translation/Translation.jsm
+#endif
rename from browser/devtools/projecteditor/lib/plugins/app-manager/lib/app-project-editor.js
rename to browser/devtools/projecteditor/lib/plugins/app-manager/app-project-editor.js
rename from browser/devtools/projecteditor/lib/plugins/app-manager/lib/plugin.js
rename to browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js
rename from browser/devtools/projecteditor/lib/plugins/delete/lib/delete.js
rename to browser/devtools/projecteditor/lib/plugins/delete/delete.js
rename from browser/devtools/projecteditor/lib/plugins/dirty/lib/dirty.js
rename to browser/devtools/projecteditor/lib/plugins/dirty/dirty.js
rename from browser/devtools/projecteditor/lib/plugins/image-view/lib/image-editor.js
rename to browser/devtools/projecteditor/lib/plugins/image-view/image-editor.js
rename from browser/devtools/projecteditor/lib/plugins/image-view/lib/plugin.js
rename to browser/devtools/projecteditor/lib/plugins/image-view/plugin.js
rename from browser/devtools/projecteditor/lib/plugins/logging/lib/logging.js
rename to browser/devtools/projecteditor/lib/plugins/logging/logging.js
rename from browser/devtools/projecteditor/lib/plugins/new/lib/new.js
rename to browser/devtools/projecteditor/lib/plugins/new/new.js
rename from browser/devtools/projecteditor/lib/plugins/save/lib/save.js
rename to browser/devtools/projecteditor/lib/plugins/save/save.js
rename from browser/devtools/projecteditor/lib/plugins/status-bar/lib/plugin.js
rename to browser/devtools/projecteditor/lib/plugins/status-bar/plugin.js
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -17,26 +17,26 @@ const { emit } = require("sdk/event/core
 const { merge } = require("sdk/util/object");
 const promise = require("projecteditor/helpers/promise");
 const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
 const { DOMHelpers } = Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
 
 // Enabled Plugins
-require("projecteditor/plugins/dirty/lib/dirty");
-require("projecteditor/plugins/delete/lib/delete");
-require("projecteditor/plugins/new/lib/new");
-require("projecteditor/plugins/save/lib/save");
-require("projecteditor/plugins/image-view/lib/plugin");
-require("projecteditor/plugins/app-manager/lib/plugin");
-require("projecteditor/plugins/status-bar/lib/plugin");
+require("projecteditor/plugins/dirty/dirty");
+require("projecteditor/plugins/delete/delete");
+require("projecteditor/plugins/new/new");
+require("projecteditor/plugins/save/save");
+require("projecteditor/plugins/image-view/plugin");
+require("projecteditor/plugins/app-manager/plugin");
+require("projecteditor/plugins/status-bar/plugin");
 
 // Uncomment to enable logging.
-// require("projecteditor/plugins/logging/lib/logging");
+// require("projecteditor/plugins/logging/logging");
 
 /**
  * This is the main class tying together an instance of the ProjectEditor.
  * The frontend is contained inside of this.iframe, which loads projecteditor.xul.
  *
  * Usage:
  *   let projecteditor = new ProjectEditor(frame);
  *   projecteditor.loaded.then((projecteditor) => {
--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
@@ -120,17 +120,17 @@
 <!ENTITY offlineAppsList.height          "7em">
 <!ENTITY offlineAppsListRemove.label     "Remove…">
 <!ENTITY offlineAppsListRemove.accesskey "R">
 <!ENTITY offlineAppRemove.confirm        "Remove offline data">
 
 <!ENTITY certificateTab.label            "Certificates">
 <!ENTITY certSelection.description       "When a server requests my personal certificate:">
 <!ENTITY certs.auto                      "Select one automatically">
-<!ENTITY certs.auto.accesskey            "l">
+<!ENTITY certs.auto.accesskey            "S">
 <!ENTITY certs.ask                       "Ask me every time">
-<!ENTITY certs.ask.accesskey             "i">
+<!ENTITY certs.ask.accesskey             "A">
 <!ENTITY viewCerts.label                 "View Certificates">
-<!ENTITY viewCerts.accesskey             "s">
+<!ENTITY viewCerts.accesskey             "C">
 <!ENTITY verify2.label                   "Validation">
 <!ENTITY verify2.accesskey               "V">
 <!ENTITY viewSecurityDevices.label       "Security Devices">
-<!ENTITY viewSecurityDevices.accesskey   "y">
+<!ENTITY viewSecurityDevices.accesskey   "D">
--- a/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
@@ -4,17 +4,17 @@
 
 <!ENTITY tracking.label                 "Tracking">
 
 <!ENTITY dntTrackingNopref.label2       "Do not tell sites anything about my tracking preferences">
 <!ENTITY dntTrackingNopref.accesskey    "o">
 <!ENTITY dntTrackingNotOkay.label2      "Tell sites that I do not want to be tracked">
 <!ENTITY dntTrackingNotOkay.accesskey   "n">
 <!ENTITY dntTrackingOkay.label2         "Tell sites that I want to be tracked">
-<!ENTITY dntTrackingOkay.accesskey      "t">
+<!ENTITY dntTrackingOkay.accesskey      "h">
 <!ENTITY doNotTrackInfo.label           "Learn More">
 
 <!ENTITY  history.label                 "History">
 
 <!ENTITY  locationBar.label             "Location Bar">
 
 <!ENTITY  locbar.pre.label              "When using the location bar, suggest:">
 <!ENTITY  locbar.pre.accessKey          "u">
--- a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
@@ -4,19 +4,19 @@
 
 <!ENTITY newWindowsAsTabs.label       "Open new windows in a new tab instead">
 <!ENTITY newWindowsAsTabs.accesskey   "t">
 
 <!ENTITY warnCloseMultipleTabs.label  "Warn me when closing multiple tabs">
 <!ENTITY warnCloseMultipleTabs.accesskey  "m">
 
 <!ENTITY warnOpenManyTabs.label       "Warn me when opening multiple tabs might slow down &brandShortName;">
-<!ENTITY warnOpenManyTabs.accesskey   "o">
+<!ENTITY warnOpenManyTabs.accesskey   "d">
 
 <!ENTITY restoreTabsOnDemand.label        "Don’t load tabs until selected">
-<!ENTITY restoreTabsOnDemand.accesskey    "l">
+<!ENTITY restoreTabsOnDemand.accesskey    "u">
 
 <!ENTITY switchToNewTabs.label        "When I open a link in a new tab, switch to it immediately">
-<!ENTITY switchToNewTabs.accesskey    "s">
+<!ENTITY switchToNewTabs.accesskey    "h">
 
 <!ENTITY showTabsInTaskbar.label          "Show tab previews in the Windows taskbar">
 <!ENTITY showTabsInTaskbar.accesskey      "k">
 <!ENTITY tabsGroup.label          "Tabs">
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1523,16 +1523,77 @@ Example
       "cpuCount": 8,
       "memoryMB": 8192,
       "architecture": "x86-64",
       "name": "Darwin",
       "version": "12.2.0"
     }
 
 
+org.mozilla.translation.translation
+-----------------------------------
+
+This daily measurement contains information about the usage of the translation
+feature. It is a special telemetry measurement which will only be recorded in
+FHR if telemetry is enabled.
+
+Version 1
+^^^^^^^^^
+
+Daily counts are reported in the following properties:
+
+translationOpportunityCount
+    Integer count of the number of opportunities there were to translate a page.
+pageTranslatedCount
+    Integer count of the number of pages translated.
+charactersTranslatedCount
+    Integer count of the number of characters translated.
+detectedLanguageChangedBefore
+    Integer count of the number of times the user manually adjusted the detected
+    language before translating.
+detectedLanguageChangedAfter
+    Integer count of the number of times the user manually adjusted the detected
+    language after having first translated the page.
+
+Additional daily counts broken down by language are reported in the following
+properties:
+
+translationOpportunityCountsByLanguage
+    A mapping from language to count of opportunities to translate that
+    language.
+pageTranslatedCountsByLanguage
+    A mapping from language to the counts of pages translated from that
+    language. Each language entry will be an object containing a "total" member
+    along with individual counts for each language translated to.
+
+Example
+^^^^^^^
+
+::
+
+    "org.mozilla.translation.translation": {
+      "_v": 1,
+      "translationOpportunityCount": 134,
+      "pageTranslatedCount": 6,
+      "charactersTranslatedCount": "1126",
+      "detectedLanguageChangedBefore": 1,
+      "detectedLanguageChangedAfter": 2,
+      "translationOpportunityCountsByLanguage": {
+        "fr": 100,
+        "es": 34
+      },
+      "pageTranslatedCountsByLanguage": {
+        "fr": {
+          "total": 6,
+          "es": 5,
+          "en": 1
+        }
+      }
+    }
+
 
 org.mozilla.experiments.info
 ----------------------------------
 
 Daily measurement reporting information about the Telemetry Experiments service.
 
 Version 1
 ^^^^^^^^^
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -537,16 +537,19 @@ let WebConsoleUtils = {
    *
    * @type number
    * @private
    */
   _usageCount: 0,
   get usageCount() {
     if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) {
       WebConsoleUtils._usageCount = Services.prefs.getIntPref("devtools.selfxss.count")
+      if (Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+        WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
+      }
     }
     return WebConsoleUtils._usageCount;
   },
   set usageCount(newUC) {
     if (newUC <= CONSOLE_ENTRY_THRESHOLD) {
       WebConsoleUtils._usageCount = newUC;
       Services.prefs.setIntPref("devtools.selfxss.count", newUC);
     }