mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
Issue #832 Part 1: Remove modules, chrome, and preferences.
This commit is contained in:
parent
2bce502df7
commit
7dac76098e
21 changed files with 0 additions and 2998 deletions
|
|
@ -1266,13 +1266,6 @@ pref("media.gmp-widevinecdm.enabled", true);
|
|||
// -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");
|
||||
|
||||
// Telemetry settings.
|
||||
// Determines if Telemetry pings can be archived locally.
|
||||
pref("toolkit.telemetry.archive.enabled", true);
|
||||
|
|
|
|||
|
|
@ -933,11 +933,6 @@ html|*#gcli-output-frame,
|
|||
transition: none;
|
||||
}
|
||||
|
||||
/* Translation */
|
||||
notification[value="translation"] {
|
||||
-moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
|
||||
}
|
||||
|
||||
/** See bug 872317 for why the following rule is necessary. */
|
||||
|
||||
#downloads-button {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ Cu.import("resource://gre/modules/NotificationDB.jsm");
|
|||
["SitePermissions", "resource:///modules/SitePermissions.jsm"],
|
||||
["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"],
|
||||
["Task", "resource://gre/modules/Task.jsm"],
|
||||
["Translation", "resource:///modules/translation/Translation.jsm"],
|
||||
["UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"],
|
||||
["Weave", "resource://services-sync/main.js"],
|
||||
["fxAccounts", "resource://gre/modules/FxAccounts.jsm"],
|
||||
|
|
@ -974,7 +973,6 @@ var gBrowserInit = {
|
|||
// the listener is registered.
|
||||
DOMLinkHandler.init();
|
||||
gPageStyleMenu.init();
|
||||
LanguageDetectionListener.init();
|
||||
BrowserOnClick.init();
|
||||
FeedHandler.init();
|
||||
DevEdition.init();
|
||||
|
|
@ -5688,16 +5686,6 @@ function setStyleDisabled(disabled) {
|
|||
gPageStyleMenu.disableStyle();
|
||||
}
|
||||
|
||||
|
||||
var LanguageDetectionListener = {
|
||||
init: function() {
|
||||
window.messageManager.addMessageListener("Translation:DocumentState", msg => {
|
||||
Translation.documentStateReceived(msg.target, msg.data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var BrowserOffline = {
|
||||
_inited: false,
|
||||
|
||||
|
|
|
|||
|
|
@ -677,10 +677,6 @@
|
|||
tooltiptext="&urlbar.webRTCShareScreenNotificationAnchor.tooltip;"/>
|
||||
<image id="servicesInstall-notification-icon" class="notification-anchor-icon service-icon" role="button"
|
||||
tooltiptext="&urlbar.servicesNotificationAnchor.tooltip;"/>
|
||||
<image id="translate-notification-icon" class="notification-anchor-icon translation-icon" role="button"
|
||||
tooltiptext="&urlbar.translateNotificationAnchor.tooltip;"/>
|
||||
<image id="translated-notification-icon" class="notification-anchor-icon translation-icon in-use" role="button"
|
||||
tooltiptext="&urlbar.translatedNotificationAnchor.tooltip;"/>
|
||||
<image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button"
|
||||
tooltiptext="&urlbar.emeNotificationAnchor.tooltip;"/>
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -558,13 +558,6 @@ var PageStyleHandler = {
|
|||
};
|
||||
PageStyleHandler.init();
|
||||
|
||||
// Keep a reference to the translation content handler to avoid it it being GC'ed.
|
||||
var trHandler = null;
|
||||
if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) {
|
||||
Cu.import("resource:///modules/translation/TranslationContentHandler.jsm");
|
||||
trHandler = new TranslationContentHandler(global, docShell);
|
||||
}
|
||||
|
||||
function gKeywordURIFixup(fixupInfo) {
|
||||
fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
|
||||
if (!fixupInfo.consumer) {
|
||||
|
|
|
|||
|
|
@ -31,18 +31,6 @@ var gContentPane = {
|
|||
menulist.value = FontBuilder.readFontSelection(menulist);
|
||||
}
|
||||
|
||||
// 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.
|
||||
Components.utils.import("resource:///modules/translation/Translation.jsm");
|
||||
if (Translation.translationEngine == "bing") {
|
||||
document.getElementById("bingAttribution").removeAttribute("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (AlertsServiceDND) {
|
||||
let notificationsDoNotDisturbRow =
|
||||
document.getElementById("notificationsDoNotDisturbRow");
|
||||
|
|
@ -66,10 +54,6 @@ var gContentPane = {
|
|||
gContentPane.configureColors);
|
||||
setEventListener("chooseLanguage", "command",
|
||||
gContentPane.showLanguages);
|
||||
setEventListener("translationAttributionImage", "click",
|
||||
gContentPane.openTranslationProviderAttribution);
|
||||
setEventListener("translateButton", "command",
|
||||
gContentPane.showTranslationExceptions);
|
||||
setEventListener("notificationsDoNotDisturb", "command",
|
||||
gContentPane.toggleDoNotDisturbNotifications);
|
||||
|
||||
|
|
@ -274,21 +258,6 @@ var gContentPane = {
|
|||
gSubDialog.open("chrome://browser/content/preferences/languages.xul");
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the translation exceptions dialog where specific site and language
|
||||
* translation preferences can be set.
|
||||
*/
|
||||
showTranslationExceptions: function ()
|
||||
{
|
||||
gSubDialog.open("chrome://browser/content/preferences/translation.xul");
|
||||
},
|
||||
|
||||
openTranslationProviderAttribution: function ()
|
||||
{
|
||||
Components.utils.import("resource:///modules/translation/Translation.jsm");
|
||||
Translation.openProviderAttribution();
|
||||
},
|
||||
|
||||
toggleDoNotDisturbNotifications: function (event)
|
||||
{
|
||||
AlertsServiceDND.manualDoNotDisturb = event.target.checked;
|
||||
|
|
|
|||
|
|
@ -22,11 +22,6 @@
|
|||
<preference id="font.language.group"
|
||||
name="font.language.group"
|
||||
type="wstring"/>
|
||||
|
||||
<!-- Languages -->
|
||||
<preference id="browser.translation.detectLanguage"
|
||||
name="browser.translation.detectLanguage"
|
||||
type="bool"/>
|
||||
</preferences>
|
||||
|
||||
<script type="application/javascript"
|
||||
|
|
@ -191,23 +186,4 @@
|
|||
label="&chooseButton.label;"
|
||||
accesskey="&chooseButton.accesskey;"/>
|
||||
</hbox>
|
||||
|
||||
<hbox id="translationBox" hidden="true">
|
||||
<hbox align="center" flex="1">
|
||||
<checkbox id="translate" preference="browser.translation.detectLanguage"
|
||||
label="&translateWebPages.label;." accesskey="&translateWebPages.accesskey;"
|
||||
onsyncfrompreference="return gContentPane.updateButtons('translateButton',
|
||||
'browser.translation.detectLanguage');"/>
|
||||
<hbox id="bingAttribution" hidden="true">
|
||||
<label>&translation.options.attribution.beforeLogo;</label>
|
||||
<separator orient="vertical" class="thin"/>
|
||||
<image id="translationAttributionImage" aria-label="Microsoft Translator"
|
||||
src="chrome://browser/content/microsoft-translator-attribution.png"/>
|
||||
<separator orient="vertical" class="thin"/>
|
||||
<label>&translation.options.attribution.afterLogo;</label>
|
||||
</hbox>
|
||||
</hbox>
|
||||
<button id="translateButton" label="&translateExceptions.label;"
|
||||
accesskey="&translateExceptions.accesskey;"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
|
|
|||
|
|
@ -31,5 +31,3 @@ browser.jar:
|
|||
content/browser/preferences/sanitize.js
|
||||
content/browser/preferences/selectBookmark.xul
|
||||
content/browser/preferences/selectBookmark.js
|
||||
content/browser/preferences/translation.xul
|
||||
content/browser/preferences/translation.js
|
||||
|
|
|
|||
|
|
@ -1,255 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
|
||||
/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gLangBundle", () =>
|
||||
Services.strings.createBundle("chrome://global/locale/languageNames.properties"));
|
||||
|
||||
const kPermissionType = "translate";
|
||||
const kLanguagesPref = "browser.translation.neverForLanguages";
|
||||
|
||||
function Tree(aId, aData)
|
||||
{
|
||||
this._data = aData;
|
||||
this._tree = document.getElementById(aId);
|
||||
this._tree.view = this;
|
||||
}
|
||||
|
||||
Tree.prototype = {
|
||||
get boxObject() {
|
||||
return this._tree.treeBoxObject;
|
||||
},
|
||||
get isEmpty() {
|
||||
return !this._data.length;
|
||||
},
|
||||
get hasSelection() {
|
||||
return this.selection.count > 0;
|
||||
},
|
||||
getSelectedItems: function() {
|
||||
let result = [];
|
||||
|
||||
let rc = this.selection.getRangeCount();
|
||||
for (let i = 0; i < rc; ++i) {
|
||||
let min = {}, max = {};
|
||||
this.selection.getRangeAt(i, min, max);
|
||||
for (let j = min.value; j <= max.value; ++j)
|
||||
result.push(this._data[j]);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// nsITreeView implementation
|
||||
get rowCount() {
|
||||
return this._data.length;
|
||||
},
|
||||
getCellText: function (aRow, aColumn) {
|
||||
return this._data[aRow];
|
||||
},
|
||||
isSeparator: function(aIndex) {
|
||||
return false;
|
||||
},
|
||||
isSorted: function() {
|
||||
return false;
|
||||
},
|
||||
isContainer: function(aIndex) {
|
||||
return false;
|
||||
},
|
||||
setTree: function(aTree) {},
|
||||
getImageSrc: function(aRow, aColumn) {},
|
||||
getProgressMode: function(aRow, aColumn) {},
|
||||
getCellValue: function(aRow, aColumn) {},
|
||||
cycleHeader: function(column) {},
|
||||
getRowProperties: function(row) {
|
||||
return "";
|
||||
},
|
||||
getColumnProperties: function(column) {
|
||||
return "";
|
||||
},
|
||||
getCellProperties: function(row, column) {
|
||||
return "";
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView])
|
||||
};
|
||||
|
||||
function Lang(aCode)
|
||||
{
|
||||
this.langCode = aCode;
|
||||
this._label = gLangBundle.GetStringFromName(aCode);
|
||||
}
|
||||
|
||||
Lang.prototype = {
|
||||
toString: function() {
|
||||
return this._label;
|
||||
}
|
||||
}
|
||||
|
||||
var gTranslationExceptions = {
|
||||
onLoad: function() {
|
||||
if (this._siteTree) {
|
||||
// Re-using an open dialog, clear the old observers.
|
||||
this.uninit();
|
||||
}
|
||||
|
||||
// Load site permissions into an array.
|
||||
this._sites = [];
|
||||
let enumerator = Services.perms.enumerator;
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let perm = enumerator.getNext().QueryInterface(Ci.nsIPermission);
|
||||
|
||||
if (perm.type == kPermissionType &&
|
||||
perm.capability == Services.perms.DENY_ACTION) {
|
||||
this._sites.push(perm.principal.origin);
|
||||
}
|
||||
}
|
||||
Services.obs.addObserver(this, "perm-changed", false);
|
||||
this._sites.sort();
|
||||
|
||||
this._siteTree = new Tree("sitesTree", this._sites);
|
||||
this.onSiteSelected();
|
||||
|
||||
this._langs = this.getLanguageExceptions();
|
||||
Services.prefs.addObserver(kLanguagesPref, this, false);
|
||||
this._langTree = new Tree("languagesTree", this._langs);
|
||||
this.onLanguageSelected();
|
||||
},
|
||||
|
||||
// Get the list of languages we don't translate as an array.
|
||||
getLanguageExceptions: function() {
|
||||
let langs = Services.prefs.getCharPref(kLanguagesPref);
|
||||
if (!langs)
|
||||
return [];
|
||||
|
||||
let result = langs.split(",").map(code => new Lang(code));
|
||||
result.sort();
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
if (aTopic == "perm-changed") {
|
||||
if (aData == "cleared") {
|
||||
if (!this._sites.length)
|
||||
return;
|
||||
let removed = this._sites.splice(0, this._sites.length);
|
||||
this._siteTree.boxObject.rowCountChanged(0, - removed.length);
|
||||
}
|
||||
else {
|
||||
let perm = aSubject.QueryInterface(Ci.nsIPermission);
|
||||
if (perm.type != kPermissionType)
|
||||
return;
|
||||
|
||||
if (aData == "added") {
|
||||
if (perm.capability != Services.perms.DENY_ACTION)
|
||||
return;
|
||||
this._sites.push(perm.principal.origin);
|
||||
this._sites.sort();
|
||||
let boxObject = this._siteTree.boxObject;
|
||||
boxObject.rowCountChanged(0, 1);
|
||||
boxObject.invalidate();
|
||||
}
|
||||
else if (aData == "deleted") {
|
||||
let index = this._sites.indexOf(perm.principal.origin);
|
||||
if (index == -1)
|
||||
return;
|
||||
this._sites.splice(index, 1);
|
||||
this._siteTree.boxObject.rowCountChanged(index, -1);
|
||||
this.onSiteSelected();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.onSiteSelected();
|
||||
}
|
||||
else if (aTopic == "nsPref:changed") {
|
||||
this._langs = this.getLanguageExceptions();
|
||||
let change = this._langs.length - this._langTree.rowCount;
|
||||
this._langTree._data = this._langs;
|
||||
let boxObject = this._langTree.boxObject;
|
||||
if (change)
|
||||
boxObject.rowCountChanged(0, change);
|
||||
boxObject.invalidate();
|
||||
this.onLanguageSelected();
|
||||
}
|
||||
},
|
||||
|
||||
_handleButtonDisabling: function(aTree, aIdPart) {
|
||||
let empty = aTree.isEmpty;
|
||||
document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
|
||||
document.getElementById("remove" + aIdPart).disabled =
|
||||
empty || !aTree.hasSelection;
|
||||
},
|
||||
|
||||
onLanguageSelected: function() {
|
||||
this._handleButtonDisabling(this._langTree, "Language");
|
||||
},
|
||||
|
||||
onSiteSelected: function() {
|
||||
this._handleButtonDisabling(this._siteTree, "Site");
|
||||
},
|
||||
|
||||
onLanguageDeleted: function() {
|
||||
let langs = Services.prefs.getCharPref(kLanguagesPref);
|
||||
if (!langs)
|
||||
return;
|
||||
|
||||
let removed = this._langTree.getSelectedItems().map(l => l.langCode);
|
||||
|
||||
langs = langs.split(",").filter(l => removed.indexOf(l) == -1);
|
||||
Services.prefs.setCharPref(kLanguagesPref, langs.join(","));
|
||||
},
|
||||
|
||||
onAllLanguagesDeleted: function() {
|
||||
Services.prefs.setCharPref(kLanguagesPref, "");
|
||||
},
|
||||
|
||||
onSiteDeleted: function() {
|
||||
let removedSites = this._siteTree.getSelectedItems();
|
||||
for (let origin of removedSites) {
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
|
||||
Services.perms.removeFromPrincipal(principal, kPermissionType);
|
||||
}
|
||||
},
|
||||
|
||||
onAllSitesDeleted: function() {
|
||||
if (this._siteTree.isEmpty)
|
||||
return;
|
||||
|
||||
let removedSites = this._sites.splice(0, this._sites.length);
|
||||
this._siteTree.boxObject.rowCountChanged(0, -removedSites.length);
|
||||
|
||||
for (let origin of removedSites) {
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
|
||||
Services.perms.removeFromPrincipal(principal, kPermissionType);
|
||||
}
|
||||
|
||||
this.onSiteSelected();
|
||||
},
|
||||
|
||||
onSiteKeyPress: function(aEvent) {
|
||||
if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE)
|
||||
this.onSiteDeleted();
|
||||
},
|
||||
|
||||
onLanguageKeyPress: function(aEvent) {
|
||||
if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE)
|
||||
this.onLanguageDeleted();
|
||||
},
|
||||
|
||||
onWindowKeyPress: function(aEvent) {
|
||||
if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE)
|
||||
window.close();
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
Services.obs.removeObserver(this, "perm-changed");
|
||||
Services.prefs.removeObserver(kLanguagesPref, this);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
|
||||
|
||||
<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/translation.dtd">
|
||||
|
||||
<window id="TranslationDialog" class="windowDialog"
|
||||
windowtype="Browser:TranslationExceptions"
|
||||
title="&window.title;"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
style="width: &window.width;;"
|
||||
onload="gTranslationExceptions.onLoad();"
|
||||
onunload="gTranslationExceptions.uninit();"
|
||||
persist="screenX screenY width height"
|
||||
onkeypress="gTranslationExceptions.onWindowKeyPress(event);">
|
||||
|
||||
<script src="chrome://browser/content/preferences/translation.js"/>
|
||||
|
||||
<stringbundle id="bundlePreferences"
|
||||
src="chrome://browser/locale/preferences/preferences.properties"/>
|
||||
|
||||
<keyset>
|
||||
<key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
|
||||
</keyset>
|
||||
|
||||
<vbox class="largeDialogContainer">
|
||||
<vbox class="contentPane" flex="1">
|
||||
<label id="languagesLabel" control="permissionsTree">&noTranslationForLanguages.label;</label>
|
||||
<separator class="thin"/>
|
||||
<tree id="languagesTree" flex="1" style="height: 12em;"
|
||||
hidecolumnpicker="true"
|
||||
onkeypress="gTranslationExceptions.onLanguageKeyPress(event)"
|
||||
onselect="gTranslationExceptions.onLanguageSelected();">
|
||||
<treecols>
|
||||
<treecol id="languageCol" label="&treehead.languageName.label;" flex="1"/>
|
||||
</treecols>
|
||||
<treechildren/>
|
||||
</tree>
|
||||
</vbox>
|
||||
<hbox align="end">
|
||||
<hbox class="actionButtons" flex="1">
|
||||
<button id="removeLanguage" disabled="true"
|
||||
accesskey="&removeLanguage.accesskey;"
|
||||
icon="remove" label="&removeLanguage.label;"
|
||||
oncommand="gTranslationExceptions.onLanguageDeleted();"/>
|
||||
<button id="removeAllLanguages"
|
||||
icon="clear" label="&removeAllLanguages.label;"
|
||||
accesskey="&removeAllLanguages.accesskey;"
|
||||
oncommand="gTranslationExceptions.onAllLanguagesDeleted();"/>
|
||||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
<separator/>
|
||||
<vbox class="contentPane" flex="1">
|
||||
<label id="languagesLabel" control="permissionsTree">&noTranslationForSites.label;</label>
|
||||
<separator class="thin"/>
|
||||
<tree id="sitesTree" flex="1" style="height: 12em;"
|
||||
hidecolumnpicker="true"
|
||||
onkeypress="gTranslationExceptions.onSiteKeyPress(event)"
|
||||
onselect="gTranslationExceptions.onSiteSelected();">
|
||||
<treecols>
|
||||
<treecol id="siteCol" label="&treehead.siteName.label;" flex="1"/>
|
||||
</treecols>
|
||||
<treechildren/>
|
||||
</tree>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<hbox align="end">
|
||||
<hbox class="actionButtons" flex="1">
|
||||
<button id="removeSite" disabled="true"
|
||||
accesskey="&removeSite.accesskey;"
|
||||
icon="remove" label="&removeSite.label;"
|
||||
oncommand="gTranslationExceptions.onSiteDeleted();"/>
|
||||
<button id="removeAllSites"
|
||||
icon="clear" label="&removeAllSites.label;"
|
||||
accesskey="&removeAllSites.accesskey;"
|
||||
oncommand="gTranslationExceptions.onAllSitesDeleted();"/>
|
||||
<spacer flex="1"/>
|
||||
<button oncommand="close();" icon="close"
|
||||
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
</window>
|
||||
|
|
@ -1,449 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "BingTranslator" ];
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/Http.jsm");
|
||||
|
||||
// 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 number of chunks allowed to be translated in a single
|
||||
// request.
|
||||
const MAX_REQUEST_CHUNKS = 1000; // Documentation says 2000.
|
||||
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Translates a webpage using Bing'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.
|
||||
*/
|
||||
this.BingTranslator = 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 = {
|
||||
/**
|
||||
* 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: function() {
|
||||
return Task.spawn(function *() {
|
||||
let currentIndex = 0;
|
||||
this._onFinishedDeferred = Promise.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.
|
||||
yield CommonUtils.laterTickResolvingPromise();
|
||||
|
||||
// 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),
|
||||
this._chunkFailed.bind(this));
|
||||
|
||||
currentIndex = request.lastIndex;
|
||||
if (request.finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this._onFinishedDeferred.promise;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 : function() {
|
||||
// Force the token manager to get update token
|
||||
BingTokenManager._currentExpiryTime = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
_chunkCompleted: function(bingRequest) {
|
||||
if (this._parseChunkResult(bingRequest)) {
|
||||
this._partialSuccess = true;
|
||||
// Count the number of characters successfully translated.
|
||||
this._translatedCharacterCount += bingRequest.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: function(aError) {
|
||||
if (aError instanceof Ci.nsIXMLHttpRequest &&
|
||||
[400, 401].indexOf(aError.status) != -1) {
|
||||
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.
|
||||
*/
|
||||
_checkIfFinished: function() {
|
||||
// Check if all pending requests have been
|
||||
// completed and then resolves the promise.
|
||||
// If at least one chunk was successful, the
|
||||
// promise will be resolved positively which will
|
||||
// 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 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: function(bingRequest) {
|
||||
let results;
|
||||
try {
|
||||
let doc = bingRequest.networkRequest.responseXML;
|
||||
results = doc.querySelectorAll("TranslatedText");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let len = results.length;
|
||||
if (len != bingRequest.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(/&/g, "&");
|
||||
}
|
||||
|
||||
root.parseResult(result);
|
||||
} catch (e) { error = true; }
|
||||
}
|
||||
|
||||
return !error;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function will determine what is the data to be used for
|
||||
* the Nth request we are generating, based on the input params.
|
||||
*
|
||||
* @param startIndex What is the index, in the roots list, that the
|
||||
* chunk should start.
|
||||
*/
|
||||
_generateNextTranslationRequest: function(startIndex) {
|
||||
let currentDataSize = 0;
|
||||
let currentChunks = 0;
|
||||
let output = [];
|
||||
let rootsList = this.translationDocument.roots;
|
||||
|
||||
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
|
||||
// the caller to pass back on the next call, so that the function
|
||||
// can keep working from where it stopped.
|
||||
return {
|
||||
data: output,
|
||||
finished: false,
|
||||
lastIndex: i
|
||||
};
|
||||
}
|
||||
|
||||
currentDataSize = newCurSize;
|
||||
currentChunks = newChunks;
|
||||
output.push([root, text]);
|
||||
}
|
||||
|
||||
return {
|
||||
data: output,
|
||||
finished: true,
|
||||
lastIndex: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a request (for 1 chunk) sent off to Bing'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) {
|
||||
this.translationData = translationData;
|
||||
this.sourceLanguage = sourceLanguage;
|
||||
this.targetLanguage = targetLanguage;
|
||||
this.characterCount = 0;
|
||||
}
|
||||
|
||||
BingRequest.prototype = {
|
||||
/**
|
||||
* Initiates the request
|
||||
*/
|
||||
fireRequest: function() {
|
||||
return Task.spawn(function *() {
|
||||
// Prepare authentication.
|
||||
let token = yield 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>';
|
||||
|
||||
// Set up request options.
|
||||
let deferred = Promise.defer();
|
||||
let options = {
|
||||
onLoad: (function(responseText, xhr) {
|
||||
deferred.resolve(this);
|
||||
}).bind(this),
|
||||
onError: function(e, responseText, xhr) {
|
||||
deferred.reject(xhr);
|
||||
},
|
||||
postData: requestString,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
// Fire the request.
|
||||
let request = httpRequest(url, options);
|
||||
|
||||
// Override the response MIME type.
|
||||
request.overrideMimeType("text/xml");
|
||||
this.networkRequest = request;
|
||||
return deferred.promise;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: function() {
|
||||
if (this._pendingRequest) {
|
||||
return this._pendingRequest;
|
||||
}
|
||||
|
||||
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: function() {
|
||||
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")]
|
||||
];
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let options = {
|
||||
onLoad: function(responseText, xhr) {
|
||||
BingTokenManager._pendingRequest = null;
|
||||
try {
|
||||
let json = JSON.parse(responseText);
|
||||
|
||||
if (json.error) {
|
||||
deferred.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);
|
||||
deferred.resolve(token);
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
}
|
||||
},
|
||||
onError: function(e, responseText, xhr) {
|
||||
BingTokenManager._pendingRequest = null;
|
||||
deferred.reject(e);
|
||||
},
|
||||
postData: params
|
||||
};
|
||||
|
||||
this._pendingRequest = deferred.promise;
|
||||
httpRequest(url, options);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape a string to be valid XML content.
|
||||
*/
|
||||
function escapeXML(aStr) {
|
||||
return aStr.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/\"/g, """)
|
||||
.replace(/\'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -1,446 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"Translation",
|
||||
"TranslationTelemetry",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
|
||||
const TRANSLATION_PREF_DETECT_LANG = "browser.translation.detectLanguage";
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
|
||||
this.Translation = {
|
||||
STATE_OFFER: 0,
|
||||
STATE_TRANSLATING: 1,
|
||||
STATE_TRANSLATED: 2,
|
||||
STATE_ERROR: 3,
|
||||
STATE_UNAVAILABLE: 4,
|
||||
|
||||
serviceUnavailable: false,
|
||||
|
||||
supportedSourceLanguages: ["bg", "cs", "de", "en", "es", "fr", "ja", "ko", "nl", "no", "pl", "pt", "ru", "tr", "vi", "zh"],
|
||||
supportedTargetLanguages: ["bg", "cs", "de", "en", "es", "fr", "ja", "ko", "nl", "no", "pl", "pt", "ru", "tr", "vi", "zh"],
|
||||
|
||||
_defaultTargetLanguage: "",
|
||||
get defaultTargetLanguage() {
|
||||
if (!this._defaultTargetLanguage) {
|
||||
this._defaultTargetLanguage = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIXULChromeRegistry)
|
||||
.getSelectedLocale("global")
|
||||
.split("-")[0];
|
||||
}
|
||||
return this._defaultTargetLanguage;
|
||||
},
|
||||
|
||||
documentStateReceived: function(aBrowser, aData) {
|
||||
if (aData.state == this.STATE_OFFER) {
|
||||
if (aData.detectedLanguage == this.defaultTargetLanguage) {
|
||||
// Detected language is the same as the user's locale.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.supportedSourceLanguages.indexOf(aData.detectedLanguage) == -1) {
|
||||
// Detected language is not part of the supported languages.
|
||||
TranslationTelemetry.recordMissedTranslationOpportunity(aData.detectedLanguage);
|
||||
return;
|
||||
}
|
||||
|
||||
TranslationTelemetry.recordTranslationOpportunity(aData.detectedLanguage);
|
||||
}
|
||||
|
||||
if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI))
|
||||
return;
|
||||
|
||||
if (!aBrowser.translationUI)
|
||||
aBrowser.translationUI = new TranslationUI(aBrowser);
|
||||
let trUI = aBrowser.translationUI;
|
||||
|
||||
// Set all values before showing a new translation infobar.
|
||||
trUI._state = Translation.serviceUnavailable ? Translation.STATE_UNAVAILABLE
|
||||
: aData.state;
|
||||
trUI.detectedLanguage = aData.detectedLanguage;
|
||||
trUI.translatedFrom = aData.translatedFrom;
|
||||
trUI.translatedTo = aData.translatedTo;
|
||||
trUI.originalShown = aData.originalShown;
|
||||
|
||||
trUI.showURLBarIcon();
|
||||
|
||||
if (trUI.shouldShowInfoBar(aBrowser.currentURI))
|
||||
trUI.showTranslationInfoBar();
|
||||
},
|
||||
|
||||
openProviderAttribution: function() {
|
||||
let attribution = this.supportedEngines[this.translationEngine];
|
||||
Cu.import("resource:///modules/RecentWindow.jsm");
|
||||
RecentWindow.getMostRecentBrowserWindow().openUILinkIn(attribution, "tab");
|
||||
},
|
||||
|
||||
/**
|
||||
* The list of translation engines and their attributions.
|
||||
*/
|
||||
supportedEngines: {
|
||||
"bing" : "http://aka.ms/MicrosoftTranslatorAttribution",
|
||||
"yandex" : "http://translate.yandex.com/"
|
||||
},
|
||||
|
||||
/**
|
||||
* Fallback engine (currently Bing Translator) if the preferences seem
|
||||
* confusing.
|
||||
*/
|
||||
get defaultEngine() {
|
||||
return this.supportedEngines.keys[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the name of the preferred translation engine.
|
||||
*/
|
||||
get translationEngine() {
|
||||
let engine = Services.prefs.getCharPref("browser.translation.engine");
|
||||
return Object.keys(this.supportedEngines).indexOf(engine) == -1 ? this.defaultEngine : engine;
|
||||
},
|
||||
};
|
||||
|
||||
/* 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.
|
||||
* - state, the state in which the infobar should be displayed
|
||||
* - translatedFrom, if already translated, source language code.
|
||||
* - translatedTo, if already translated, target language code.
|
||||
* - translate, method starting the translation of the current page.
|
||||
* - showOriginalContent, method showing the original page content.
|
||||
* - showTranslatedContent, method showing the translation for an
|
||||
* already translated page whose original content is shown.
|
||||
* - originalShown, boolean indicating if the original or translated
|
||||
* version of the page is shown.
|
||||
*/
|
||||
function TranslationUI(aBrowser) {
|
||||
this.browser = aBrowser;
|
||||
}
|
||||
|
||||
TranslationUI.prototype = {
|
||||
get browser() {
|
||||
return this._browser;
|
||||
},
|
||||
set browser(aBrowser) {
|
||||
if (this._browser)
|
||||
this._browser.messageManager.removeMessageListener("Translation:Finished", this);
|
||||
aBrowser.messageManager.addMessageListener("Translation:Finished", this);
|
||||
this._browser = aBrowser;
|
||||
},
|
||||
translate: function(aFrom, aTo) {
|
||||
if (aFrom == aTo ||
|
||||
(this.state == Translation.STATE_TRANSLATED &&
|
||||
this.translatedFrom == aFrom && this.translatedTo == aTo)) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state == Translation.STATE_OFFER) {
|
||||
if (this.detectedLanguage != aFrom)
|
||||
TranslationTelemetry.recordDetectedLanguageChange(true);
|
||||
} else {
|
||||
if (this.translatedFrom != aFrom)
|
||||
TranslationTelemetry.recordDetectedLanguageChange(false);
|
||||
if (this.translatedTo != aTo)
|
||||
TranslationTelemetry.recordTargetLanguageChange();
|
||||
}
|
||||
|
||||
this.state = Translation.STATE_TRANSLATING;
|
||||
this.translatedFrom = aFrom;
|
||||
this.translatedTo = aTo;
|
||||
|
||||
this.browser.messageManager.sendAsyncMessage(
|
||||
"Translation:TranslateDocument",
|
||||
{ from: aFrom, to: aTo }
|
||||
);
|
||||
},
|
||||
|
||||
showURLBarIcon: function() {
|
||||
let chromeWin = this.browser.ownerGlobal;
|
||||
let PopupNotifications = chromeWin.PopupNotifications;
|
||||
let removeId = this.originalShown ? "translated" : "translate";
|
||||
let notification =
|
||||
PopupNotifications.getNotification(removeId, this.browser);
|
||||
if (notification)
|
||||
PopupNotifications.remove(notification);
|
||||
|
||||
let callback = (aTopic, aNewBrowser) => {
|
||||
if (aTopic == "swapping") {
|
||||
let infoBarVisible =
|
||||
this.notificationBox.getNotificationWithValue("translation");
|
||||
aNewBrowser.translationUI = this;
|
||||
this.browser = aNewBrowser;
|
||||
if (infoBarVisible)
|
||||
this.showTranslationInfoBar();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aTopic != "showing")
|
||||
return false;
|
||||
let notification = this.notificationBox.getNotificationWithValue("translation");
|
||||
if (notification)
|
||||
notification.close();
|
||||
else
|
||||
this.showTranslationInfoBar();
|
||||
return true;
|
||||
};
|
||||
|
||||
let addId = this.originalShown ? "translate" : "translated";
|
||||
PopupNotifications.show(this.browser, addId, null,
|
||||
addId + "-notification-icon", null, null,
|
||||
{dismissed: true, eventCallback: callback});
|
||||
},
|
||||
|
||||
_state: 0,
|
||||
get state() {
|
||||
return this._state;
|
||||
},
|
||||
set state(val) {
|
||||
let notif = this.notificationBox.getNotificationWithValue("translation");
|
||||
if (notif)
|
||||
notif.state = val;
|
||||
this._state = val;
|
||||
},
|
||||
|
||||
originalShown: true,
|
||||
showOriginalContent: function() {
|
||||
this.originalShown = true;
|
||||
this.showURLBarIcon();
|
||||
this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal");
|
||||
TranslationTelemetry.recordShowOriginalContent();
|
||||
},
|
||||
|
||||
showTranslatedContent: function() {
|
||||
this.originalShown = false;
|
||||
this.showURLBarIcon();
|
||||
this.browser.messageManager.sendAsyncMessage("Translation:ShowTranslation");
|
||||
},
|
||||
|
||||
get notificationBox() {
|
||||
return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
|
||||
},
|
||||
|
||||
showTranslationInfoBar: function() {
|
||||
let notificationBox = this.notificationBox;
|
||||
let notif = notificationBox.appendNotification("", "translation", null,
|
||||
notificationBox.PRIORITY_INFO_HIGH);
|
||||
notif.init(this);
|
||||
return notif;
|
||||
},
|
||||
|
||||
shouldShowInfoBar: function(aURI) {
|
||||
// Never show the infobar automatically while the translation
|
||||
// service is temporarily unavailable.
|
||||
if (Translation.serviceUnavailable)
|
||||
return false;
|
||||
|
||||
// Check if we should never show the infobar for this language.
|
||||
let neverForLangs =
|
||||
Services.prefs.getCharPref("browser.translation.neverForLanguages");
|
||||
if (neverForLangs.split(",").indexOf(this.detectedLanguage) != -1) {
|
||||
TranslationTelemetry.recordAutoRejectedTranslationOffer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// or if we should never show the infobar for this domain.
|
||||
let perms = Services.perms;
|
||||
if (perms.testExactPermission(aURI, "translate") == perms.DENY_ACTION) {
|
||||
TranslationTelemetry.recordAutoRejectedTranslationOffer();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
receiveMessage: function(msg) {
|
||||
switch (msg.name) {
|
||||
case "Translation:Finished":
|
||||
if (msg.data.success) {
|
||||
this.originalShown = false;
|
||||
this.state = Translation.STATE_TRANSLATED;
|
||||
this.showURLBarIcon();
|
||||
|
||||
// Record the number of characters translated.
|
||||
TranslationTelemetry.recordTranslation(msg.data.from, msg.data.to,
|
||||
msg.data.characterCount);
|
||||
} else if (msg.data.unavailable) {
|
||||
Translation.serviceUnavailable = true;
|
||||
this.state = Translation.STATE_UNAVAILABLE;
|
||||
} else {
|
||||
this.state = Translation.STATE_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
infobarClosed: function() {
|
||||
if (this.state == Translation.STATE_OFFER)
|
||||
TranslationTelemetry.recordDeniedTranslationOffer();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses telemetry histograms for collecting statistics on the usage of the
|
||||
* translation component.
|
||||
*
|
||||
* NOTE: Metrics are only recorded if the user enabled the telemetry option.
|
||||
*/
|
||||
this.TranslationTelemetry = {
|
||||
|
||||
init: function () {
|
||||
// Constructing histograms.
|
||||
const plain = (id) => Services.telemetry.getHistogramById(id);
|
||||
const keyed = (id) => Services.telemetry.getKeyedHistogramById(id);
|
||||
this.HISTOGRAMS = {
|
||||
OPPORTUNITIES : () => plain("TRANSLATION_OPPORTUNITIES"),
|
||||
OPPORTUNITIES_BY_LANG : () => keyed("TRANSLATION_OPPORTUNITIES_BY_LANGUAGE"),
|
||||
PAGES : () => plain("TRANSLATED_PAGES"),
|
||||
PAGES_BY_LANG : () => keyed("TRANSLATED_PAGES_BY_LANGUAGE"),
|
||||
CHARACTERS : () => plain("TRANSLATED_CHARACTERS"),
|
||||
DENIED : () => plain("DENIED_TRANSLATION_OFFERS"),
|
||||
AUTO_REJECTED : () => plain("AUTO_REJECTED_TRANSLATION_OFFERS"),
|
||||
SHOW_ORIGINAL : () => plain("REQUESTS_OF_ORIGINAL_CONTENT"),
|
||||
TARGET_CHANGES : () => plain("CHANGES_OF_TARGET_LANGUAGE"),
|
||||
DETECTION_CHANGES : () => plain("CHANGES_OF_DETECTED_LANGUAGE"),
|
||||
SHOW_UI : () => plain("SHOULD_TRANSLATION_UI_APPEAR"),
|
||||
DETECT_LANG : () => plain("SHOULD_AUTO_DETECT_LANGUAGE"),
|
||||
};
|
||||
|
||||
// Capturing the values of flags at the startup.
|
||||
this.recordPreferences();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a translation opportunity in the health report.
|
||||
* @param language
|
||||
* The language of the page.
|
||||
*/
|
||||
recordTranslationOpportunity: function (language) {
|
||||
return this._recordOpportunity(language, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a missed translation opportunity in the health report.
|
||||
* A missed opportunity is when the language detected is not part
|
||||
* of the supported languages.
|
||||
* @param language
|
||||
* The language of the page.
|
||||
*/
|
||||
recordMissedTranslationOpportunity: function (language) {
|
||||
return this._recordOpportunity(language, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record an automatically rejected translation offer in the health
|
||||
* report. A translation offer is automatically rejected when a user
|
||||
* has previously clicked "Never translate this language" or "Never
|
||||
* translate this site", which results in the infobar not being shown for
|
||||
* the translation opportunity.
|
||||
*
|
||||
* These translation opportunities should still be recorded in addition to
|
||||
* recording the automatic rejection of the offer.
|
||||
*/
|
||||
recordAutoRejectedTranslationOffer: function () {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.AUTO_REJECTED().add();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.PAGES().add();
|
||||
this.HISTOGRAMS.PAGES_BY_LANG().add(langFrom + " -> " + langTo);
|
||||
this.HISTOGRAMS.CHARACTERS().add(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.
|
||||
*/
|
||||
recordDetectedLanguageChange: function (beforeFirstTranslation) {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.DETECTION_CHANGES().add(beforeFirstTranslation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a change of the target 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.
|
||||
*/
|
||||
recordTargetLanguageChange: function () {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.TARGET_CHANGES().add();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a denied translation offer.
|
||||
*/
|
||||
recordDeniedTranslationOffer: function () {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.DENIED().add();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a "Show Original" command use.
|
||||
*/
|
||||
recordShowOriginalContent: function () {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.SHOW_ORIGINAL().add();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record the state of translation preferences.
|
||||
*/
|
||||
recordPreferences: function () {
|
||||
if (!this._canRecord) return;
|
||||
if (Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI)) {
|
||||
this.HISTOGRAMS.SHOW_UI().add(1);
|
||||
}
|
||||
if (Services.prefs.getBoolPref(TRANSLATION_PREF_DETECT_LANG)) {
|
||||
this.HISTOGRAMS.DETECT_LANG().add(1);
|
||||
}
|
||||
},
|
||||
|
||||
_recordOpportunity: function(language, success) {
|
||||
if (!this._canRecord) return;
|
||||
this.HISTOGRAMS.OPPORTUNITIES().add(success);
|
||||
this.HISTOGRAMS.OPPORTUNITIES_BY_LANG().add(language, success);
|
||||
},
|
||||
|
||||
/**
|
||||
* A shortcut for reading the telemetry preference.
|
||||
*
|
||||
*/
|
||||
_canRecord: function () {
|
||||
return Services.prefs.getBoolPref("toolkit.telemetry.enabled");
|
||||
}
|
||||
};
|
||||
|
||||
this.TranslationTelemetry.init();
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "TranslationContentHandler" ];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
|
||||
"resource:///modules/translation/LanguageDetector.jsm");
|
||||
|
||||
const STATE_OFFER = 0;
|
||||
const STATE_TRANSLATED = 2;
|
||||
const STATE_ERROR = 3;
|
||||
|
||||
this.TranslationContentHandler = function(global, docShell) {
|
||||
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebProgress);
|
||||
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
|
||||
|
||||
global.addEventListener("pageshow", this);
|
||||
|
||||
global.addMessageListener("Translation:TranslateDocument", this);
|
||||
global.addMessageListener("Translation:ShowTranslation", this);
|
||||
global.addMessageListener("Translation:ShowOriginal", this);
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
TranslationContentHandler.prototype = {
|
||||
handleEvent: function(aEvent) {
|
||||
// We are only listening to pageshow events.
|
||||
let target = aEvent.target;
|
||||
|
||||
// Only handle top-level frames.
|
||||
let win = target.defaultView;
|
||||
if (win.parent !== win)
|
||||
return;
|
||||
|
||||
let content = this.global.content;
|
||||
if (!content.detectedLanguage)
|
||||
return;
|
||||
|
||||
let data = {};
|
||||
let trDoc = content.translationDocument;
|
||||
if (trDoc) {
|
||||
data.state = trDoc.translationError ? STATE_ERROR : STATE_TRANSLATED;
|
||||
data.translatedFrom = trDoc.translatedFrom;
|
||||
data.translatedTo = trDoc.translatedTo;
|
||||
data.originalShown = trDoc.originalShown;
|
||||
} else {
|
||||
data.state = STATE_OFFER;
|
||||
data.originalShown = true;
|
||||
}
|
||||
data.detectedLanguage = content.detectedLanguage;
|
||||
|
||||
this.global.sendAsyncMessage("Translation:DocumentState", data);
|
||||
},
|
||||
|
||||
/* nsIWebProgressListener implementation */
|
||||
onStateChange: function(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://"))
|
||||
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);
|
||||
encoder.init(content.document, "text/plain", encoder.SkipInvisibleContent);
|
||||
let string = encoder.encodeToStringWithMaxLength(60 * 1024);
|
||||
|
||||
// Language detection isn't reliable on very short strings.
|
||||
if (string.length < 100)
|
||||
return;
|
||||
|
||||
LanguageDetector.detectLanguage(string).then(result => {
|
||||
// Bail if we're not confident.
|
||||
if (!result.confident) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The window might be gone by now.
|
||||
if (Cu.isDeadWrapper(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
content.detectedLanguage = result.language;
|
||||
|
||||
let data = {
|
||||
state: STATE_OFFER,
|
||||
originalShown: true,
|
||||
detectedLanguage: result.language
|
||||
};
|
||||
this.global.sendAsyncMessage("Translation:DocumentState", data);
|
||||
});
|
||||
},
|
||||
|
||||
// Unused methods.
|
||||
onProgressChange: function() {},
|
||||
onLocationChange: function() {},
|
||||
onStatusChange: function() {},
|
||||
onSecurityChange: function() {},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
|
||||
receiveMessage: function(msg) {
|
||||
switch (msg.name) {
|
||||
case "Translation:TranslateDocument":
|
||||
{
|
||||
Cu.import("resource:///modules/translation/TranslationDocument.jsm");
|
||||
|
||||
// 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") {
|
||||
Cu.import("resource:///modules/translation/YandexTranslator.jsm");
|
||||
translator = new YandexTranslator(translationDocument,
|
||||
msg.data.from,
|
||||
msg.data.to);
|
||||
} else {
|
||||
Cu.import("resource:///modules/translation/BingTranslator.jsm");
|
||||
translator = new BingTranslator(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 => {
|
||||
this.global.sendAsyncMessage("Translation:Finished", {
|
||||
characterCount: result.characterCount,
|
||||
from: msg.data.from,
|
||||
to: msg.data.to,
|
||||
success: true
|
||||
});
|
||||
translationDocument.showTranslation();
|
||||
},
|
||||
error => {
|
||||
translationDocument.translationError = true;
|
||||
let data = {success: false};
|
||||
if (error == "unavailable")
|
||||
data.unavailable = true;
|
||||
this.global.sendAsyncMessage("Translation:Finished", data);
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Translation:ShowOriginal":
|
||||
this.global.content.translationDocument.showOriginal();
|
||||
break;
|
||||
|
||||
case "Translation:ShowTranslation":
|
||||
this.global.content.translationDocument.showTranslation();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,683 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "TranslationDocument" ];
|
||||
|
||||
const SHOW_ELEMENT = Ci.nsIDOMNodeFilter.SHOW_ELEMENT;
|
||||
const SHOW_TEXT = Ci.nsIDOMNodeFilter.SHOW_TEXT;
|
||||
const TEXT_NODE = Ci.nsIDOMNode.TEXT_NODE;
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
/**
|
||||
* This class represents a document that is being translated,
|
||||
* and it is responsible for parsing the document,
|
||||
* generating the data structures translation (the list of
|
||||
* translation items and roots), and managing the original
|
||||
* and translated texts on the translation items.
|
||||
*
|
||||
* @param document The document to be translated
|
||||
*/
|
||||
this.TranslationDocument = function(document) {
|
||||
this.itemsMap = new Map();
|
||||
this.roots = [];
|
||||
this._init(document);
|
||||
};
|
||||
|
||||
this.TranslationDocument.prototype = {
|
||||
translatedFrom: "",
|
||||
translatedTo: "",
|
||||
translationError: false,
|
||||
originalShown: true,
|
||||
|
||||
/**
|
||||
* Initializes the object and populates
|
||||
* the roots lists.
|
||||
*
|
||||
* @param document The document to be translated
|
||||
*/
|
||||
_init: function(document) {
|
||||
let window = document.defaultView;
|
||||
let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
|
||||
// Get all the translation nodes in the document's body:
|
||||
// a translation node is a node from the document which
|
||||
// contains useful content for translation, and therefore
|
||||
// must be included in the translation process.
|
||||
let nodeList = winUtils.getTranslationNodes(document.body);
|
||||
|
||||
let length = nodeList.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
let node = nodeList.item(i);
|
||||
let isRoot = nodeList.isTranslationRootAtIndex(i);
|
||||
|
||||
// Create a TranslationItem object for this node.
|
||||
// This function will also add it to the this.roots array.
|
||||
this._createItemForNode(node, i, isRoot);
|
||||
}
|
||||
|
||||
// At first all roots are stored in the roots list, and only after
|
||||
// the process has finished we're able to determine which roots are
|
||||
// simple, and which ones are not.
|
||||
|
||||
// A simple root is defined by a root with no children items, which
|
||||
// basically represents an element from a page with only text content
|
||||
// inside.
|
||||
|
||||
// This distinction is useful for optimization purposes: we treat a
|
||||
// simple root as plain-text in the translation process and with that
|
||||
// we are able to reduce their data payload sent to the translation service.
|
||||
|
||||
for (let root of this.roots) {
|
||||
if (root.children.length == 0 &&
|
||||
root.nodeRef.childElementCount == 0) {
|
||||
root.isSimpleRoot = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a TranslationItem object, which should be called
|
||||
* for each node returned by getTranslationNodes.
|
||||
*
|
||||
* @param node The DOM node for this item.
|
||||
* @param id A unique, numeric id for this item.
|
||||
* @parem isRoot A boolean saying whether this item is a root.
|
||||
*
|
||||
* @returns A TranslationItem object.
|
||||
*/
|
||||
_createItemForNode: function(node, id, isRoot) {
|
||||
if (this.itemsMap.has(node)) {
|
||||
return this.itemsMap.get(node);
|
||||
}
|
||||
|
||||
let item = new TranslationItem(node, id, isRoot);
|
||||
|
||||
if (isRoot) {
|
||||
// Root items do not have a parent item.
|
||||
this.roots.push(item);
|
||||
} else {
|
||||
let parentItem = this.itemsMap.get(node.parentNode);
|
||||
if (parentItem) {
|
||||
parentItem.children.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.itemsMap.set(node, item);
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the text string that represents a TranslationItem object.
|
||||
* Besides generating the string, it's also stored in the "original"
|
||||
* field of the TranslationItem object, which needs to be stored for
|
||||
* later to be used in the "Show Original" functionality.
|
||||
* If this function had already been called for the given item (determined
|
||||
* by the presence of the "original" array in the item), the text will
|
||||
* be regenerated from the "original" data instead of from the related
|
||||
* DOM nodes (because the nodes might contain translated data).
|
||||
*
|
||||
* @param item A TranslationItem object
|
||||
*
|
||||
* @returns A string representation of the TranslationItem.
|
||||
*/
|
||||
generateTextForItem: function(item) {
|
||||
if (item.original) {
|
||||
return regenerateTextFromOriginalHelper(item);
|
||||
}
|
||||
|
||||
if (item.isSimpleRoot) {
|
||||
let text = item.nodeRef.firstChild.nodeValue.trim();
|
||||
item.original = [text];
|
||||
return text;
|
||||
}
|
||||
|
||||
let str = "";
|
||||
item.original = [];
|
||||
let wasLastItemPlaceholder = false;
|
||||
|
||||
for (let child of item.nodeRef.childNodes) {
|
||||
if (child.nodeType == TEXT_NODE) {
|
||||
let x = child.nodeValue.trim();
|
||||
if (x != "") {
|
||||
item.original.push(x);
|
||||
str += x;
|
||||
wasLastItemPlaceholder = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let objInMap = this.itemsMap.get(child);
|
||||
if (objInMap && !objInMap.isRoot) {
|
||||
// If this childNode is present in the itemsMap, it means
|
||||
// it's a translation node: it has useful content for translation.
|
||||
// In this case, we need to stringify this node.
|
||||
// However, if this item is a root, we should skip it here in this
|
||||
// object's child list (and just add a placeholder for it), because
|
||||
// it will be stringfied separately for being a root.
|
||||
item.original.push(objInMap);
|
||||
str += this.generateTextForItem(objInMap);
|
||||
wasLastItemPlaceholder = false;
|
||||
} else if (!wasLastItemPlaceholder) {
|
||||
// Otherwise, if this node doesn't contain any useful content,
|
||||
// or if it is a root itself, we can replace it with a placeholder node.
|
||||
// We can't simply eliminate this node from our string representation
|
||||
// because that could change the HTML structure (e.g., it would
|
||||
// probably merge two separate text nodes).
|
||||
// It's not necessary to add more than one placeholder in sequence;
|
||||
// we can optimize them away.
|
||||
item.original.push(TranslationItem_NodePlaceholder);
|
||||
str += '<br>';
|
||||
wasLastItemPlaceholder = true;
|
||||
}
|
||||
}
|
||||
|
||||
return generateTranslationHtmlForItem(item, str);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the document to display its translated
|
||||
* content.
|
||||
*/
|
||||
showTranslation: function() {
|
||||
this.originalShown = false;
|
||||
this._swapDocumentContent("translation");
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the document to display its original
|
||||
* content.
|
||||
*/
|
||||
showOriginal: function() {
|
||||
this.originalShown = true;
|
||||
this._swapDocumentContent("original");
|
||||
},
|
||||
|
||||
/**
|
||||
* Swap the document with the resulting translation,
|
||||
* or back with the original content.
|
||||
*
|
||||
* @param target A string that is either "translation"
|
||||
* or "original".
|
||||
*/
|
||||
_swapDocumentContent: function(target) {
|
||||
Task.spawn(function *() {
|
||||
// Let the event loop breath on every 100 nodes
|
||||
// that are replaced.
|
||||
const YIELD_INTERVAL = 100;
|
||||
let count = YIELD_INTERVAL;
|
||||
|
||||
for (let root of this.roots) {
|
||||
root.swapText(target);
|
||||
if (count-- == 0) {
|
||||
count = YIELD_INTERVAL;
|
||||
yield CommonUtils.laterTickResolvingPromise();
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This class represents an item for translation. It's basically our
|
||||
* wrapper class around a node returned by getTranslationNode, with
|
||||
* more data and structural information on it.
|
||||
*
|
||||
* At the end of the translation process, besides the properties below,
|
||||
* a TranslationItem will contain two other properties: one called "original"
|
||||
* and one called "translation". They are twin objects, one which reflect
|
||||
* the structure of that node in its original state, and the other in its
|
||||
* translated state.
|
||||
*
|
||||
* The "original" array is generated in the generateTextForItem function,
|
||||
* and the "translation" array is generated when the translation results
|
||||
* are parsed.
|
||||
*
|
||||
* They are both arrays, which contain a mix of strings and references to
|
||||
* child TranslationItems. The references in both arrays point to the * same *
|
||||
* TranslationItem object, but they might appear in different orders between the
|
||||
* "original" and "translation" arrays.
|
||||
*
|
||||
* An example:
|
||||
*
|
||||
* English: <div id="n1">Welcome to <b id="n2">Mozilla's</b> website</div>
|
||||
* Portuguese: <div id="n1">Bem vindo a pagina <b id="n2">da Mozilla</b></div>
|
||||
*
|
||||
* TranslationItem n1 = {
|
||||
* id: 1,
|
||||
* original: ["Welcome to", ptr to n2, "website"]
|
||||
* translation: ["Bem vindo a pagina", ptr to n2]
|
||||
* }
|
||||
*
|
||||
* TranslationItem n2 = {
|
||||
* id: 2,
|
||||
* original: ["Mozilla's"],
|
||||
* translation: ["da Mozilla"]
|
||||
* }
|
||||
*/
|
||||
function TranslationItem(node, id, isRoot) {
|
||||
this.nodeRef = node;
|
||||
this.id = id;
|
||||
this.isRoot = isRoot;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
TranslationItem.prototype = {
|
||||
isRoot: false,
|
||||
isSimpleRoot: false,
|
||||
|
||||
toString: function() {
|
||||
let rootType = "";
|
||||
if (this.isRoot) {
|
||||
if (this.isSimpleRoot) {
|
||||
rootType = " (simple root)";
|
||||
}
|
||||
else {
|
||||
rootType = " (non simple root)";
|
||||
}
|
||||
}
|
||||
return "[object TranslationItem: <" + this.nodeRef.localName + ">"
|
||||
+ rootType + "]";
|
||||
},
|
||||
|
||||
/**
|
||||
* This function will parse the result of the translation of one translation
|
||||
* item. If this item was a simple root, all we sent was a plain-text version
|
||||
* of it, so the result is also straightforward text.
|
||||
*
|
||||
* For non-simple roots, we sent a simplified HTML representation of that
|
||||
* node, and we'll first parse that into an HTML doc and then call the
|
||||
* parseResultNode helper function to parse it.
|
||||
*
|
||||
* While parsing, the result is stored in the "translation" field of the
|
||||
* TranslationItem, which will be used to display the final translation when
|
||||
* all items are finished. It remains stored too to allow back-and-forth
|
||||
* switching between the "Show Original" and "Show Translation" functions.
|
||||
*
|
||||
* @param result A string with the textual result received from the server,
|
||||
* which can be plain-text or a serialized HTML doc.
|
||||
*/
|
||||
parseResult: function(result) {
|
||||
if (this.isSimpleRoot) {
|
||||
this.translation = [result];
|
||||
return;
|
||||
}
|
||||
|
||||
let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]
|
||||
.createInstance(Ci.nsIDOMParser);
|
||||
|
||||
let doc = domParser.parseFromString(result, "text/html");
|
||||
parseResultNode(this, doc.body.firstChild);
|
||||
},
|
||||
|
||||
/**
|
||||
* This function finds a child TranslationItem
|
||||
* with the given id.
|
||||
* @param id The id to look for, in the format "n#"
|
||||
* @returns A TranslationItem with the given id, or null if
|
||||
* it was not found.
|
||||
*/
|
||||
getChildById: function(id) {
|
||||
for (let child of this.children) {
|
||||
if (("n" + child.id) == id) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Swap the text of this TranslationItem between
|
||||
* its original and translated states.
|
||||
*
|
||||
* @param target A string that is either "translation"
|
||||
* or "original".
|
||||
*/
|
||||
swapText: function(target) {
|
||||
swapTextForItem(this, target);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This object represents a placeholder item for translation. It's similar to
|
||||
* the TranslationItem class, but it represents nodes that have no meaningful
|
||||
* content for translation. These nodes will be replaced by "<br>" in a
|
||||
* translation request. It's necessary to keep them to use it as a mark
|
||||
* for correct positioning and spliting of text nodes.
|
||||
*/
|
||||
const TranslationItem_NodePlaceholder = {
|
||||
toString: function() {
|
||||
return "[object TranslationItem_NodePlaceholder]";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the outer HTML representation for a given item.
|
||||
*
|
||||
* @param item A TranslationItem object.
|
||||
* param content The inner content for this item.
|
||||
* @returns string The outer HTML needed for translation
|
||||
* of this item.
|
||||
*/
|
||||
function generateTranslationHtmlForItem(item, content) {
|
||||
let localName = item.isRoot ? "div" : "b";
|
||||
return '<' + localName + ' id=n' + item.id + '>' +
|
||||
content +
|
||||
"</" + localName + ">";
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the text string that represents a TranslationItem object,
|
||||
* with data from its "original" array. The array must have already
|
||||
* been created by TranslationDocument.generateTextForItem().
|
||||
*
|
||||
* @param item A TranslationItem object
|
||||
*
|
||||
* @returns A string representation of the TranslationItem.
|
||||
*/
|
||||
function regenerateTextFromOriginalHelper(item) {
|
||||
if (item.isSimpleRoot) {
|
||||
return item.original[0];
|
||||
}
|
||||
|
||||
let str = "";
|
||||
for (let child of item.original) {
|
||||
if (child instanceof TranslationItem) {
|
||||
str += regenerateTextFromOriginalHelper(child);
|
||||
} else if (child === TranslationItem_NodePlaceholder) {
|
||||
str += "<br>";
|
||||
} else {
|
||||
str += child;
|
||||
}
|
||||
}
|
||||
|
||||
return generateTranslationHtmlForItem(item, str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to parse a HTML doc result.
|
||||
* How it works:
|
||||
*
|
||||
* An example result string is:
|
||||
*
|
||||
* <div id="n1">Hello <b id="n2">World</b> of Mozilla.</div>
|
||||
*
|
||||
* For an element node, we look at its id and find the corresponding
|
||||
* TranslationItem that was associated with this node, and then we
|
||||
* walk down it repeating the process.
|
||||
*
|
||||
* For text nodes we simply add it as a string.
|
||||
*/
|
||||
function parseResultNode(item, node) {
|
||||
item.translation = [];
|
||||
for (let child of node.childNodes) {
|
||||
if (child.nodeType == TEXT_NODE) {
|
||||
item.translation.push(child.nodeValue);
|
||||
} else if (child.localName == "br") {
|
||||
item.translation.push(TranslationItem_NodePlaceholder);
|
||||
} else {
|
||||
let translationItemChild = item.getChildById(child.id);
|
||||
|
||||
if (translationItemChild) {
|
||||
item.translation.push(translationItemChild);
|
||||
parseResultNode(translationItemChild, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to swap the text of a TranslationItem
|
||||
* between its original and translated states.
|
||||
* How it works:
|
||||
*
|
||||
* The function iterates through the target array (either the `original` or
|
||||
* `translation` array from the TranslationItem), while also keeping a pointer
|
||||
* to a current position in the child nodes from the actual DOM node that we
|
||||
* are modifying. This pointer is moved forward after each item of the array
|
||||
* is translated. If, at any given time, the pointer doesn't match the expected
|
||||
* node that was supposed to be seen, it means that the original and translated
|
||||
* contents have a different ordering, and thus we need to adjust that.
|
||||
*
|
||||
* A full example of the reordering process, swapping from Original to
|
||||
* Translation:
|
||||
*
|
||||
* Original (en): <div>I <em>miss</em> <b>you</b></div>
|
||||
*
|
||||
* Translation (fr): <div><b>Tu</b> me <em>manques</em></div>
|
||||
*
|
||||
* Step 1:
|
||||
* pointer points to firstChild of the DOM node, textnode "I "
|
||||
* first item in item.translation is [object TranslationItem <b>]
|
||||
*
|
||||
* pointer does not match the expected element, <b>. So let's move <b> to the
|
||||
* pointer position.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b>I <em>miss</em> </div>
|
||||
*
|
||||
* Step 2:
|
||||
* pointer moves forward to nextSibling, textnode "I " again.
|
||||
* second item in item.translation is the string " me "
|
||||
*
|
||||
* pointer points to a text node, and we were expecting a text node. Match!
|
||||
* just replace the text content.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b> me <em>miss</em> </div>
|
||||
*
|
||||
* Step 3:
|
||||
* pointer moves forward to nextSibling, <em>miss</em>
|
||||
* third item in item.translation is [object TranslationItem <em>]
|
||||
*
|
||||
* pointer points to the expected node. Match! Nothing to do.
|
||||
*
|
||||
* Step 4:
|
||||
* all items in this item.translation were transformed. The remaining
|
||||
* text nodes are cleared to "", and domNode.normalize() removes them.
|
||||
*
|
||||
* Current state of the DOM:
|
||||
* <div><b>you</b> me <em>miss</em></div>
|
||||
*
|
||||
* Further steps:
|
||||
* After that, the function will visit the child items (from the visitStack),
|
||||
* and the text inside the <b> and <em> nodes will be swapped as well,
|
||||
* yielding the final result:
|
||||
*
|
||||
* <div><b>Tu</b> me <em>manques</em></div>
|
||||
*
|
||||
*
|
||||
* @param item A TranslationItem object
|
||||
* @param target A string that is either "translation"
|
||||
* or "original".
|
||||
*/
|
||||
function swapTextForItem(item, target) {
|
||||
// visitStack is the stack of items that we still need to visit.
|
||||
// Let's start the process by adding the root item.
|
||||
let visitStack = [ item ];
|
||||
|
||||
while (visitStack.length > 0) {
|
||||
let curItem = visitStack.shift();
|
||||
|
||||
let domNode = curItem.nodeRef;
|
||||
if (!domNode) {
|
||||
// Skipping this item due to a missing node.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!curItem[target]) {
|
||||
// Translation not found for this item. This could be due to
|
||||
// an error in the server response. For example, if a translation
|
||||
// was broken in various chunks, and one of the chunks failed,
|
||||
// the items from that chunk will be missing its "translation"
|
||||
// field.
|
||||
continue;
|
||||
}
|
||||
|
||||
domNode.normalize();
|
||||
|
||||
// curNode points to the child nodes of the DOM node that we are
|
||||
// modifying. During most of the process, while the target array is
|
||||
// being iterated (in the for loop below), it should walk together with
|
||||
// the array and be pointing to the correct node that needs to modified.
|
||||
// If it's not pointing to it, that means some sort of node reordering
|
||||
// will be necessary to produce the correct translation.
|
||||
// Note that text nodes don't need to be reordered, as we can just replace
|
||||
// the content of one text node with another.
|
||||
//
|
||||
// curNode starts in the firstChild...
|
||||
let curNode = domNode.firstChild;
|
||||
|
||||
// ... actually, let's make curNode start at the first useful node (either
|
||||
// a non-blank text node or something else). This is not strictly necessary,
|
||||
// as the reordering algorithm would correctly handle this case. However,
|
||||
// this better aligns the resulting translation with the DOM content of the
|
||||
// page, avoiding cases that would need to be unecessarily reordered.
|
||||
//
|
||||
// An example of how this helps:
|
||||
//
|
||||
// ---- Original: <div> <b>Hello </b> world.</div>
|
||||
// ^textnode 1 ^item 1 ^textnode 2
|
||||
//
|
||||
// - Translation: <div><b>Hallo </b> Welt.</div>
|
||||
//
|
||||
// Transformation process without this optimization:
|
||||
// 1 - start pointer at textnode 1
|
||||
// 2 - move item 1 to first position inside the <div>
|
||||
//
|
||||
// Node now looks like: <div><b>Hello </b>[ ][ world.]</div>
|
||||
// textnode 1^ ^textnode 2
|
||||
//
|
||||
// 3 - replace textnode 1 with " Welt."
|
||||
// 4 - clear remaining text nodes (in this case, textnode 2)
|
||||
//
|
||||
// Transformation process with this optimization:
|
||||
// 1 - start pointer at item 1
|
||||
// 2 - item 1 is already in position
|
||||
// 3 - replace textnode 2 with " Welt."
|
||||
//
|
||||
// which completely avoids any node reordering, and requires only one
|
||||
// text change instead of two (while also leaving the page closer to
|
||||
// its original state).
|
||||
while (curNode &&
|
||||
curNode.nodeType == TEXT_NODE &&
|
||||
curNode.nodeValue.trim() == "") {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
// Now let's walk through all items in the `target` array of the
|
||||
// TranslationItem. This means either the TranslationItem.original or
|
||||
// TranslationItem.translation array.
|
||||
for (let targetItem of curItem[target]) {
|
||||
|
||||
if (targetItem instanceof TranslationItem) {
|
||||
// If the array element is another TranslationItem object, let's
|
||||
// add it to the stack to be visited.
|
||||
visitStack.push(targetItem);
|
||||
|
||||
let targetNode = targetItem.nodeRef;
|
||||
|
||||
// If the node is not in the expected position, let's reorder
|
||||
// it into position...
|
||||
if (curNode != targetNode &&
|
||||
// ...unless the page has reparented this node under a totally
|
||||
// different node (or removed it). In this case, all bets are off
|
||||
// on being able to do anything correctly, so it's better not to
|
||||
// bring back the node to this parent.
|
||||
targetNode.parentNode == domNode) {
|
||||
|
||||
// We don't need to null-check curNode because insertBefore(..., null)
|
||||
// does what we need in that case: reorder this node to the end
|
||||
// of child nodes.
|
||||
domNode.insertBefore(targetNode, curNode);
|
||||
curNode = targetNode;
|
||||
}
|
||||
|
||||
// Move pointer forward. Since we do not add empty text nodes to the
|
||||
// list of translation items, we must skip them here too while
|
||||
// traversing the DOM in order to get better alignment between the
|
||||
// text nodes and the translation items.
|
||||
if (curNode) {
|
||||
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
|
||||
}
|
||||
|
||||
} else if (targetItem === TranslationItem_NodePlaceholder) {
|
||||
// If the current item is a placeholder node, we need to move
|
||||
// our pointer "past" it, jumping from one side of a block of
|
||||
// elements + empty text nodes to the other side. Even if
|
||||
// non-placeholder elements exists inside the jumped block,
|
||||
// they will be pulled correctly later in the process when the
|
||||
// targetItem for those nodes are handled.
|
||||
|
||||
while (curNode &&
|
||||
(curNode.nodeType != TEXT_NODE ||
|
||||
curNode.nodeValue.trim() == "")) {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Finally, if it's a text item, we just need to find the next
|
||||
// text node to use. Text nodes don't need to be reordered, so
|
||||
// the first one found can be used.
|
||||
while (curNode && curNode.nodeType != TEXT_NODE) {
|
||||
curNode = curNode.nextSibling;
|
||||
}
|
||||
|
||||
// If none was found and we reached the end of the child nodes,
|
||||
// let's create a new one.
|
||||
if (!curNode) {
|
||||
// We don't know if the original content had a space or not,
|
||||
// so the best bet is to create the text node with " " which
|
||||
// will add one space at the beginning and one at the end.
|
||||
curNode = domNode.appendChild(domNode.ownerDocument.createTextNode(" "));
|
||||
}
|
||||
|
||||
// A trailing and a leading space must be preserved because
|
||||
// they are meaningful in HTML.
|
||||
let preSpace = /^\s/.test(curNode.nodeValue) ? " " : "";
|
||||
let endSpace = /\s$/.test(curNode.nodeValue) ? " " : "";
|
||||
|
||||
curNode.nodeValue = preSpace + targetItem + endSpace;
|
||||
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
|
||||
}
|
||||
}
|
||||
|
||||
// The translated version of a node might have less text nodes than its
|
||||
// original version. If that's the case, let's clear the remaining nodes.
|
||||
if (curNode) {
|
||||
clearRemainingNonEmptyTextNodesFromElement(curNode);
|
||||
}
|
||||
|
||||
// And remove any garbage "" nodes left after clearing.
|
||||
domNode.normalize();
|
||||
}
|
||||
}
|
||||
|
||||
function getNextSiblingSkippingEmptyTextNodes(startSibling) {
|
||||
let item = startSibling.nextSibling;
|
||||
while (item &&
|
||||
item.nodeType == TEXT_NODE &&
|
||||
item.nodeValue.trim() == "") {
|
||||
item = item.nextSibling;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function clearRemainingNonEmptyTextNodesFromElement(startSibling) {
|
||||
let item = startSibling;
|
||||
while (item) {
|
||||
if (item.nodeType == TEXT_NODE &&
|
||||
item.nodeValue != "") {
|
||||
item.nodeValue = "";
|
||||
}
|
||||
item = item.nextSibling;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,343 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "YandexTranslator" ];
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/Http.jsm");
|
||||
|
||||
// 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 number of chunks allowed to be translated in a single
|
||||
// request.
|
||||
const MAX_REQUEST_CHUNKS = 1000; // Documentation says 2000.
|
||||
|
||||
// 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 YANDEX_RETURN_CODE_OK = 200;
|
||||
|
||||
const YANDEX_ERR_KEY_INVALID = 401; // Invalid API key
|
||||
const YANDEX_ERR_KEY_BLOCKED = 402; // This API key has been blocked
|
||||
const YANDEX_ERR_DAILY_REQ_LIMIT_EXCEEDED = 403; // Daily limit for requests reached
|
||||
const YANDEX_ERR_DAILY_CHAR_LIMIT_EXCEEDED = 404; // Daily limit of chars reached
|
||||
const YANDEX_ERR_TEXT_TOO_LONG = 413; // The text size exceeds the maximum
|
||||
const YANDEX_ERR_UNPROCESSABLE_TEXT = 422; // The text could not be translated
|
||||
const YANDEX_ERR_LANG_NOT_SUPPORTED = 501; // The specified translation direction is not supported
|
||||
|
||||
// Errors that should activate the service unavailable handling
|
||||
const YANDEX_PERMANENT_ERRORS = [
|
||||
YANDEX_ERR_KEY_INVALID,
|
||||
YANDEX_ERR_KEY_BLOCKED,
|
||||
YANDEX_ERR_DAILY_REQ_LIMIT_EXCEEDED,
|
||||
YANDEX_ERR_DAILY_CHAR_LIMIT_EXCEEDED,
|
||||
];
|
||||
|
||||
/**
|
||||
* Translates a webpage using Yandex'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.
|
||||
*/
|
||||
this.YandexTranslator = 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.YandexTranslator.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: function() {
|
||||
return Task.spawn(function *() {
|
||||
let currentIndex = 0;
|
||||
this._onFinishedDeferred = Promise.defer();
|
||||
|
||||
// Let's split the document into various requests to be sent to
|
||||
// Yandex'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.
|
||||
yield CommonUtils.laterTickResolvingPromise();
|
||||
|
||||
// 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 yandexRequest = new YandexRequest(request.data,
|
||||
this.sourceLanguage,
|
||||
this.targetLanguage);
|
||||
this._pendingRequests++;
|
||||
yandexRequest.fireRequest().then(this._chunkCompleted.bind(this),
|
||||
this._chunkFailed.bind(this));
|
||||
|
||||
currentIndex = request.lastIndex;
|
||||
if (request.finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this._onFinishedDeferred.promise;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 are no pending requests left.
|
||||
*
|
||||
* @param request The YandexRequest sent to the server
|
||||
*/
|
||||
_chunkCompleted: function(yandexRequest) {
|
||||
if (this._parseChunkResult(yandexRequest)) {
|
||||
this._partialSuccess = true;
|
||||
// Count the number of characters successfully translated.
|
||||
this._translatedCharacterCount += yandexRequest.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 are no pending
|
||||
* requests left.
|
||||
*
|
||||
* @param aError [optional] The XHR object of the request that failed.
|
||||
*/
|
||||
_chunkFailed: function(aError) {
|
||||
if (aError instanceof Ci.nsIXMLHttpRequest) {
|
||||
let body = aError.responseText;
|
||||
let json = { code: 0 };
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch (e) {}
|
||||
|
||||
if (json.code && YANDEX_PERMANENT_ERRORS.indexOf(json.code) != -1)
|
||||
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.
|
||||
*/
|
||||
_checkIfFinished: function() {
|
||||
// Check if all pending requests have been
|
||||
// completed and then resolves the promise.
|
||||
// If at least one chunk was successful, the
|
||||
// promise will be resolved positively which will
|
||||
// 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 function parses the result returned by Yandex's Translation API,
|
||||
* which returns a JSON result that contains a number of elements. The
|
||||
* API is documented here:
|
||||
* http://api.yandex.com/translate/doc/dg/reference/translate.xml
|
||||
*
|
||||
* @param request The request sent to the server.
|
||||
* @returns boolean True if parsing of this chunk was successful.
|
||||
*/
|
||||
_parseChunkResult: function(yandexRequest) {
|
||||
let results;
|
||||
try {
|
||||
let result = JSON.parse(yandexRequest.networkRequest.responseText);
|
||||
if (result.code != 200) {
|
||||
Services.console.logStringMessage("YandexTranslator: Result is " + result.code);
|
||||
return false;
|
||||
}
|
||||
results = result.text
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let len = results.length;
|
||||
if (len != yandexRequest.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];
|
||||
let root = yandexRequest.translationData[i][0];
|
||||
root.parseResult(result);
|
||||
} catch (e) { error = true; }
|
||||
}
|
||||
|
||||
return !error;
|
||||
},
|
||||
|
||||
/**
|
||||
* This function will determine what is the data to be used for
|
||||
* the Nth request we are generating, based on the input params.
|
||||
*
|
||||
* @param startIndex What is the index, in the roots list, that the
|
||||
* chunk should start.
|
||||
*/
|
||||
_generateNextTranslationRequest: function(startIndex) {
|
||||
let currentDataSize = 0;
|
||||
let currentChunks = 0;
|
||||
let output = [];
|
||||
let rootsList = this.translationDocument.roots;
|
||||
|
||||
for (let i = startIndex; i < rootsList.length; i++) {
|
||||
let root = rootsList[i];
|
||||
let text = this.translationDocument.generateTextForItem(root);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
// the caller to pass back on the next call, so that the function
|
||||
// can keep working from where it stopped.
|
||||
return {
|
||||
data: output,
|
||||
finished: false,
|
||||
lastIndex: i
|
||||
};
|
||||
}
|
||||
|
||||
currentDataSize = newCurSize;
|
||||
currentChunks = newChunks;
|
||||
output.push([root, text]);
|
||||
}
|
||||
|
||||
return {
|
||||
data: output,
|
||||
finished: true,
|
||||
lastIndex: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a request (for 1 chunk) sent off to Yandex'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 YandexRequest(translationData, sourceLanguage, targetLanguage) {
|
||||
this.translationData = translationData;
|
||||
this.sourceLanguage = sourceLanguage;
|
||||
this.targetLanguage = targetLanguage;
|
||||
this.characterCount = 0;
|
||||
}
|
||||
|
||||
YandexRequest.prototype = {
|
||||
/**
|
||||
* Initiates the request
|
||||
*/
|
||||
fireRequest: function() {
|
||||
return Task.spawn(function *() {
|
||||
// Prepare URL.
|
||||
let url = getUrlParam("https://translate.yandex.net/api/v1.5/tr.json/translate",
|
||||
"browser.translation.yandex.translateURLOverride");
|
||||
|
||||
// Prepare the request body.
|
||||
let apiKey = getUrlParam("%YANDEX_API_KEY%", "browser.translation.yandex.apiKeyOverride");
|
||||
let params = [
|
||||
["key", apiKey],
|
||||
["format", "html"],
|
||||
["lang", this.sourceLanguage + "-" + this.targetLanguage],
|
||||
];
|
||||
|
||||
for (let [, text] of this.translationData) {
|
||||
params.push(["text", text]);
|
||||
this.characterCount += text.length;
|
||||
}
|
||||
|
||||
// Set up request options.
|
||||
let deferred = Promise.defer();
|
||||
let options = {
|
||||
onLoad: (function(responseText, xhr) {
|
||||
deferred.resolve(this);
|
||||
}).bind(this),
|
||||
onError: function(e, responseText, xhr) {
|
||||
deferred.reject(xhr);
|
||||
},
|
||||
postData: params
|
||||
};
|
||||
|
||||
// Fire the request.
|
||||
this.networkRequest = httpRequest(url, options);
|
||||
|
||||
return deferred.promise;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# 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/.
|
||||
browser.jar:
|
||||
content/browser/translation-infobar.xml
|
||||
content/browser/microsoft-translator-attribution.png
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
|
|
@ -3,14 +3,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXTRA_JS_MODULES.translation = [
|
||||
'BingTranslator.jsm',
|
||||
'cld2/cld-worker.js',
|
||||
'cld2/cld-worker.js.mem',
|
||||
'LanguageDetector.jsm',
|
||||
'Translation.jsm',
|
||||
'TranslationContentHandler.jsm',
|
||||
'TranslationDocument.jsm',
|
||||
'YandexTranslator.jsm'
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
|
|
|||
|
|
@ -1,441 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<!DOCTYPE bindings [
|
||||
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
|
||||
%notificationDTD;
|
||||
<!ENTITY % translationDTD SYSTEM "chrome://browser/locale/translation.dtd" >
|
||||
%translationDTD;
|
||||
]>
|
||||
|
||||
<bindings id="translationBindings"
|
||||
xmlns="http://www.mozilla.org/xbl"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:xbl="http://www.mozilla.org/xbl">
|
||||
<binding id="translationbar" extends="chrome://global/content/bindings/notification.xml#notification" role="xul:alert">
|
||||
<resources>
|
||||
<stylesheet src="chrome://global/skin/notification.css"/>
|
||||
</resources>
|
||||
<content>
|
||||
<xul:hbox class="notification-inner" flex="1" xbl:inherits="type">
|
||||
<xul:hbox anonid="details" align="center" flex="1">
|
||||
<xul:image class="translate-infobar-element messageImage"
|
||||
anonid="messageImage"/>
|
||||
<xul:panel anonid="welcomePanel" class="translation-welcome-panel"
|
||||
type="arrow" align="start">
|
||||
<xul:image class="translation-welcome-logo"/>
|
||||
<xul:vbox flex="1" class="translation-welcome-content">
|
||||
<xul:description class="translation-welcome-headline"
|
||||
anonid="welcomeHeadline"/>
|
||||
<xul:description class="translation-welcome-body" anonid="welcomeBody"/>
|
||||
<xul:hbox align="center">
|
||||
<xul:label anonid="learnMore" class="plain text-link"
|
||||
onclick="openUILinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
|
||||
<xul:spacer flex="1"/>
|
||||
<xul:button class="translate-infobar-element" anonid="thanksButton"
|
||||
onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
|
||||
</xul:hbox>
|
||||
</xul:vbox>
|
||||
</xul:panel>
|
||||
<xul:deck anonid="translationStates" selectedIndex="0">
|
||||
|
||||
<!-- offer to translate -->
|
||||
<xul:hbox class="translate-offer-box" align="center">
|
||||
<xul:label class="translate-infobar-element" value="&translation.thisPageIsIn.label;"/>
|
||||
<xul:menulist class="translate-infobar-element" anonid="detectedLanguage">
|
||||
<xul:menupopup/>
|
||||
</xul:menulist>
|
||||
<xul:label class="translate-infobar-element" value="&translation.translateThisPage.label;"/>
|
||||
<xul:button class="translate-infobar-element"
|
||||
label="&translation.translate.button;"
|
||||
anonid="translate"
|
||||
oncommand="document.getBindingParent(this).translate();"/>
|
||||
<xul:button class="translate-infobar-element"
|
||||
label="&translation.notNow.button;" anonid="notNow"
|
||||
oncommand="document.getBindingParent(this).closeCommand();"/>
|
||||
</xul:hbox>
|
||||
|
||||
<!-- translating -->
|
||||
<xul:vbox class="translating-box" pack="center">
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.translatingContent.label;"/>
|
||||
</xul:vbox>
|
||||
|
||||
<!-- translated -->
|
||||
<xul:hbox class="translated-box" align="center">
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.translatedFrom.label;"/>
|
||||
<xul:menulist class="translate-infobar-element"
|
||||
anonid="fromLanguage"
|
||||
oncommand="document.getBindingParent(this).translate()">
|
||||
<xul:menupopup/>
|
||||
</xul:menulist>
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.translatedTo.label;"/>
|
||||
<xul:menulist class="translate-infobar-element"
|
||||
anonid="toLanguage"
|
||||
oncommand="document.getBindingParent(this).translate()">
|
||||
<xul:menupopup/>
|
||||
</xul:menulist>
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.translatedToSuffix.label;"/>
|
||||
<xul:button anonid="showOriginal"
|
||||
class="translate-infobar-element"
|
||||
label="&translation.showOriginal.button;"
|
||||
oncommand="document.getBindingParent(this).showOriginal();"/>
|
||||
<xul:button anonid="showTranslation"
|
||||
class="translate-infobar-element"
|
||||
label="&translation.showTranslation.button;"
|
||||
oncommand="document.getBindingParent(this).showTranslation();"/>
|
||||
</xul:hbox>
|
||||
|
||||
<!-- error -->
|
||||
<xul:hbox class="translation-error" align="center">
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.errorTranslating.label;"/>
|
||||
<xul:button class="translate-infobar-element"
|
||||
label="&translation.tryAgain.button;"
|
||||
anonid="tryAgain"
|
||||
oncommand="document.getBindingParent(this).translate();"/>
|
||||
</xul:hbox>
|
||||
|
||||
<!-- unavailable -->
|
||||
<xul:vbox class="translation-unavailable" pack="center">
|
||||
<xul:label class="translate-infobar-element"
|
||||
value="&translation.serviceUnavailable.label;"/>
|
||||
</xul:vbox>
|
||||
|
||||
</xul:deck>
|
||||
<xul:spacer flex="1"/>
|
||||
|
||||
<xul:button type="menu"
|
||||
class="translate-infobar-element options-menu-button"
|
||||
anonid="options"
|
||||
label="&translation.options.menu;">
|
||||
<xul:menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
|
||||
cui-widget-panelWithFooter PanelUI-subView"
|
||||
onpopupshowing="document.getBindingParent(this).optionsShowing();">
|
||||
<xul:menuitem anonid="neverForLanguage"
|
||||
oncommand="document.getBindingParent(this).neverForLanguage();"/>
|
||||
<xul:menuitem anonid="neverForSite"
|
||||
oncommand="document.getBindingParent(this).neverForSite();"
|
||||
label="&translation.options.neverForSite.label;"
|
||||
accesskey="&translation.options.neverForSite.accesskey;"/>
|
||||
<xul:menuseparator/>
|
||||
<xul:menuitem oncommand="openPreferences('paneContent');"
|
||||
label="&translation.options.preferences.label;"
|
||||
accesskey="&translation.options.preferences.accesskey;"/>
|
||||
<xul:menuitem class="subviewbutton panel-subview-footer"
|
||||
oncommand="document.getBindingParent(this).openProviderAttribution();">
|
||||
<xul:deck anonid="translationEngine" selectedIndex="0">
|
||||
<xul:hbox class="translation-attribution">
|
||||
<xul:label>&translation.options.attribution.beforeLogo;</xul:label>
|
||||
<xul:image src="chrome://browser/content/microsoft-translator-attribution.png"
|
||||
aria-label="Microsoft Translator"/>
|
||||
<xul:label>&translation.options.attribution.afterLogo;</xul:label>
|
||||
</xul:hbox>
|
||||
<xul:label class="translation-attribution">&translation.options.attribution.yandexTranslate;</xul:label>
|
||||
</xul:deck>
|
||||
</xul:menuitem>
|
||||
</xul:menupopup>
|
||||
</xul:button>
|
||||
|
||||
</xul:hbox>
|
||||
<xul:toolbarbutton ondblclick="event.stopPropagation();"
|
||||
anonid="closeButton"
|
||||
class="messageCloseButton close-icon tabbable"
|
||||
xbl:inherits="hidden=hideclose"
|
||||
tooltiptext="&closeNotification.tooltip;"
|
||||
oncommand="document.getBindingParent(this).closeCommand();"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
<implementation>
|
||||
<property name="state"
|
||||
onget="return this._getAnonElt('translationStates').selectedIndex;">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
let deck = this._getAnonElt('translationStates');
|
||||
|
||||
let activeElt = document.activeElement;
|
||||
if (activeElt && deck.contains(activeElt))
|
||||
activeElt.blur();
|
||||
|
||||
let stateName;
|
||||
for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
|
||||
if (Translation["STATE_" + name] == val) {
|
||||
stateName = name.toLowerCase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setAttribute("state", stateName);
|
||||
|
||||
if (val == Translation.STATE_TRANSLATED)
|
||||
this._handleButtonHiding();
|
||||
|
||||
deck.selectedIndex = val;
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<method name="init">
|
||||
<parameter name="aTranslation"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.translation = aTranslation;
|
||||
let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
|
||||
.getService(Ci.nsIStringBundleService)
|
||||
.createBundle("chrome://global/locale/languageNames.properties");
|
||||
let sortByLocalizedName = function(aList) {
|
||||
return aList.map(code => [code, bundle.GetStringFromName(code)])
|
||||
.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
};
|
||||
|
||||
// Fill the lists of supported source languages.
|
||||
let detectedLanguage = this._getAnonElt("detectedLanguage");
|
||||
let fromLanguage = this._getAnonElt("fromLanguage");
|
||||
let sourceLanguages =
|
||||
sortByLocalizedName(Translation.supportedSourceLanguages);
|
||||
for (let [code, name] of sourceLanguages) {
|
||||
detectedLanguage.appendItem(name, code);
|
||||
fromLanguage.appendItem(name, code);
|
||||
}
|
||||
detectedLanguage.value = this.translation.detectedLanguage;
|
||||
|
||||
// translatedFrom is only set if we have already translated this page.
|
||||
if (aTranslation.translatedFrom)
|
||||
fromLanguage.value = aTranslation.translatedFrom;
|
||||
|
||||
// Fill the list of supported target languages.
|
||||
let toLanguage = this._getAnonElt("toLanguage");
|
||||
let targetLanguages =
|
||||
sortByLocalizedName(Translation.supportedTargetLanguages);
|
||||
for (let [code, name] of targetLanguages)
|
||||
toLanguage.appendItem(name, code);
|
||||
|
||||
if (aTranslation.translatedTo)
|
||||
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);
|
||||
if (engineIndex != -1) {
|
||||
this._getAnonElt('translationEngine').selectedIndex = engineIndex;
|
||||
}
|
||||
|
||||
const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
|
||||
if (Services.prefs.prefHasUserValue(kWelcomePref) ||
|
||||
this.translation.browser != gBrowser.selectedBrowser)
|
||||
return;
|
||||
|
||||
this.addEventListener("transitionend", function onShown() {
|
||||
this.removeEventListener("transitionend", onShown);
|
||||
|
||||
// These strings are hardcoded because they need to reach beta
|
||||
// without riding the trains.
|
||||
let localizedStrings = {
|
||||
en: ["Hey look! It's something new!",
|
||||
"Now the Web is even more accessible with our new in-page translation feature. Click the translate button to try it!",
|
||||
"Learn more.",
|
||||
"Thanks"],
|
||||
"es-AR": ["\xA1Mir\xE1! \xA1Hay algo nuevo!",
|
||||
"Ahora la web es a\xFAn m\xE1s accesible con nuestra nueva funcionalidad de traducci\xF3n integrada. \xA1Hac\xE9 clic en el bot\xF3n traducir para probarla!",
|
||||
"Conoc\xE9 m\xE1s.",
|
||||
"Gracias"],
|
||||
"es-ES": ["\xA1Mira! \xA1Hay algo nuevo!",
|
||||
"Con la nueva funcionalidad de traducci\xF3n integrada, ahora la Web es a\xFAn m\xE1s accesible. \xA1Pulsa el bot\xF3n Traducir y pru\xE9bala!",
|
||||
"M\xE1s informaci\xF3n.",
|
||||
"Gracias"],
|
||||
pl: ["Sp\xF3jrz tutaj! To co\u015B nowego!",
|
||||
"Sie\u0107 sta\u0142a si\u0119 w\u0142a\u015Bnie jeszcze bardziej dost\u0119pna dzi\u0119ki opcji bezpo\u015Bredniego t\u0142umaczenia stron. Kliknij przycisk t\u0142umaczenia, aby spr\xF3bowa\u0107!",
|
||||
"Dowiedz si\u0119 wi\u0119cej",
|
||||
"Dzi\u0119kuj\u0119"],
|
||||
tr: ["Bak\u0131n, burada yeni bir \u015Fey var!",
|
||||
"Yeni sayfa i\xE7i \xE7eviri \xF6zelli\u011Fimiz sayesinde Web art\u0131k \xE7ok daha anla\u015F\u0131l\u0131r olacak. Denemek i\xE7in \xC7evir d\xFC\u011Fmesine t\u0131klay\u0131n!",
|
||||
"Daha fazla bilgi al\u0131n.",
|
||||
"Te\u015Fekk\xFCrler"],
|
||||
vi: ["Nh\xECn n\xE0y! \u0110\u1ED3 m\u1EDBi!",
|
||||
"Gi\u1EDD \u0111\xE2y ch\xFAng ta c\xF3 th\u1EC3 ti\u1EBFp c\u1EADn web d\u1EC5 d\xE0ng h\u01A1n n\u1EEFa v\u1EDBi t\xEDnh n\u0103ng d\u1ECBch ngay trong trang. Hay nh\u1EA5n n\xFAt d\u1ECBch \u0111\u1EC3 th\u1EED!",
|
||||
"T\xECm hi\u1EC3u th\xEAm.",
|
||||
"C\u1EA3m \u01A1n"]
|
||||
};
|
||||
|
||||
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIXULChromeRegistry)
|
||||
.getSelectedLocale("browser");
|
||||
if (!(locale in localizedStrings))
|
||||
locale = "en";
|
||||
let strings = localizedStrings[locale];
|
||||
|
||||
this._getAnonElt("welcomeHeadline").setAttribute("value", strings[0]);
|
||||
this._getAnonElt("welcomeBody").textContent = strings[1];
|
||||
this._getAnonElt("learnMore").setAttribute("value", strings[2]);
|
||||
this._getAnonElt("thanksButton").setAttribute("label", strings[3]);
|
||||
|
||||
let panel = this._getAnonElt("welcomePanel");
|
||||
panel.openPopup(this._getAnonElt("messageImage"),
|
||||
"bottomcenter topleft");
|
||||
|
||||
Services.prefs.setBoolPref(kWelcomePref, true);
|
||||
});
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="_getAnonElt">
|
||||
<parameter name="aAnonId"/>
|
||||
<body>
|
||||
return document.getAnonymousElementByAttribute(this, "anonid", aAnonId);
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="translate">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (this.state == Translation.STATE_OFFER) {
|
||||
this._getAnonElt("fromLanguage").value =
|
||||
this._getAnonElt("detectedLanguage").value;
|
||||
this._getAnonElt("toLanguage").value =
|
||||
Translation.defaultTargetLanguage;
|
||||
}
|
||||
|
||||
this.translation.translate(this._getAnonElt("fromLanguage").value,
|
||||
this._getAnonElt("toLanguage").value);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<!-- To be called when the infobar should be closed per user's wish (e.g.
|
||||
by clicking the notification's close button -->
|
||||
<method name="closeCommand">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.close();
|
||||
this.translation.infobarClosed();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
<method name="_handleButtonHiding">
|
||||
<body>
|
||||
<![CDATA[
|
||||
let originalShown = this.translation.originalShown;
|
||||
this._getAnonElt("showOriginal").hidden = originalShown;
|
||||
this._getAnonElt("showTranslation").hidden = !originalShown;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="showOriginal">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.translation.showOriginalContent();
|
||||
this._handleButtonHiding();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="showTranslation">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.translation.showTranslatedContent();
|
||||
this._handleButtonHiding();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="optionsShowing">
|
||||
<body>
|
||||
<![CDATA[
|
||||
// Get the source language name.
|
||||
let lang;
|
||||
if (this.state == Translation.STATE_OFFER)
|
||||
lang = this._getAnonElt("detectedLanguage").value;
|
||||
else {
|
||||
lang = this._getAnonElt("fromLanguage").value;
|
||||
|
||||
// If we have never attempted to translate the page before the
|
||||
// service became unavailable, "fromLanguage" isn't set.
|
||||
if (!lang && this.state == Translation.STATE_UNAVAILABLE)
|
||||
lang = this.translation.detectedLanguage;
|
||||
}
|
||||
|
||||
let langBundle =
|
||||
Cc["@mozilla.org/intl/stringbundle;1"]
|
||||
.getService(Ci.nsIStringBundleService)
|
||||
.createBundle("chrome://global/locale/languageNames.properties");
|
||||
let langName = langBundle.GetStringFromName(lang);
|
||||
|
||||
// Set the label and accesskey on the menuitem.
|
||||
let bundle =
|
||||
Cc["@mozilla.org/intl/stringbundle;1"]
|
||||
.getService(Ci.nsIStringBundleService)
|
||||
.createBundle("chrome://browser/locale/translation.properties");
|
||||
let item = this._getAnonElt("neverForLanguage");
|
||||
const kStrId = "translation.options.neverForLanguage";
|
||||
item.setAttribute("label",
|
||||
bundle.formatStringFromName(kStrId + ".label",
|
||||
[langName], 1));
|
||||
item.setAttribute("accesskey",
|
||||
bundle.GetStringFromName(kStrId + ".accesskey"));
|
||||
item.langCode = lang;
|
||||
|
||||
// We may need to disable the menuitems if they have already been used.
|
||||
// Check if translation is already disabled for this language:
|
||||
let neverForLangs =
|
||||
Services.prefs.getCharPref("browser.translation.neverForLanguages");
|
||||
item.disabled = neverForLangs.split(",").indexOf(lang) != -1;
|
||||
|
||||
// Check if translation is disabled for the domain:
|
||||
let uri = this.translation.browser.currentURI;
|
||||
let perms = Services.perms;
|
||||
item = this._getAnonElt("neverForSite");
|
||||
item.disabled =
|
||||
perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="neverForLanguage">
|
||||
<body>
|
||||
<![CDATA[
|
||||
const kPrefName = "browser.translation.neverForLanguages";
|
||||
|
||||
let val = Services.prefs.getCharPref(kPrefName);
|
||||
if (val)
|
||||
val += ",";
|
||||
val += this._getAnonElt("neverForLanguage").langCode;
|
||||
|
||||
Services.prefs.setCharPref(kPrefName, val);
|
||||
|
||||
this.closeCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="neverForSite">
|
||||
<body>
|
||||
<![CDATA[
|
||||
let uri = this.translation.browser.currentURI;
|
||||
let perms = Services.perms;
|
||||
perms.add(uri, "translate", perms.DENY_ACTION);
|
||||
|
||||
this.closeCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="openProviderAttribution">
|
||||
<body>
|
||||
<![CDATA[
|
||||
Translation.openProviderAttribution();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
</implementation>
|
||||
</binding>
|
||||
</bindings>
|
||||
|
|
@ -284,12 +284,6 @@ user_pref("apz.content_response_timeout", 60000);
|
|||
// Make sure SSL Error reports don't hit the network
|
||||
user_pref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/ssl_error_reports.sjs?succeed");
|
||||
|
||||
// Make sure Translation won't hit the network.
|
||||
user_pref("browser.translation.bing.authURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
|
||||
user_pref("browser.translation.bing.translateArrayURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
|
||||
user_pref("browser.translation.yandex.translateURLOverride", "http://%(server)s/browser/browser/components/translation/test/yandex.sjs");
|
||||
user_pref("browser.translation.engine", "bing");
|
||||
|
||||
// Make sure we don't try to load snippets from the network.
|
||||
user_pref("browser.aboutHomeSnippets.updateUrl", "nonexistent://test");
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,6 @@
|
|||
"tokenserverclient.js": ["TokenServerClient", "TokenServerClientError", "TokenServerClientNetworkError", "TokenServerClientServerError"],
|
||||
"ToolboxProcess.jsm": ["BrowserToolboxProcess"],
|
||||
"tps.jsm": ["ACTIONS", "TPS"],
|
||||
"Translation.jsm": ["Translation", "TranslationTelemetry"],
|
||||
"Traversal.jsm": ["TraversalRules", "TraversalHelper"],
|
||||
"UpdateTelemetry.jsm": ["AUSTLMY"],
|
||||
"userapi.js": ["UserAPI10Client"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue