Bug 1710546 - Firefox Translations integration r=mossop,mixedpuppy,mhoye

Bundle Firefox Translation as a builtin pref'd off addon in Nightly only

Differential Revision: https://phabricator.services.mozilla.com/D114810
This commit is contained in:
Andre Natal 2021-05-28 18:38:59 +00:00
parent 1ee8086c02
commit 760b23419b
33 changed files with 50309 additions and 1 deletions

View File

@ -218,3 +218,6 @@ tools/update-packaging/**/*refs.js
# Ignore backgroundtasks preferences files.
toolkit/components/backgroundtasks/defaults
# Ignore pre-generated webpack and typescript transpiled files for translations
browser/extensions/translations/extension/

View File

@ -2583,3 +2583,8 @@ pref("first-startup.timeout", 30000);
// are expected to go away once a standardized alternative becomes
// available.
pref("svg.context-properties.content.allowed-domains", "profile.accounts.firefox.com,profile.stage.mozaws.net");
// Preference that allows individual users to disable Firefox Translations.
#ifdef NIGHTLY_BUILD
pref("extensions.translations.disabled", true);
#endif

View File

@ -69,6 +69,11 @@ if (AppConstants.MOZ_BACKGROUNDTASKS) {
gExceptionPaths.push("resource://gre/modules/backgroundtasks/");
}
// Bug 1710546 https://bugzilla.mozilla.org/show_bug.cgi?id=1710546
if (AppConstants.NIGHTLY_BUILD) {
gExceptionPaths.push("resource://builtin-addons/translations/");
}
// Each whitelist entry should have a comment indicating which file is
// referencing the whitelisted file in a way that the test can't detect, or a
// bug number to remove or use the file if it is indeed currently unreferenced.

View File

