From 867194fac2b0c1fec3a165de9a89a5d31d67cd58 Mon Sep 17 00:00:00 2001 From: Steven MacLeod Date: Tue, 3 Jun 2014 18:08:35 +0200 Subject: [PATCH] Bug 978158 - Setup FHR provider for Translation project r=felipe,rnewman --HG-- rename : browser/components/translation/test/test_cld2.js => browser/components/translation/test/unit/test_cld2.js rename : browser/components/translation/test/xpcshell.ini => browser/components/translation/test/unit/xpcshell.ini --- .../components/translation/Translation.jsm | 257 ++++++++++++++++- browser/components/translation/moz.build | 6 +- .../test/browser_translation_exceptions.js | 4 +- .../test/browser_translation_infobar.js | 4 +- .../translation/test/{ => unit}/test_cld2.js | 0 .../test/unit/test_healthreport.js | 269 ++++++++++++++++++ .../translation/test/{ => unit}/xpcshell.ini | 1 + .../translation/translation.manifest | 3 + services/healthreport/docs/dataformat.rst | 61 ++++ 9 files changed, 593 insertions(+), 12 deletions(-) rename browser/components/translation/test/{ => unit}/test_cld2.js (100%) create mode 100644 browser/components/translation/test/unit/test_healthreport.js rename browser/components/translation/test/{ => unit}/xpcshell.ini (74%) create mode 100644 browser/components/translation/translation.manifest diff --git a/browser/components/translation/Translation.jsm b/browser/components/translation/Translation.jsm index 13f736a0704..2a26471d1a3 100644 --- a/browser/components/translation/Translation.jsm +++ b/browser/components/translation/Translation.jsm @@ -1,16 +1,26 @@ /* 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"], @@ -28,16 +38,20 @@ this.Translation = { }, 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); } }; @@ -187,3 +201,228 @@ TranslationUI.prototype = { } } }; + +/** + * 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); + }); + } +}); diff --git a/browser/components/translation/moz.build b/browser/components/translation/moz.build index 68b87bb91b9..9e665587931 100644 --- a/browser/components/translation/moz.build +++ b/browser/components/translation/moz.build @@ -21,5 +21,9 @@ BROWSER_CHROME_MANIFESTS += [ ] XPCSHELL_TESTS_MANIFESTS += [ - 'test/xpcshell.ini' + 'test/unit/xpcshell.ini' +] + +EXTRA_PP_COMPONENTS += [ + 'translation.manifest', ] diff --git a/browser/components/translation/test/browser_translation_exceptions.js b/browser/components/translation/test/browser_translation_exceptions.js index aa3b21b06d1..620e8b36a8f 100644 --- a/browser/components/translation/test/browser_translation_exceptions.js +++ b/browser/components/translation/test/browser_translation_exceptions.js @@ -4,7 +4,9 @@ // 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"; diff --git a/browser/components/translation/test/browser_translation_infobar.js b/browser/components/translation/test/browser_translation_infobar.js index 202e6c90871..13d56299997 100644 --- a/browser/components/translation/test/browser_translation_infobar.js +++ b/browser/components/translation/test/browser_translation_infobar.js @@ -4,7 +4,9 @@ // 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"; diff --git a/browser/components/translation/test/test_cld2.js b/browser/components/translation/test/unit/test_cld2.js similarity index 100% rename from browser/components/translation/test/test_cld2.js rename to browser/components/translation/test/unit/test_cld2.js diff --git a/browser/components/translation/test/unit/test_healthreport.js b/browser/components/translation/test/unit/test_healthreport.js new file mode 100644 index 00000000000..6347345eea2 --- /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(); + } +}); diff --git a/browser/components/translation/test/xpcshell.ini b/browser/components/translation/test/unit/xpcshell.ini similarity index 74% rename from browser/components/translation/test/xpcshell.ini rename to browser/components/translation/test/unit/xpcshell.ini index c00af81f6b6..6a97124ef7c 100644 --- a/browser/components/translation/test/xpcshell.ini +++ b/browser/components/translation/test/unit/xpcshell.ini @@ -4,3 +4,4 @@ tail = firefox-appdir = browser [test_cld2.js] +[test_healthreport.js] diff --git a/browser/components/translation/translation.manifest b/browser/components/translation/translation.manifest new file mode 100644 index 00000000000..774fdf40c79 --- /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 diff --git a/services/healthreport/docs/dataformat.rst b/services/healthreport/docs/dataformat.rst index c44a1b0db14..b259541bf39 100644 --- a/services/healthreport/docs/dataformat.rst +++ b/services/healthreport/docs/dataformat.rst @@ -1528,6 +1528,67 @@ Example } +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 ----------------------------------