mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
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:
parent
1ee8086c02
commit
760b23419b
@ -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/
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -12,3 +12,8 @@ DIRS += [
|
||||
"report-site-issue",
|
||||
"pictureinpicture",
|
||||
]
|
||||
|
||||
if CONFIG["NIGHTLY_BUILD"]:
|
||||
DIRS += [
|
||||
"translations",
|
||||
]
|
||||
|
20
browser/extensions/translations/README
Normal file
20
browser/extensions/translations/README
Normal 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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10665
browser/extensions/translations/extension/background.js
Normal file
10665
browser/extensions/translations/extension/background.js
Normal file
File diff suppressed because it is too large
Load Diff
26473
browser/extensions/translations/extension/commons.js
Normal file
26473
browser/extensions/translations/extension/commons.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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",
|
||||
},
|
||||
);
|
@ -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 |
115
browser/extensions/translations/extension/manifest.json
Normal file
115
browser/extensions/translations/extension/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
8574
browser/extensions/translations/extension/translation-worker.js
Normal file
8574
browser/extensions/translations/extension/translation-worker.js
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
90
browser/extensions/translations/extension/wasm/cld-worker.js
Normal file
90
browser/extensions/translations/extension/wasm/cld-worker.js
Normal file
File diff suppressed because one or more lines are too long
BIN
browser/extensions/translations/extension/wasm/cld-worker.js.mem
Normal file
BIN
browser/extensions/translations/extension/wasm/cld-worker.js.mem
Normal file
Binary file not shown.
107
browser/extensions/translations/import_xpi.py
Normal file
107
browser/extensions/translations/import_xpi.py
Normal 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")
|
8
browser/extensions/translations/jar.mn
Normal file
8
browser/extensions/translations/jar.mn
Normal 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/**)
|
4
browser/extensions/translations/moz.build
Normal file
4
browser/extensions/translations/moz.build
Normal file
@ -0,0 +1,4 @@
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Firefox", "Translations")
|
||||
|
||||
JAR_MANIFESTS += ["jar.mn"]
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user