@ -1975,6 +1975,35 @@ BrowserGlue.prototype = {
});
},
// Set up a listener to enable/disable the translation extension
// based on its preference.
_monitorTranslationsPref() {
const PREF = "extensions.translations.disabled";
const ID = "firefox-translations@mozilla.org";
const _checkTranslationsPref = async () => {
let addon = await AddonManager.getAddonByID(ID);
let disabled = Services.prefs.getBoolPref(PREF, false);
if (!addon && disabled) {
// not installed, bail out early.
return;
}
if (!disabled) {
// first time install of addon and install on firefox update
addon =
(await AddonManager.maybeInstallBuiltinAddon(
ID,
"0.4.0",
"resource://builtin-addons/translations/"
)) || addon;
await addon.enable();
} else if (addon) {
await addon.disable();
}
};
Services.prefs.addObserver(PREF, _checkTranslationsPref);
_checkTranslationsPref();
},
_monitorHTTPSOnlyPref() {
const PREF_ENABLED = "dom.security.https_only_mode";
const PREF_WAS_ENABLED = "dom.security.https_only_mode_ever_enabled";
@ -2172,6 +2201,9 @@ BrowserGlue.prototype = {
this._monitorHTTPSOnlyPref();
this._monitorIonPref();
this._monitorIonStudies();
if (AppConstants.NIGHTLY_BUILD) {
this._monitorTranslationsPref();
}
FirefoxMonitor.init();
},

View File

@ -12,3 +12,8 @@ DIRS += [
"report-site-issue",
"pictureinpicture",
]
if CONFIG["NIGHTLY_BUILD"]:
DIRS += [
"translations",
]

View File

@ -0,0 +1,20 @@
This folder contains the source files for Firefox Translations, which utilizes
the proceedings from Project Bergamot [1].
The feature is developed as a webextension [2] which utilizes a neural machine
translation decoder [3] ported to WebAssembly in order to process the
in page translation. The translation models [4] are downloaded on-demand by the
extension, so there's no need to bundle them too.
The folder `extension_src` contains the entire code of the extension's xpi and
the wasm module (which lies inside the folder wasm), and is automatically
generated by the `import_xpi.py` script, which is responsibile for cloning
the extension repo [2], build it, and generate the `jar.mn` package containing
all the pertinent files necessary for running it.
For any questions, reach out to anatal@mozilla.com.
[1] https://browser.mt/
[2] https://github.com/mozilla-extensions/bergamot-browser-extension
[3] https://github.com/mozilla/bergamot-translator/
[4] https://github.com/mozilla-applied-ml/bergamot-models

View File

@ -0,0 +1,46 @@
{
"extensionName": {
"message": "Firefox Translations",
"description": "Name of the extension"
},
"extensionDescription": {
"message": "Neural Machine Translation for the browser.",
"description": "Description of the extension."
},
"currentlyDownloadingLanguageModel": {
"message": "Currently downloading language model",
"description": "Informs the user that the translations feature is currently downloading the language model files."
},
"detailedDownloadProgress": {
"message": "$PERCENTAGE$% out of $MB$ mb downloaded",
"description": "Informs the user how the language model file downloading is progressing.",
"placeholders": {
"percentage": {
"content": "$1",
"example": "50"
},
"mb": {
"content": "$2",
"example": "22.2"
}
}
},
"currentlyLoadingLanguageModel": {
"message": "Currently loading language model",
"description": "Informs the user that the translations feature is currently loading the language model files into memory."
},
"loadedLanguageModel": {
"message": "Language model loaded",
"description": "Informs the user that the translations feature is has loaded the language model files into memory."
},
"partsLeftToTranslate": {
"message": "Parts left to translate: $NUM$",
"description": "Informs the user that the translations feature has $NUM$ parts left to translate.",
"placeholders": {
"num": {
"content": "$1",
"example": "3"
}
}
}
}

View File

@ -0,0 +1,46 @@
{
"extensionName": {
"message": "Traducciones de Firefox",
"description": "Name of the extension"
},
"extensionDescription": {
"message": "Traducción de la Máquina Neural para el navegador.",
"description": "Description of the extension."
},
"currentlyDownloadingLanguageModel": {
"message": "Actualmente descargando el modelo de idioma",
"description": "Informs the user that the translations feature is currently downloading the language model files."
},
"detailedDownloadProgress": {
"message": "$PERCENTAGE$% de $MB$ mb descargado",
"description": "Informs the user how the language model file downloading is progressing.",
"placeholders": {
"percentage": {
"content": "$1",
"example": "50"
},
"mb": {
"content": "$2",
"example": "22.2"
}
}
},
"currentlyLoadingLanguageModel": {
"message": "Actualmente cargar el modelo de idioma",
"description": "Informs the user that the translations feature is currently loading the language model files into memory."
},
"loadedLanguageModel": {
"message": "Modelo de lenguaje cargado",
"description": "Informs the user that the translations feature is has loaded the language model files into memory."
},
"partsLeftToTranslate": {
"message": "Partes que quedan para traducir: $NUM$",
"description": "Informs the user that the translations feature has $NUM$ parts left to translate.",
"placeholders": {
"num": {
"content": "$1",
"example": "3"
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
/* 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/. */
/* global ExtensionAPI, ExtensionCommon, Services */
"use strict";
this.extensionPreferences = class extends ExtensionAPI {
getAPI() {
const { Preferences } = ChromeUtils.import(
"resource://gre/modules/Preferences.jsm",
{},
);
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm",
{},
);
const { ExtensionError } = ExtensionUtils;
const telemetryInactivityThresholdInSecondsOverridePrefName = `extensions.translations.telemetryInactivityThresholdInSecondsOverride`;
return {
experiments: {
extensionPreferences: {
async getTelemetryInactivityThresholdInSecondsOverridePref() {
try {
const value = Preferences.get(
telemetryInactivityThresholdInSecondsOverridePrefName,
false,
);
if (!value) {
return false;
}
return parseFloat(value);
} catch (error) {
// Surface otherwise silent or obscurely reported errors
console.error(error.message, error.stack);
throw new ExtensionError(error.message);
}
},
},
},
};
}
};

View File

@ -0,0 +1,15 @@
[
{
"namespace": "experiments.extensionPreferences",
"description": "Enables read-only access to specific Extensions-related about:config preferences",
"functions": [
{
"name": "getTelemetryInactivityThresholdInSecondsOverridePref",
"type": "function",
"description": "Get the `extensions.translations.telemetryInactivityThresholdInSecondsOverride` preference's value",
"parameters": [],
"async": true
}
]
}
]

View File

@ -0,0 +1,58 @@
/* 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/. */
/* global ExtensionAPI, ExtensionCommon, Services */
"use strict";
this.telemetryEnvironment = class extends ExtensionAPI {
getAPI(context) {
const { TelemetryController } = ChromeUtils.import(
"resource://gre/modules/TelemetryController.jsm",
{},
);
const { TelemetryEnvironment } = ChromeUtils.import(
"resource://gre/modules/TelemetryEnvironment.jsm",
{},
);
/**
* These attributes are already sent as part of the telemetry ping envelope
* @returns {{}}
*/
const collectTelemetryEnvironmentBasedAttributes = () => {
const environment = TelemetryEnvironment.currentEnvironment;
// console.debug("TelemetryEnvironment.currentEnvironment", environment);
return {
systemMemoryMb: environment.system.memoryMB,
systemCpuCount: environment.system.cpu.count,
systemCpuCores: environment.system.cpu.cores,
systemCpuVendor: environment.system.cpu.vendor,
systemCpuFamily: environment.system.cpu.family,
systemCpuModel: environment.system.cpu.model,
systemCpuStepping: environment.system.cpu.stepping,
systemCpuL2cacheKB: environment.system.cpu.l2cacheKB,
systemCpuL3cacheKB: environment.system.cpu.l3cacheKB,
systemCpuSpeedMhz: environment.system.cpu.speedMHz,
systemCpuExtensions: environment.system.cpu.extensions,
};
};
return {
experiments: {
telemetryEnvironment: {
async getTranslationRelevantFxTelemetryMetrics() {
await TelemetryController.promiseInitialized();
const telemetryEnvironmentBasedAttributes = collectTelemetryEnvironmentBasedAttributes();
// console.debug("telemetryEnvironmentBasedAttributes", telemetryEnvironmentBasedAttributes);
return {
...telemetryEnvironmentBasedAttributes,
};
},
},
},
};
}
};

View File

@ -0,0 +1,15 @@
[
{
"namespace": "experiments.telemetryEnvironment",
"description": "Enables read-only access to the translation-relevant info from the current telemetry environment",
"functions": [
{
"name": "getTranslationRelevantFxTelemetryMetrics",
"type": "function",
"description": "Get the translation-relevant info from the current telemetry environment",
"parameters": [],
"async": true
}
]
}
]

View File

@ -0,0 +1,57 @@
/* 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/. */
/* global ExtensionAPI, ExtensionCommon, Services */
"use strict";
this.telemetryPreferences = class extends ExtensionAPI {
getAPI(context) {
const EventManager = ExtensionCommon.EventManager;
const uploadEnabledPrefName = `datareporting.healthreport.uploadEnabled`;
const cachedClientIDPrefName = `toolkit.telemetry.cachedClientID`;
return {
experiments: {
telemetryPreferences: {
onUploadEnabledPrefChange: new EventManager({
context,
name: "telemetryPreferences.onUploadEnabledPrefChange",
register: fire => {
const callback = () => {
fire.async().catch(() => {}); // ignore Message Manager disconnects
};
Services.prefs.addObserver(uploadEnabledPrefName, callback);
return () => {
Services.prefs.removeObserver(uploadEnabledPrefName, callback);
};
},
}).api(),
async getUploadEnabledPref() {
return Services.prefs.getBoolPref(uploadEnabledPrefName, undefined);
},
onCachedClientIDPrefChange: new EventManager({
context,
name: "telemetryPreferences.onCachedClientIDPrefChange",
register: fire => {
const callback = () => {
fire.async().catch(() => {}); // ignore Message Manager disconnects
};
Services.prefs.addObserver(cachedClientIDPrefName, callback);
return () => {
Services.prefs.removeObserver(cachedClientIDPrefName, callback);
};
},
}).api(),
async getCachedClientIDPref() {
return Services.prefs.getStringPref(
cachedClientIDPrefName,
undefined,
);
},
},
},
};
}
};

View File

@ -0,0 +1,34 @@
[
{
"namespace": "experiments.telemetryPreferences",
"description": "Enables read-only access to specific Telemetry-related about:config preferences",
"events": [
{
"name": "onUploadEnabledPrefChange",
"type": "function",
"parameters": []
},
{
"name": "onCachedClientIDPrefChange",
"type": "function",
"parameters": []
}
],
"functions": [
{
"name": "getUploadEnabledPref",
"type": "function",
"description": "Get the uploadEnabled preference's value",
"parameters": [],
"async": true
},
{
"name": "getCachedClientIDPref",
"type": "function",
"description": "Get the cachedClientID preference's value",
"parameters": [],
"async": true
}
]
}
]

View File

@ -0,0 +1,322 @@
/* 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/. */
/* global TranslationBrowserChromeUiNotificationManager */
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(TranslationBrowserChromeUi)" }]*/
const { setTimeout, clearTimeout } = ChromeUtils.import(
"resource://gre/modules/Timer.jsm",
{},
);
const TranslationInfoBarStates = {
STATE_OFFER: 0,
STATE_TRANSLATING: 1,
STATE_TRANSLATED: 2,
STATE_ERROR: 3,
STATE_UNAVAILABLE: 4,
};
class TranslationBrowserChromeUi {
constructor(Services, browser, context, apiEventEmitter, tabId) {
this.Services = Services;
this.uiState = null;
this.browser = browser;
this.context = context;
this.translationInfoBarShown = false;
this.shouldShowTranslationProgressTimer = undefined;
this.importTranslationNotification();
// The manager instance is injected into the translation notification bar and handles events from therein
this.translationBrowserChromeUiNotificationManager = new TranslationBrowserChromeUiNotificationManager(
browser,
apiEventEmitter,
tabId,
TranslationInfoBarStates,
);
}
get notificationBox() {
return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
}
importTranslationNotification() {
const chromeWin = this.browser.ownerGlobal;
// As a workaround to be able to load updates for the translation notification on extension reload
// we use the current unix timestamp as part of the element id.
// TODO: Restrict use of Date.now() as cachebuster to development mode only
chromeWin.now = Date.now();
try {
chromeWin.customElements.setElementCreationCallback(
`translation-notification-${chromeWin.now}`,
() => {
this.Services.scriptloader.loadSubScript(
this.context.extension.getURL(
"experiment-apis/translateUi/content/translation-notification.js",
) +
"?cachebuster=" +
chromeWin.now,
chromeWin,
);
},
);
} catch (e) {
console.log(
"Error occurred when attempting to load the translation notification script, but we continue nevertheless",
e,
);
}
}
onUiStateUpdate(uiState) {
// Set all values before showing a new translation infobar.
this.translationBrowserChromeUiNotificationManager.uiState = uiState;
this.setInfobarState(uiState.infobarState);
this.updateTranslationProgress(uiState);
if (this.shouldShowInfoBar(this.browser.contentPrincipal)) {
this.showTranslationInfoBarIfNotAlreadyShown();
} else {
this.hideTranslationInfoBarIfShown();
}
}
/**
* Syncs infobarState with the inner infobar state variable of the infobar
* @param val
*/
setInfobarState(val) {
const notif = this.notificationBox.getNotificationWithValue("translation");
if (notif) {
notif.state = val;
}
}
/**
* Informs the infobar element of the current translation progress
*/
updateTranslationProgress(uiState) {
// Don't bother updating translation progress if not currently translating
if (
this.translationBrowserChromeUiNotificationManager.uiState
.infobarState !== TranslationInfoBarStates.STATE_TRANSLATING
) {
return;
}
const notif = this.notificationBox.getNotificationWithValue("translation");
if (notif) {
const {
modelDownloading,
translationDurationMs,
localizedTranslationProgressText,
} = uiState;
// Always cancel ongoing timers so that we start from a clean state
if (this.shouldShowTranslationProgressTimer) {
clearTimeout(this.shouldShowTranslationProgressTimer);
}
// Only show progress if translation has been going on for at least 3 seconds
// or we are currently downloading a model
let shouldShowTranslationProgress;
const thresholdMsAfterWhichToShouldTranslationProgress = 3000;
if (
translationDurationMs >=
thresholdMsAfterWhichToShouldTranslationProgress ||
modelDownloading
) {
shouldShowTranslationProgress = true;
} else {
// Use a timer to show the translation progress after the threshold
this.shouldShowTranslationProgressTimer = setTimeout(() => {
notif.updateTranslationProgress(
true,
localizedTranslationProgressText,
);
clearTimeout(this.shouldShowTranslationProgressTimer);
}, thresholdMsAfterWhichToShouldTranslationProgress - translationDurationMs);
// Don't show until then
shouldShowTranslationProgress = false;
}
notif.updateTranslationProgress(
shouldShowTranslationProgress,
localizedTranslationProgressText,
);
}
}
shouldShowInfoBar(principal) {
if (
![
TranslationInfoBarStates.STATE_OFFER,
TranslationInfoBarStates.STATE_TRANSLATING,
TranslationInfoBarStates.STATE_TRANSLATED,
TranslationInfoBarStates.STATE_ERROR,
].includes(
this.translationBrowserChromeUiNotificationManager.uiState.infobarState,
)
) {
return false;
}
// Don't show the infobar if we have no language detection results yet
if (
!this.translationBrowserChromeUiNotificationManager.uiState
.detectedLanguageResults
) {
return false;
}
// Don't show the infobar if we couldn't confidently detect the language
if (
!this.translationBrowserChromeUiNotificationManager.uiState
.detectedLanguageResults.confident
) {
return false;
}
// Check if we should never show the infobar for this language.
const neverForLangs = this.Services.prefs.getCharPref(
"browser.translation.neverForLanguages",
);
if (
neverForLangs
.split(",")
.includes(
this.translationBrowserChromeUiNotificationManager.uiState
.detectedLanguageResults.language,
)
) {
// TranslationTelemetry.recordAutoRejectedTranslationOffer();
return false;
}
// or if we should never show the infobar for this domain.
const perms = this.Services.perms;
if (
perms.testExactPermissionFromPrincipal(principal, "translate") ===
perms.DENY_ACTION
) {
// TranslationTelemetry.recordAutoRejectedTranslationOffer();
return false;
}
return true;
}
hideURLBarIcon() {
const chromeWin = this.browser.ownerGlobal;
const PopupNotifications = chromeWin.PopupNotifications;
const removeId = this.translationBrowserChromeUiNotificationManager.uiState
.originalShown
? "translated"
: "translate";
const notification = PopupNotifications.getNotification(
removeId,
this.browser,
);
if (notification) {
PopupNotifications.remove(notification);
}
}
showURLBarIcon() {
const chromeWin = this.browser.ownerGlobal;
const PopupNotifications = chromeWin.PopupNotifications;
const removeId = this.translationBrowserChromeUiNotificationManager.uiState
.originalShown
? "translated"
: "translate";
const notification = PopupNotifications.getNotification(
removeId,
this.browser,
);
if (notification) {
PopupNotifications.remove(notification);
}
const callback = (topic /* , aNewBrowser */) => {
if (topic === "swapping") {
const infoBarVisible = this.notificationBox.getNotificationWithValue(
"translation",
);
if (infoBarVisible) {
this.showTranslationInfoBar();
}
return true;
}
if (topic !== "showing") {
return false;
}
const translationNotification = this.notificationBox.getNotificationWithValue(
"translation",
);
if (translationNotification) {
translationNotification.close();
} else {
this.showTranslationInfoBar();
}
return true;
};
const addId = this.translationBrowserChromeUiNotificationManager.uiState
.originalShown
? "translate"
: "translated";
PopupNotifications.show(
this.browser,
addId,
null,
addId + "-notification-icon",
null,
null,
{ dismissed: true, eventCallback: callback },
);
}
showTranslationInfoBarIfNotAlreadyShown() {
const translationNotification = this.notificationBox.getNotificationWithValue(
"translation",
);
if (!translationNotification && !this.translationInfoBarShown) {
this.showTranslationInfoBar();
}
this.showURLBarIcon();
}
hideTranslationInfoBarIfShown() {
const translationNotification = this.notificationBox.getNotificationWithValue(
"translation",
);
if (translationNotification) {
translationNotification.close();
}
this.hideURLBarIcon();
this.translationInfoBarShown = false;
}
showTranslationInfoBar() {
console.debug("showTranslationInfoBar");
this.translationInfoBarShown = true;
const notificationBox = this.notificationBox;
const chromeWin = this.browser.ownerGlobal;
const notif = notificationBox.appendNotification(
"",
"translation",
null,
notificationBox.PRIORITY_INFO_HIGH,
null,
null,
`translation-notification-${chromeWin.now}`,
);
notif.init(this.translationBrowserChromeUiNotificationManager);
this.translationBrowserChromeUiNotificationManager.infobarDisplayed(
notif._getSourceLang(),
notif._getTargetLang(),
);
return notif;
}
}

View File

@ -0,0 +1,80 @@
/* 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/. */
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(TranslationBrowserChromeUiNotificationManager)" }]*/
class TranslationBrowserChromeUiNotificationManager {
constructor(browser, apiEventEmitter, tabId, TranslationInfoBarStates) {
this.uiState = null;
this.TranslationInfoBarStates = TranslationInfoBarStates;
this.apiEventEmitter = apiEventEmitter;
this.tabId = tabId;
this.browser = browser;
}
infobarDisplayed(from, to) {
console.log("infobarDisplayed", { from, to });
this.apiEventEmitter.emit("onInfoBarDisplayed", this.tabId, from, to);
}
toLanguageChanged(from, newTo) {
console.log("toLanguageChanged", { from, newTo });
this.apiEventEmitter.emit("onSelectTranslateTo", this.tabId, from, newTo);
}
fromLanguageChanged(newFrom, to) {
console.log("fromLanguageChanged", { newFrom, to });
this.apiEventEmitter.emit("onSelectTranslateFrom", this.tabId, newFrom, to);
}
infobarClosed(from, to) {
console.log("infobarClosed", { from, to });
this.apiEventEmitter.emit("onInfoBarClosed", this.tabId, from, to);
}
neverForLanguage(from, to) {
console.log("neverForLanguage", { from, to });
this.apiEventEmitter.emit(
"onNeverTranslateSelectedLanguage",
this.tabId,
from,
to,
);
}
neverForSite(from, to) {
console.log("neverForSite", { from, to });
this.apiEventEmitter.emit("onNeverTranslateThisSite", this.tabId, from, to);
}
showOriginalContent(from, to) {
console.log("showOriginalContent", { from, to });
this.apiEventEmitter.emit(
"onShowOriginalButtonPressed",
this.tabId,
from,
to,
);
}
showTranslatedContent(from, to) {
console.log("showTranslatedContent", { from, to });
this.apiEventEmitter.emit(
"onShowTranslatedButtonPressed",
this.tabId,
from,
to,
);
}
translate(from, to) {
console.log("translate", { from, to });
this.apiEventEmitter.emit("onTranslateButtonPressed", this.tabId, from, to);
}
notNow(from, to) {
console.log("notNow", { from, to });
this.apiEventEmitter.emit("onNotNowButtonPressed", this.tabId, from, to);
}
}

View File

@ -0,0 +1,181 @@
/* 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/. */
/* eslint-env commonjs */
/* eslint no-unused-vars: off */
/* eslint no-inner-declarations: off */
/* eslint no-console: ["warn", { allow: ["info", "warn", "error"] }] */
/* global ExtensionAPI */
"use strict";
this.translateUi = class extends ExtensionAPI {
getAPI(context) {
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm",
{},
);
const now = Date.now();
/* global TranslationBrowserChromeUiManager */
Services.scriptloader.loadSubScript(
context.extension.getURL(
"experiment-apis/translateUi/TranslationBrowserChromeUiManager.js",
) +
"?cachebuster=" +
now,
);
/* global TranslationBrowserChromeUi */
Services.scriptloader.loadSubScript(
context.extension.getURL(
"experiment-apis/translateUi/TranslationBrowserChromeUi.js",
) +
"?cachebuster=" +
now,
);
const { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm",
{},
);
const { EventManager, EventEmitter } = ExtensionCommon;
const apiEventEmitter = new EventEmitter();
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm",
{},
);
const { ExtensionError } = ExtensionUtils;
/**
* Boilerplate-reducing factory method translating between
* apiEventEmitter.emit("translateUi.onFoo", ...args)
* and the actual web extension event being emitted
*
* @param {string} eventRef the event reference, eg "onFoo"
* @returns {void}
*/
const eventManagerFactory = eventRef => {
const eventId = `translateUi.${eventRef}`;
return new EventManager({
context,
name: eventId,
register: fire => {
const listener = (event, ...args) => fire.async(...args);
apiEventEmitter.on(eventRef, listener);
return () => {
apiEventEmitter.off(eventRef, listener);
};
},
});
};
const { tabManager } = context.extension;
const translationBrowserChromeUiInstancesByTabId = new Map();
const getTranslationBrowserChromeUiInstanceByTabId = tabId => {
if (translationBrowserChromeUiInstancesByTabId.has(tabId)) {
return translationBrowserChromeUiInstancesByTabId.get(tabId);
}
const tab = tabManager.get(tabId);
const { browser } = tab;
const translationBrowserChromeUi = new TranslationBrowserChromeUi(
Services,
browser,
context,
apiEventEmitter,
tabId,
);
translationBrowserChromeUiInstancesByTabId.set(
tabId,
translationBrowserChromeUi,
);
return translationBrowserChromeUi;
};
return {
experiments: {
translateUi: {
/* Start reacting to translation state updates */
start: async function start() {
try {
console.log("Called start()");
console.log(
"Inactivating legacy built-in translation feature (by setting browser.translation.ui.show and browser.translation.detectLanguage to false)",
);
Services.prefs.setBoolPref(`browser.translation.ui.show`, false);
Services.prefs.setBoolPref(
`browser.translation.detectLanguage`,
false,
);
return undefined;
} catch (error) {
// Surface otherwise silent or obscurely reported errors
console.error(error.message, error.stack);
throw new ExtensionError(error.message);
}
},
/* Set current ui state */
setUiState: async function setUiState(tabId, uiState) {
try {
// console.log("Called setUiState(tabId, uiState)", {tabId,uiState});
const translationBrowserChromeUi = getTranslationBrowserChromeUiInstanceByTabId(
tabId,
);
translationBrowserChromeUi.onUiStateUpdate(uiState);
return undefined;
} catch (error) {
// Surface otherwise silent or obscurely reported errors
console.error(error.message, error.stack);
throw new ExtensionError(error.message);
}
},
/* Stop reacting to translation state updates */
stop: async function stop() {
try {
console.log("Called stop()");
return undefined;
} catch (error) {
// Surface otherwise silent or obscurely reported errors
console.error(error.message, error.stack);
throw new ExtensionError(error.message);
}
},
/* Event boilerplate with listeners that forwards all but the first argument to the web extension event */
onInfoBarDisplayed: eventManagerFactory("onInfoBarDisplayed").api(),
onSelectTranslateTo: eventManagerFactory("onSelectTranslateTo").api(),
onSelectTranslateFrom: eventManagerFactory(
"onSelectTranslateFrom",
).api(),
onInfoBarClosed: eventManagerFactory("onInfoBarClosed").api(),
onNeverTranslateSelectedLanguage: eventManagerFactory(
"onNeverTranslateSelectedLanguage",
).api(),
onNeverTranslateThisSite: eventManagerFactory(
"onNeverTranslateThisSite",
).api(),
onShowOriginalButtonPressed: eventManagerFactory(
"onShowOriginalButtonPressed",
).api(),
onShowTranslatedButtonPressed: eventManagerFactory(
"onShowTranslatedButtonPressed",
).api(),
onTranslateButtonPressed: eventManagerFactory(
"onTranslateButtonPressed",
).api(),
onNotNowButtonPressed: eventManagerFactory(
"onNotNowButtonPressed",
).api(),
},
},
};
}
};

View File

@ -0,0 +1,449 @@
/* 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/. */
/* global MozElements */
"use strict";
window.MozTranslationNotification = class extends MozElements.Notification {
static get markup() {
return `
<hbox anonid="details" align="center" flex="1">
<image class="messageImage"/>
<panel anonid="welcomePanel" class="translation-welcome-panel" type="arrow" align="start">
<image class="translation-welcome-logo"/>
<vbox flex="1" class="translation-welcome-content">
<description class="translation-welcome-headline" anonid="welcomeHeadline"/>
<description class="translation-welcome-body" anonid="welcomeBody"/>
<hbox align="center">
<label anonid="learnMore" class="plain" onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();" is="text-link"/>
<spacer flex="1"/>
<button anonid="thanksButton" onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
</hbox>
</vbox>
</panel>
<deck anonid="translationStates" selectedIndex="0">
<hbox class="translate-offer-box" align="center">
<label value="&translation.thisPageIsIn.label;"/>
<menulist class="notification-button" anonid="detectedLanguage">
<menupopup/>
</menulist>
<label value="&translation.translateThisPage.label;"/>
<button class="notification-button primary" label="&translation.translate.button;" anonid="translate" oncommand="this.closest('notification').translate();"/>
<button class="notification-button" label="&translation.notNow.button;" anonid="notNow" oncommand="this.closest('notification').notNow();"/>
</hbox>
<vbox class="translating-box" pack="center">
<hbox><label value="&translation.translatingContent.label;"/><label anonid="progress-label" value=""/></hbox>
</vbox>
<hbox class="translated-box" align="center">
<label value="&translation.translatedFrom.label;"/>
<menulist class="notification-button" anonid="fromLanguage" oncommand="this.closest('notification').fromLanguageChanged();">
<menupopup/>
</menulist>
<label value="&translation.translatedTo.label;"/>
<menulist class="notification-button" anonid="toLanguage" oncommand="this.closest('notification').toLanguageChanged();">
<menupopup/>
</menulist>
<label value="&translation.translatedToSuffix.label;"/>
<button anonid="showOriginal" class="notification-button" label="&translation.showOriginal.button;" oncommand="this.closest('notification').showOriginal();"/>
<button anonid="showTranslation" class="notification-button" label="&translation.showTranslation.button;" oncommand="this.closest('notification').showTranslation();"/>
</hbox>
<hbox class="translation-error" align="center">
<label value="&translation.errorTranslating.label;"/>
<button class="notification-button" label="&translation.tryAgain.button;" anonid="tryAgain" oncommand="this.closest('notification').translate();"/>
</hbox>
<vbox class="translation-unavailable" pack="center">
<label value="&translation.serviceUnavailable.label;"/>
</vbox>
</deck>
<spacer flex="1"/>
<button type="menu" class="notification-button" anonid="options" label="&translation.options.menu;">
<menupopup class="translation-menupopup" onpopupshowing="this.closest('notification').optionsShowing();">
<menuitem anonid="neverForLanguage" oncommand="this.closest('notification').neverForLanguage();"/>
<menuitem anonid="neverForSite" oncommand="this.closest('notification').neverForSite();" label="&translation.options.neverForSite.label;" accesskey="&translation.options.neverForSite.accesskey;"/>
<menuseparator/>
<menuitem oncommand="openPreferences('paneGeneral');" label="&translation.options.preferences.label;" accesskey="&translation.options.preferences.accesskey;"/>
</menupopup>
</button>
</hbox>
<toolbarbutton anonid="closeButton" ondblclick="event.stopPropagation();"
class="messageCloseButton close-icon tabbable"
tooltiptext="&closeNotification.tooltip;"
oncommand="this.parentNode.closeCommand();"/>
`;
}
static get entities() {
return [
"chrome://global/locale/notification.dtd",
"chrome://browser/locale/translation.dtd",
];
}
connectedCallback() {
this.appendChild(this.constructor.fragment);
for (const [propertyName, selector] of [
["details", "[anonid=details]"],
["messageImage", ".messageImage"],
["spacer", "[anonid=spacer]"],
]) {
this[propertyName] = this.querySelector(selector);
}
}
async updateTranslationProgress(
shouldShowTranslationProgress,
localizedTranslationProgressText,
) {
const progressLabelValue = shouldShowTranslationProgress
? localizedTranslationProgressText
: "";
this._getAnonElt("progress-label").setAttribute(
"value",
progressLabelValue,
);
}
set state(val) {
const deck = this._getAnonElt("translationStates");
const activeElt = document.activeElement;
if (activeElt && deck.contains(activeElt)) {
activeElt.blur();
}
let stateName;
for (const name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
if (Translation["STATE_" + name] === val) {
stateName = name.toLowerCase();
break;
}
}
this.setAttribute("state", stateName);
if (val === this.translation.TranslationInfoBarStates.STATE_TRANSLATED) {
this._handleButtonHiding();
}
deck.selectedIndex = val;
}
get state() {
return this._getAnonElt("translationStates").selectedIndex;
}
init(translationBrowserChromeUiNotificationManager) {
this.translation = translationBrowserChromeUiNotificationManager;
const sortByLocalizedName = function(list) {
const names = Services.intl.getLanguageDisplayNames(undefined, list);
return list
.map((code, i) => [code, names[i]])
.sort((a, b) => a[1].localeCompare(b[1]));
};
// Fill the lists of supported source languages.
const detectedLanguage = this._getAnonElt("detectedLanguage");
const fromLanguage = this._getAnonElt("fromLanguage");
const sourceLanguages = sortByLocalizedName(
this.translation.uiState.supportedSourceLanguages,
);
for (const [code, name] of sourceLanguages) {
detectedLanguage.appendItem(name, code);
fromLanguage.appendItem(name, code);
}
detectedLanguage.value = this.translation.uiState.detectedLanguageResults.language;
// translatedFrom is only set if we have already translated this page.
if (translationBrowserChromeUiNotificationManager.uiState.translatedFrom) {
fromLanguage.value =
translationBrowserChromeUiNotificationManager.uiState.translatedFrom;
}
// Fill the list of supported target languages.
const toLanguage = this._getAnonElt("toLanguage");
const targetLanguages = sortByLocalizedName(
this.translation.uiState.supportedTargetLanguages,
);
for (const [code, name] of targetLanguages) {
toLanguage.appendItem(name, code);
}
if (translationBrowserChromeUiNotificationManager.uiState.translatedTo) {
toLanguage.value =
translationBrowserChromeUiNotificationManager.uiState.translatedTo;
}
if (translationBrowserChromeUiNotificationManager.uiState.infobarState) {
this.state =
translationBrowserChromeUiNotificationManager.uiState.infobarState;
}
/*
// The welcome popup/notification is currently disabled
const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
if (
Services.prefs.prefHasUserValue(kWelcomePref) ||
this.translation.browser !== gBrowser.selectedBrowser
) {
return;
}
this.addEventListener(
"transitionend",
function() {
// These strings are hardcoded because they need to reach beta
// without riding the trains.
const 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 = Services.locale.appLocaleAsBCP47;
if (!(locale in localizedStrings)) {
locale = "en";
}
const 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]);
// TODO: Figure out why this shows a strangely rendered popup at the corner of the window instead next to the URL bar
const panel = this._getAnonElt("welcomePanel");
panel.openPopup(
this._getAnonElt("messageImage"),
"bottomcenter topleft",
);
Services.prefs.setBoolPref(kWelcomePref, true);
},
{ once: true },
);
*/
}
_getAnonElt(anonId) {
return this.querySelector("[anonid=" + anonId + "]");
}
fromLanguageChanged() {
this.translation.fromLanguageChanged(
this._getSourceLang(),
this._getTargetLang(),
);
this.translate();
}
toLanguageChanged() {
this.translation.toLanguageChanged(
this._getSourceLang(),
this._getTargetLang(),
);
this.translate();
}
translate() {
const from = this._getSourceLang();
const to = this._getTargetLang();
// Initiate translation
this.translation.translate(from, to);
// Store the values used in the translation in the from and to inputs
if (
this.translation.uiState.infobarState ===
this.translation.TranslationInfoBarStates.STATE_OFFER
) {
this._getAnonElt("fromLanguage").value = from;
this._getAnonElt("toLanguage").value = to;
}
}
/**
* To be called when the infobar should be closed per user's wish (e.g.
* by clicking the notification's close button, the not now button or choosing never to translate)
*/
closeCommand() {
const from = this._getSourceLang();
const to = this._getTargetLang();
this.close();
this.translation.infobarClosed(from, to);
}
/**
* To be called when the infobar should be closed per user's wish
* by clicking the Not now button
*/
notNow() {
this.translation.notNow(this._getSourceLang(), this._getTargetLang());
this.closeCommand();
}
_handleButtonHiding() {
const originalShown = this.translation.uiState.originalShown;
this._getAnonElt("showOriginal").hidden = originalShown;
this._getAnonElt("showTranslation").hidden = !originalShown;
}
showOriginal() {
this.translation.showOriginalContent(
this._getSourceLang(),
this._getTargetLang(),
);
this._handleButtonHiding();
}
showTranslation() {
this.translation.showTranslatedContent(
this._getSourceLang(),
this._getTargetLang(),
);
this._handleButtonHiding();
}
_getSourceLang() {
let lang;
if (
this.translation.uiState.infobarState ===
this.translation.TranslationInfoBarStates.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.translation.uiState.infobarState ===
this.translation.TranslationInfoBarStates.STATE_UNAVAILABLE
) {
lang = this.translation.uiState.defaultTargetLanguage;
}
}
if (!lang) {
throw new Error("Source language is not defined");
}
return lang;
}
_getTargetLang() {
return (
this._getAnonElt("toLanguage").value ||
this.translation.uiState.defaultTargetLanguage
);
}
optionsShowing() {
const lang = this._getSourceLang();
// Get the source language name.
const langName = Services.intl.getLanguageDisplayNames(undefined, [
lang,
])[0];
// Set the label and accesskey on the menuitem.
const bundle = Services.strings.createBundle(
"chrome://browser/locale/translation.properties",
);
let item = this._getAnonElt("neverForLanguage");
const kStrId = "translation.options.neverForLanguage";
item.setAttribute(
"label",
bundle.formatStringFromName(kStrId + ".label", [langName]),
);
item.setAttribute(
"accesskey",
bundle.GetStringFromName(kStrId + ".accesskey"),
);
// We may need to disable the menuitems if they have already been used.
// Check if translation is already disabled for this language:
const neverForLangs = Services.prefs.getCharPref(
"browser.translation.neverForLanguages",
);
item.disabled = neverForLangs.split(",").includes(lang);
// Check if translation is disabled for the domain:
const principal = this.translation.browser.contentPrincipal;
const perms = Services.perms;
item = this._getAnonElt("neverForSite");
item.disabled =
perms.testExactPermissionFromPrincipal(principal, "translate") ===
perms.DENY_ACTION;
}
neverForLanguage() {
const kPrefName = "browser.translation.neverForLanguages";
const sourceLang = this._getSourceLang();
let val = Services.prefs.getCharPref(kPrefName);
if (val) {
val += ",";
}
val += sourceLang;
Services.prefs.setCharPref(kPrefName, val);
this.translation.neverForLanguage(
this._getSourceLang(),
this._getTargetLang(),
);
this.closeCommand();
}
neverForSite() {
const principal = this.translation.browser.contentPrincipal;
const perms = Services.perms;
perms.addFromPrincipal(principal, "translate", perms.DENY_ACTION);
this.translation.neverForSite(this._getSourceLang(), this._getTargetLang());
this.closeCommand();
}
};
customElements.define(
`translation-notification-${window.now}`,
window.MozTranslationNotification,
{
extends: "notification",
},
);

View File

@ -0,0 +1,100 @@
[
{
"namespace": "experiments.translateUi",
"description": "Provides browser chrome UI that reacts to translation states and fires events on user interaction",
"functions": [
{
"name": "start",
"type": "function",
"async": true,
"description": "Start reacting to translation state updates",
"parameters": []
},
{
"name": "setUiState",
"type": "function",
"async": true,
"description": "Set current ui state",
"parameters": [
{
"name": "tabId",
"type": "number"
},
{
"name": "uiState",
"type": "any"
}
]
},
{
"name": "stop",
"type": "function",
"async": true,
"description": "Stop reacting to translation state updates",
"parameters": []
}
],
"events": [
{
"name": "onInfoBarDisplayed",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onSelectTranslateTo",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onSelectTranslateFrom",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onInfoBarClosed",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onNeverTranslateSelectedLanguage",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onNeverTranslateThisSite",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onShowOriginalButtonPressed",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onShowTranslatedButtonPressed",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onTranslateButtonPressed",
"type": "function",
"description": "Foo",
"parameters": []
},
{
"name": "onNotNowButtonPressed",
"type": "function",
"description": "Foo",
"parameters": []
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,115 @@
{
"manifest_version": 2,
"name": "Firefox Translations",
"description": "__MSG_extensionDescription__",
"version": "0.4.0",
"incognito": "spanning",
"default_locale": "en_US",
"background": {
"scripts": [
"commons.js",
"background.js"
]
},
"content_scripts": [
{
"js": [
"commons.js"
],
"matches": [
"<all_urls>"
],
"all_frames": true,
"run_at": "document_idle",
"match_about_blank": false
},
{
"js": [
"dom-translation-content-script.js"
],
"matches": [
"<all_urls>"
],
"all_frames": true,
"run_at": "document_idle",
"match_about_blank": false
}
],
"permissions": [
"<all_urls>",
"storage"
],
"icons": {
"16": "icons/translation.16x16.png",
"32": "icons/translation.32x32.png"
},
"hidden": false,
"experiment_apis": {
"extensionPreferences": {
"schema": "./experiment-apis/extensionPreferences/schema.json",
"parent": {
"scopes": [
"addon_parent"
],
"script": "./experiment-apis/extensionPreferences/api.js",
"paths": [
[
"experiments",
"extensionPreferences"
]
]
}
},
"telemetryEnvironment": {
"schema": "./experiment-apis/telemetryEnvironment/schema.json",
"parent": {
"scopes": [
"addon_parent"
],
"script": "./experiment-apis/telemetryEnvironment/api.js",
"paths": [
[
"experiments",
"telemetryEnvironment"
]
]
}
},
"telemetryPreferences": {
"schema": "./experiment-apis/telemetryPreferences/schema.json",
"parent": {
"scopes": [
"addon_parent"
],
"script": "./experiment-apis/telemetryPreferences/api.js",
"paths": [
[
"experiments",
"telemetryPreferences"
]
]
}
},
"translateUi": {
"schema": "./experiment-apis/translateUi/schema.json",
"parent": {
"scopes": [
"addon_parent"
],
"script": "./experiment-apis/translateUi/api.js",
"paths": [
[
"experiments",
"translateUi"
]
]
}
}
},
"applications": {
"gecko": {
"id": "firefox-translations@mozilla.org",
"strict_min_version": "90.0a1"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,107 @@
# script to pull and import Firefox Translations's extension source code
import os.path
from zipfile import ZipFile
import subprocess
import shutil
import sys
if not os.path.exists("import_xpi.py"):
sys.exit("This script is intended to be executed from its local folder")
have_xpi = "N"
local_xpi_file = (
"bergamot-browser-extension-src/dist/production/firefox/"
"firefox-infobar-ui/firefox-translations-0.4.0.xpi"
)
if os.path.isfile(local_xpi_file):
have_xpi = input(
"Extension xpi exists. Press Y to use it or any other key to rebuild it."
)
if have_xpi.lower() != "y":
# deleting old files if any
shutil.rmtree("bergamot-browser-extension-src", ignore_errors=True)
# cloning the extension
subprocess.call(
(
"git clone -b v0.4.0 "
"https://github.com/mozilla-extensions/bergamot-browser-extension/ "
"bergamot-browser-extension-src "
).split()
)
# setting up the repo
subprocess.call("yarn install".split(), cwd="bergamot-browser-extension-src")
# pulling bergamot-translator submodule, the repo containing the port of the
# neural machine translation engine to wasm
subprocess.call(
"git submodule update --init --recursive".split(),
cwd="bergamot-browser-extension-src",
)
# build the wasm nmt module
subprocess.call(
"./bergamot-translator/build-wasm.sh".split(),
cwd="bergamot-browser-extension-src",
)
# import the generated wasm module to the extension
subprocess.call(
"./import-bergamot-translator.sh ./bergamot-translator/build-wasm/".split(),
cwd="bergamot-browser-extension-src",
)
# build the final xpi
subprocess.call(
"yarn build:firefox-infobar-ui".split(), cwd="bergamot-browser-extension-src"
)
shutil.rmtree("extension", ignore_errors=True)
os.mkdir("extension")
file_exceptions = [
"META-INF",
".md",
"BRANCH",
"COMMITHASH",
"LASTCOMMITDATETIME",
"VERSION",
".map",
".yaml",
]
fo = open("jar.mn", "w")
fo.write(
"##### This file was automatically generated by the import_xpi.py script ####\n"
)
fo.write("# This Source Code Form is subject to the terms of the Mozilla Public\n")
fo.write("# License, v. 2.0. If a copy of the MPL was not distributed with this\n")
fo.write("# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\n")
fo.write("browser.jar:\n")
fo.write("% resource builtin-addons %builtin-addons/ contentaccessible=yes\n")
fo.write(" builtin-addons/translations/ (extension/**)")
fo.write("\n")
fo.close()
def isValidFile(filename):
for exception in file_exceptions:
if exception in filename:
return False
return True
file_set = set()
# read xpi files
with ZipFile(local_xpi_file, "r") as zip:
namelist = zip.namelist()
cleared_namelist = []
for filename in namelist:
if isValidFile(filename):
full_file_path = zip.extract(filename, "extension")
if filename.endswith(".js"):
filename = "browser/extensions/translations/{}".format(full_file_path)
subprocess.call(
str(
"./mach lint --linter license {} --fix".format(filename)
).split(),
cwd="../../../",
)
print("Import finalized successfully")

View File

@ -0,0 +1,8 @@
##### This file was automatically generated by the import_xpi.py script ####
# 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:
% resource builtin-addons %builtin-addons/ contentaccessible=yes
builtin-addons/translations/ (extension/**)

View File

@ -0,0 +1,4 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Translations")
JAR_MANIFESTS += ["jar.mn"]

View File

@ -174,4 +174,9 @@ browser/chrome/browser/skin/classic/browser/zoom-in.svg
# Bug 1709445 - De-duplicate history icons
browser/chrome/browser/skin/classic/browser/history.svg
browser/chrome/browser/skin/classic/browser/places/history.svg
browser/chrome/browser/skin/classic/browser/places/history.svg
# Bug 1710546 - Bundle Firefox extension as a builtin addon Nighly only
# We plan to remove this duplicity after Firefox Translations become pref'd on
browser/chrome/browser/builtin-addons/translations/wasm/cld-worker.js.mem
browser/modules/translation/cld-worker.js.mem