fune/browser/extensions/translations/extension/dom-translation-content-script.js
Andre Natal cfd82965b8 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
2021-05-26 21:25:50 +00:00

2750 lines
No EOL
132 KiB
JavaScript

/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 9023:
/*!************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/DomTranslationManager.ts ***!
\************************************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DomTranslationManager = void 0;
const TranslationDocument_1 = __webpack_require__(/*! ./TranslationDocument */ 992);
const BergamotDomTranslator_1 = __webpack_require__(/*! ./dom-translators/BergamotDomTranslator */ 1518);
const getTranslationNodes_1 = __webpack_require__(/*! ./getTranslationNodes */ 5173);
const ContentScriptLanguageDetectorProxy_1 = __webpack_require__(/*! ../../shared-resources/ContentScriptLanguageDetectorProxy */ 6336);
const BaseTranslationState_1 = __webpack_require__(/*! ../../shared-resources/models/BaseTranslationState */ 4779);
const LanguageSupport_1 = __webpack_require__(/*! ../../shared-resources/LanguageSupport */ 5602);
const detagAndProject_1 = __webpack_require__(/*! ./dom-translators/detagAndProject */ 961);
class DomTranslationManager {
constructor(documentTranslationStateCommunicator, document, contentWindow) {
this.documentTranslationStateCommunicator = documentTranslationStateCommunicator;
this.document = document;
this.contentWindow = contentWindow;
this.languageDetector = new ContentScriptLanguageDetectorProxy_1.ContentScriptLanguageDetectorProxy();
}
attemptToDetectLanguage() {
return __awaiter(this, void 0, void 0, function* () {
console.debug("Attempting to detect language");
const url = String(this.document.location);
if (!url.startsWith("http://") && !url.startsWith("https://")) {
console.debug("Not a HTTP(S) url, translation unavailable", { url });
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.UNAVAILABLE);
return;
}
console.debug("Setting status to reflect detection of language ongoing");
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.DETECTING_LANGUAGE);
// Grab a 60k sample of text from the page.
// (The CLD2 library used by the language detector is capable of
// analyzing raw HTML. Unfortunately, that takes much more memory,
// and since it's hosted by emscripten, and therefore can't shrink
// its heap after it's grown, it has a performance cost.
// So we send plain text instead.)
const translationNodes = getTranslationNodes_1.getTranslationNodes(document.body);
const domElementsToStringWithMaxLength = (elements, maxLength) => {
return elements
.map(el => el.textContent)
.join("\n")
.substr(0, maxLength);
};
const string = domElementsToStringWithMaxLength(translationNodes.map(tn => tn.content), 60 * 1024);
// Language detection isn't reliable on very short strings.
if (string.length < 100) {
console.debug("Language detection isn't reliable on very short strings. Skipping language detection", { string });
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.LANGUAGE_NOT_DETECTED);
return;
}
const detectedLanguageResults = yield this.languageDetector.detectLanguage(string);
console.debug("Language detection results are in", {
detectedLanguageResults,
});
// The window might be gone by now.
if (!this.contentWindow) {
console.info("Content window reference invalid, deleting document translation state");
this.documentTranslationStateCommunicator.clear();
return;
}
// Save results in extension state
this.documentTranslationStateCommunicator.updatedDetectedLanguageResults(detectedLanguageResults);
if (!detectedLanguageResults.confident) {
console.debug("Language detection results not confident enough, bailing.");
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.LANGUAGE_NOT_DETECTED);
return;
}
console.debug("Updating state to reflect that language has been detected");
yield this.checkLanguageSupport(detectedLanguageResults);
});
}
checkLanguageSupport(detectedLanguageResults) {
return __awaiter(this, void 0, void 0, function* () {
const { summarizeLanguageSupport } = new LanguageSupport_1.LanguageSupport();
const detectedLanguage = detectedLanguageResults.language;
const { acceptedTargetLanguages,
// defaultSourceLanguage,
defaultTargetLanguage, supportedSourceLanguages, supportedTargetLanguagesGivenDefaultSourceLanguage, allPossiblySupportedTargetLanguages, } = yield summarizeLanguageSupport(detectedLanguageResults);
if (acceptedTargetLanguages.includes(detectedLanguage)) {
// Detected language is the same as the user's accepted target languages.
console.info("Detected language is in one of the user's accepted target languages.", {
acceptedTargetLanguages,
});
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.SOURCE_LANGUAGE_UNDERSTOOD);
return;
}
if (!supportedSourceLanguages.includes(detectedLanguage)) {
// Detected language is not part of the supported source languages.
console.info("Detected language is not part of the supported source languages.", { detectedLanguage, supportedSourceLanguages });
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.TRANSLATION_UNSUPPORTED);
return;
}
if (!allPossiblySupportedTargetLanguages.includes(defaultTargetLanguage)) {
// Detected language is not part of the supported source languages.
console.info("Default target language is not part of the supported target languages.", {
acceptedTargetLanguages,
defaultTargetLanguage,
allPossiblySupportedTargetLanguages,
});
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.TRANSLATION_UNSUPPORTED);
return;
}
if (!defaultTargetLanguage ||
!supportedTargetLanguagesGivenDefaultSourceLanguage.includes(defaultTargetLanguage)) {
// Combination of source and target languages unsupported
console.info("Combination of source and target languages unsupported.", {
acceptedTargetLanguages,
defaultTargetLanguage,
supportedTargetLanguagesGivenDefaultSourceLanguage,
});
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.TRANSLATION_UNSUPPORTED);
return;
}
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.OFFER);
});
}
doTranslation(from, to) {
return __awaiter(this, void 0, void 0, function* () {
// If a TranslationDocument already exists for this document, it should
// be used instead of creating a new one so that we can use the original
// content of the page for the new translation instead of the newly
// translated text.
const translationDocument = this.contentWindow.translationDocument ||
new TranslationDocument_1.TranslationDocument(this.document);
console.info("Translating web page");
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.TRANSLATING);
const domTranslator = new BergamotDomTranslator_1.BergamotDomTranslator(translationDocument, from, to);
this.contentWindow.translationDocument = translationDocument;
translationDocument.translatedFrom = from;
translationDocument.translatedTo = to;
translationDocument.translationError = false;
try {
console.info(`About to translate web page document (${translationDocument.translationRoots.length} translation items)`, { from, to });
// TODO: Timeout here to be able to abort UI in case translation hangs
yield domTranslator.translate((frameTranslationProgress) => {
this.documentTranslationStateCommunicator.broadcastUpdatedFrameTranslationProgress(frameTranslationProgress);
});
console.info(`Translation of web page document completed (translated ${translationDocument.translationRoots.filter(translationRoot => translationRoot.currentDisplayMode === "translation").length} out of ${translationDocument.translationRoots.length} translation items)`, { from, to });
console.info("Translated web page");
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.TRANSLATED);
}
catch (err) {
console.warn("Translation error occurred: ", err);
translationDocument.translationError = true;
this.documentTranslationStateCommunicator.broadcastUpdatedTranslationStatus(BaseTranslationState_1.TranslationStatus.ERROR);
}
finally {
// Communicate that errors occurred
// Positioned in finally-clause so that it gets communicated whether the
// translation attempt resulted in some translated content or not
domTranslator.errorsEncountered.forEach((error) => {
if (error.name === "BergamotTranslatorAPIModelLoadError") {
this.documentTranslationStateCommunicator.broadcastUpdatedAttributeValue("modelLoadErrorOccurred", true);
}
else if (error.name === "BergamotTranslatorAPIModelDownloadError") {
this.documentTranslationStateCommunicator.broadcastUpdatedAttributeValue("modelDownloadErrorOccurred", true);
}
else if (error.name === "BergamotTranslatorAPITranslationError") {
this.documentTranslationStateCommunicator.broadcastUpdatedAttributeValue("translationErrorOccurred", true);
}
else {
this.documentTranslationStateCommunicator.broadcastUpdatedAttributeValue("otherErrorOccurred", true);
}
});
}
});
}
getDocumentTranslationStatistics() {
return __awaiter(this, void 0, void 0, function* () {
const translationDocument = this.contentWindow.translationDocument ||
new TranslationDocument_1.TranslationDocument(this.document);
const { translationRoots } = translationDocument;
const { translationRootsVisible, translationRootsVisibleInViewport, } = yield translationDocument.determineVisibilityOfTranslationRoots();
const generateOriginalMarkupToTranslate = translationRoot => translationDocument.generateMarkupToTranslate(translationRoot);
const removeTags = originalString => {
const detaggedString = detagAndProject_1.detag(originalString);
return detaggedString.plainString;
};
const texts = translationRoots
.map(generateOriginalMarkupToTranslate)
.map(removeTags);
const textsVisible = translationRootsVisible
.map(generateOriginalMarkupToTranslate)
.map(removeTags);
const textsVisibleInViewport = translationRootsVisibleInViewport
.map(generateOriginalMarkupToTranslate)
.map(removeTags);
const wordCount = texts.join(" ").split(" ").length;
const wordCountVisible = textsVisible.join(" ").split(" ").length;
const wordCountVisibleInViewport = textsVisibleInViewport
.join(" ")
.split(" ").length;
const translationRootsCount = translationRoots.length;
const simpleTranslationRootsCount = translationRoots.filter(translationRoot => translationRoot.isSimleTranslationRoot).length;
return {
translationRootsCount,
simpleTranslationRootsCount,
texts,
textsVisible,
textsVisibleInViewport,
wordCount,
wordCountVisible,
wordCountVisibleInViewport,
};
});
}
}
exports.DomTranslationManager = DomTranslationManager;
/***/ }),
/***/ 992:
/*!**********************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/TranslationDocument.ts ***!
\**********************************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateMarkupToTranslateForItem = exports.TranslationDocument = void 0;
const getTranslationNodes_1 = __webpack_require__(/*! ./getTranslationNodes */ 5173);
const TranslationItem_1 = __webpack_require__(/*! ./TranslationItem */ 6771);
/**
* This class represents a document that is being translated,
* and it is responsible for parsing the document,
* generating the data structures translation (the list of
* translation items and roots), and managing the original
* and translated texts on the translation items.
*
* @param document The document to be translated
*/
class TranslationDocument {
constructor(document) {
this.translatedFrom = "";
this.translatedTo = "";
this.translationError = false;
this.originalShown = true;
this.qualityEstimationShown = true;
// Set temporarily to true during development to visually inspect which nodes have been processed and in which way
this.paintProcessedNodes = false;
this.nodeTranslationItemsMap = new Map();
this.translationRoots = [];
this._init(document);
}
/**
* Initializes the object and populates
* the translation roots lists.
*
* @param document The document to be translated
*/
_init(document) {
// Get all the translation nodes in the document's body:
// a translation node is a node from the document which
// contains useful content for translation, and therefore
// must be included in the translation process.
const translationNodes = getTranslationNodes_1.getTranslationNodes(document.body);
console.info(`The document has a total of ${translationNodes.length} translation nodes, of which ${translationNodes.filter(tn => tn.isTranslationRoot).length} are translation roots`);
translationNodes.forEach((translationNode, index) => {
const { content, isTranslationRoot } = translationNode;
if (this.paintProcessedNodes) {
content.style.backgroundColor = "darkorange";
}
// Create a TranslationItem object for this node.
// This function will also add it to the this.translationRoots array.
this._createItemForNode(content, index, isTranslationRoot);
});
// At first all translation roots are stored in the translation roots list, and only after
// the process has finished we're able to determine which translation roots are
// simple, and which ones are not.
// A simple root is defined by a translation root with no children items, which
// basically represents an element from a page with only text content
// inside.
// This distinction is useful for optimization purposes: we treat a
// simple root as plain-text in the translation process and with that
// we are able to reduce their data payload sent to the translation service.
for (const translationRoot of this.translationRoots) {
if (!translationRoot.children.length &&
translationRoot.nodeRef instanceof Element &&
translationRoot.nodeRef.childElementCount === 0) {
translationRoot.isSimleTranslationRoot = true;
if (this.paintProcessedNodes) {
translationRoot.nodeRef.style.backgroundColor = "orange";
}
}
}
}
/**
* Creates a TranslationItem object, which should be called
* for each node returned by getTranslationNodes.
*
* @param node The DOM node for this item.
* @param id A unique, numeric id for this item.
* @param isTranslationRoot A boolean saying whether this item is a translation root.
*
* @returns A TranslationItem object.
*/
_createItemForNode(node, id, isTranslationRoot) {
if (this.nodeTranslationItemsMap.has(node)) {
return this.nodeTranslationItemsMap.get(node);
}
const item = new TranslationItem_1.TranslationItem(node, id, isTranslationRoot);
if (isTranslationRoot) {
// Translation root items do not have a parent item.
this.translationRoots.push(item);
}
else {
// Other translation nodes have at least one ancestor which is a translation root
let ancestorTranslationItem;
for (let ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
ancestorTranslationItem = this.nodeTranslationItemsMap.get(ancestor);
if (ancestorTranslationItem) {
ancestorTranslationItem.children.push(item);
break;
}
else {
// make intermediate ancestors link to the descendent translation item
// so that it gets picked up on in generateOriginalStructureElements
this.nodeTranslationItemsMap.set(ancestor, item);
}
}
}
this.nodeTranslationItemsMap.set(node, item);
return item;
}
/**
* Generate the markup that represents a TranslationItem object.
* Besides generating the markup, it also stores a fuller representation
* of the TranslationItem in the "original" field of the TranslationItem object,
* which needs to be stored for later to be used in the "Show Original" functionality.
* If this function had already been called for the given item (determined
* by the presence of the "original" array in the item), the markup will
* be regenerated from the "original" data instead of from the related
* DOM nodes (because the nodes might contain translated data).
*
* @param item A TranslationItem object
*
* @returns A markup representation of the TranslationItem.
*/
generateMarkupToTranslate(item) {
if (!item.original) {
item.original = this.generateOriginalStructureElements(item);
}
return regenerateMarkupToTranslateFromOriginal(item);
}
/**
* Generates a fuller representation of the TranslationItem
* @param item
*/
generateOriginalStructureElements(item) {
const original = [];
if (item.isSimleTranslationRoot) {
const text = item.nodeRef.firstChild.nodeValue.trim();
original.push(text);
return original;
}
let wasLastItemPlaceholder = false;
for (const child of Array.from(item.nodeRef.childNodes)) {
if (child.nodeType === child.TEXT_NODE) {
const x = child.nodeValue;
const hasLeadingWhitespace = x.length !== x.trimStart().length;
const hasTrailingWhitespace = x.length !== x.trimEnd().length;
if (x.trim() !== "") {
const xWithNormalizedWhitespace = `${hasLeadingWhitespace ? " " : ""}${x.trim()}${hasTrailingWhitespace ? " " : ""}`;
original.push(xWithNormalizedWhitespace);
wasLastItemPlaceholder = false;
}
continue;
}
const objInMap = this.nodeTranslationItemsMap.get(child);
if (objInMap && !objInMap.isTranslationRoot) {
// If this childNode is present in the nodeTranslationItemsMap, it means
// it's a translation node: it has useful content for translation.
// In this case, we need to stringify this node.
// However, if this item is a translation root, we should skip it here in this
// object's child list (and just add a placeholder for it), because
// it will be stringified separately for being a translation root.
original.push(objInMap);
objInMap.original = this.generateOriginalStructureElements(objInMap);
wasLastItemPlaceholder = false;
}
else if (!wasLastItemPlaceholder) {
// Otherwise, if this node doesn't contain any useful content,
// or if it is a translation root itself, we can replace it with a placeholder node.
// We can't simply eliminate this node from our string representation
// because that could change the HTML structure (e.g., it would
// probably merge two separate text nodes).
// It's not necessary to add more than one placeholder in sequence;
// we can optimize them away.
original.push(new TranslationItem_1.TranslationItem_NodePlaceholder());
wasLastItemPlaceholder = true;
}
}
return original;
}
/**
* Changes the document to display its translated
* content.
*/
showTranslation() {
this.originalShown = false;
this.qualityEstimationShown = false;
this._swapDocumentContent("translation");
}
/**
* Changes the document to display its original
* content.
*/
showOriginal() {
this.originalShown = true;
this.qualityEstimationShown = false;
this._swapDocumentContent("original");
// TranslationTelemetry.recordShowOriginalContent();
}
/**
* Changes the document to display the translation with quality estimation metadata
* content.
*/
showQualityEstimation() {
this.originalShown = false;
this.qualityEstimationShown = true;
this._swapDocumentContent("qeAnnotatedTranslation");
}
/**
* Swap the document with the resulting translation,
* or back with the original content.
*/
_swapDocumentContent(target) {
(() => __awaiter(this, void 0, void 0, function* () {
this.translationRoots
.filter(translationRoot => translationRoot.currentDisplayMode !== target)
.forEach(translationRoot => translationRoot.swapText(target, this.paintProcessedNodes));
// TODO: Make sure that the above does not lock the main event loop
/*
// Let the event loop breath on every 100 nodes
// that are replaced.
const YIELD_INTERVAL = 100;
await Async.yieldingForEach(
this.roots,
root => root.swapText(target, this.paintProcessedNodes),
YIELD_INTERVAL
);
*/
}))();
}
determineVisibilityOfTranslationRoots() {
return __awaiter(this, void 0, void 0, function* () {
const { translationRoots } = this;
const elements = translationRoots.map(translationRoot => translationRoot.nodeRef);
const elementsVisibleInViewport = yield getElementsVisibleInViewport(elements);
const translationRootsVisible = [];
const translationRootsVisibleInViewport = [];
for (let i = 0; i < translationRoots.length; i++) {
const translationRoot = translationRoots[i];
const visible = isElementVisible(translationRoot.nodeRef);
if (visible) {
translationRootsVisible.push(translationRoot);
}
const visibleInViewport = isElementVisibleInViewport(elementsVisibleInViewport, translationRoot.nodeRef);
if (visibleInViewport) {
translationRootsVisibleInViewport.push(translationRoot);
}
}
if (this.paintProcessedNodes) {
translationRootsVisible.forEach(translationRoot => {
translationRoot.nodeRef.style.color = "purple";
});
translationRootsVisibleInViewport.forEach(translationRoot => {
translationRoot.nodeRef.style.color = "maroon";
});
}
return {
translationRootsVisible,
translationRootsVisibleInViewport,
};
});
}
}
exports.TranslationDocument = TranslationDocument;
/**
* Generate the translation markup for a given item.
*
* @param item A TranslationItem object.
* @param content The inner content for this item.
* @returns string The outer HTML needed for translation
* of this item.
*/
function generateMarkupToTranslateForItem(item, content) {
const localName = item.isTranslationRoot ? "div" : "b";
return ("<" + localName + " id=n" + item.id + ">" + content + "</" + localName + ">");
}
exports.generateMarkupToTranslateForItem = generateMarkupToTranslateForItem;
/**
* Regenerate the markup that represents a TranslationItem object,
* with data from its "original" array. The array must have already
* been created by TranslationDocument.generateMarkupToTranslate().
*
* @param item A TranslationItem object
*
* @returns A markup representation of the TranslationItem.
*/
function regenerateMarkupToTranslateFromOriginal(item) {
if (item.isSimleTranslationRoot) {
return item.original[0];
}
let str = "";
for (const child of item.original) {
if (child instanceof TranslationItem_1.TranslationItem) {
str += regenerateMarkupToTranslateFromOriginal(child);
}
else if (child instanceof TranslationItem_1.TranslationItem_NodePlaceholder) {
str += "<br>";
}
else {
str += child;
}
}
return generateMarkupToTranslateForItem(item, str);
}
const isElementVisible = (el) => {
const rect = el.getBoundingClientRect();
// Elements that are not visible will have a zero width/height bounding client rect
return rect.width > 0 && rect.height > 0;
};
const isElementVisibleInViewport = (elementsVisibleInViewport, el) => {
return !!elementsVisibleInViewport.filter($el => $el === el).length;
};
const getElementsVisibleInViewport = (elements) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise(resolve => {
const options = {
threshold: 0.0,
};
const callback = (entries, $observer) => {
// console.debug("InteractionObserver callback", entries.length, entries);
const elementsInViewport = entries
.filter(entry => entry.isIntersecting)
.map(entry => entry.target);
$observer.disconnect();
resolve(elementsInViewport);
};
const observer = new IntersectionObserver(callback, options);
elements.forEach(el => observer.observe(el));
});
});
/***/ }),
/***/ 6771:
/*!******************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/TranslationItem.ts ***!
\******************************************************************************************/
/***/ ((__unused_webpack_module, exports) => {
/* 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/. */
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.TranslationItem_NodePlaceholder = exports.TranslationItem = void 0;
/**
* This class represents an item for translation. It's basically our
* wrapper class around a node returned by getTranslationNode, with
* more data and structural information on it.
*
* At the end of the translation process, besides the properties below,
* a TranslationItem will contain two other properties: one called "original"
* and one called "translation". They are twin objects, one which reflect
* the structure of that node in its original state, and the other in its
* translated state.
*
* The "original" array is generated in the
* TranslationDocument.generateMarkupToTranslate function,
* and the "translation" array is generated when the translation results
* are parsed.
*
* They are both arrays, which contain a mix of strings and references to
* child TranslationItems. The references in both arrays point to the * same *
* TranslationItem object, but they might appear in different orders between the
* "original" and "translation" arrays.
*
* An example:
*
* English: <div id="n1">Welcome to <b id="n2">Mozilla's</b> website</div>
* Portuguese: <div id="n1">Bem vindo a pagina <b id="n2">da Mozilla</b></div>
*
* TranslationItem n1 = {
* id: 1,
* original: ["Welcome to", ptr to n2, "website"]
* translation: ["Bem vindo a pagina", ptr to n2]
* }
*
* TranslationItem n2 = {
* id: 2,
* original: ["Mozilla's"],
* translation: ["da Mozilla"]
* }
*/
class TranslationItem {
constructor(node, id, isTranslationRoot) {
this.isTranslationRoot = false;
this.isSimleTranslationRoot = false;
this.children = [];
this.nodeRef = node;
this.id = id;
this.isTranslationRoot = isTranslationRoot;
}
toString() {
let rootType = "";
if (this.isTranslationRoot) {
if (this.isSimleTranslationRoot) {
rootType = " (simple root)";
}
else {
rootType = " (non simple root)";
}
}
return ("[object TranslationItem: <" +
this.nodeRef.toString() +
">" +
rootType +
"]");
}
/**
* This function will parse the result of the translation of one translation
* item. If this item was a simple root, all we sent was a plain-text version
* of it, so the result is also straightforward text.
*
* For non-simple roots, we sent a simplified HTML representation of that
* node, and we'll first parse that into an HTML doc and then call the
* parseResultNode helper function to parse it.
*
* While parsing, the result is stored in the "translation" field of the
* TranslationItem, which will be used to display the final translation when
* all items are finished. It remains stored too to allow back-and-forth
* switching between the "Show Original" and "Show Translation" functions.
*
* @param translatedMarkup A string with the textual result received from the translation engine,
* which can be plain-text or a serialized HTML doc.
*/
parseTranslationResult(translatedMarkup) {
this.translatedMarkup = translatedMarkup;
if (this.isSimleTranslationRoot) {
// If translation contains HTML entities, we need to convert them.
// It is because simple roots expect a plain text result.
if (this.isSimleTranslationRoot &&
translatedMarkup.match(/&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});/gi)) {
const doc = new DOMParser().parseFromString(translatedMarkup, "text/html");
translatedMarkup = doc.body.firstChild.nodeValue;
}
this.translation = [translatedMarkup];
return;
}
const domParser = new DOMParser();
const doc = domParser.parseFromString(translatedMarkup, "text/html");
this.translation = [];
parseResultNode(this, doc.body.firstChild, "translation");
}
/**
* Note: QE-annotated translation results are never plain text nodes, despite that
* the original translation item may be a simple translation root.
* This wreaks havoc.
* @param qeAnnotatedTranslatedMarkup
*/
parseQeAnnotatedTranslationResult(qeAnnotatedTranslatedMarkup) {
const domParser = new DOMParser();
const doc = domParser.parseFromString(qeAnnotatedTranslatedMarkup, "text/html");
this.qeAnnotatedTranslation = [];
parseResultNode(this, doc.body.firstChild, "qeAnnotatedTranslation");
}
/**
* This function finds a child TranslationItem
* with the given id.
* @param id The id to look for, in the format "n#"
* @returns A TranslationItem with the given id, or null if
* it was not found.
*/
getChildById(id) {
for (const child of this.children) {
const childId = "n" + child.id;
if (childId === id) {
return child;
}
}
return null;
}
/**
* Swap the text of this TranslationItem between
* its original and translated states.
*/
swapText(target, paintProcessedNodes) {
swapTextForItem(this, target, paintProcessedNodes);
}
}
exports.TranslationItem = TranslationItem;
/**
* This object represents a placeholder item for translation. It's similar to
* the TranslationItem class, but it represents nodes that have no meaningful
* content for translation. These nodes will be replaced by "<br>" in a
* translation request. It's necessary to keep them to use it as a mark
* for correct positioning and splitting of text nodes.
*/
class TranslationItem_NodePlaceholder {
static toString() {
return "[object TranslationItem_NodePlaceholder]";
}
}
exports.TranslationItem_NodePlaceholder = TranslationItem_NodePlaceholder;
/**
* Helper function to parse a HTML doc result.
* How it works:
*
* An example result string is:
*
* <div id="n1">Hello <b id="n2">World</b> of Mozilla.</div>
*
* For an element node, we look at its id and find the corresponding
* TranslationItem that was associated with this node, and then we
* walk down it repeating the process.
*
* For text nodes we simply add it as a string.
*/
function parseResultNode(item, node, target) {
try {
const into = item[target];
// @ts-ignore
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
into.push(child.nodeValue);
}
else if (child.localName === "br") {
into.push(new TranslationItem_NodePlaceholder());
}
else if (child.dataset &&
typeof child.dataset.translationQeScore !== "undefined") {
// handle the special case of quality estimate annotated nodes
into.push(child);
}
else {
const translationRootChild = item.getChildById(child.id);
if (translationRootChild) {
into.push(translationRootChild);
translationRootChild[target] = [];
parseResultNode(translationRootChild, child, target);
}
else {
console.warn(`Result node's (belonging to translation item with id ${item.id}) child node (child.id: ${child.id}) lacks an associated translation root child`, { item, node, child });
}
}
}
}
catch (err) {
console.error(err);
throw err;
}
}
/* eslint-disable complexity */
// TODO: Simplify swapTextForItem to avoid the following eslint error:
// Function 'swapTextForItem' has a complexity of 35. Maximum allowed is 34 complexity
/**
* Helper function to swap the text of a TranslationItem
* between its original and translated states.
* How it works:
*
* The function iterates through the target array (either the `original` or
* `translation` array from the TranslationItem), while also keeping a pointer
* to a current position in the child nodes from the actual DOM node that we
* are modifying. This pointer is moved forward after each item of the array
* is translated. If, at any given time, the pointer doesn't match the expected
* node that was supposed to be seen, it means that the original and translated
* contents have a different ordering, and thus we need to adjust that.
*
* A full example of the reordering process, swapping from Original to
* Translation:
*
* Original (en): <div>I <em>miss</em> <b>you</b></div>
*
* Translation (fr): <div><b>Tu</b> me <em>manques</em></div>
*
* Step 1:
* pointer points to firstChild of the DOM node, textnode "I "
* first item in item.translation is [object TranslationItem <b>]
*
* pointer does not match the expected element, <b>. So let's move <b> to the
* pointer position.
*
* Current state of the DOM:
* <div><b>you</b>I <em>miss</em> </div>
*
* Step 2:
* pointer moves forward to nextSibling, textnode "I " again.
* second item in item.translation is the string " me "
*
* pointer points to a text node, and we were expecting a text node. Match!
* just replace the text content.
*
* Current state of the DOM:
* <div><b>you</b> me <em>miss</em> </div>
*
* Step 3:
* pointer moves forward to nextSibling, <em>miss</em>
* third item in item.translation is [object TranslationItem <em>]
*
* pointer points to the expected node. Match! Nothing to do.
*
* Step 4:
* all items in this item.translation were transformed. The remaining
* text nodes are cleared to "", and domNode.normalize() removes them.
*
* Current state of the DOM:
* <div><b>you</b> me <em>miss</em></div>
*
* Further steps:
* After that, the function will visit the child items (from the visitStack),
* and the text inside the <b> and <em> nodes will be swapped as well,
* yielding the final result:
*
* <div><b>Tu</b> me <em>manques</em></div>
*/
function swapTextForItem(item, target, paintProcessedNodes) {
// visitStack is the stack of items that we still need to visit.
// Let's start the process by adding the translation root item.
const visitStack = [item];
if (paintProcessedNodes) {
item.nodeRef.style.border = "1px solid maroon";
}
while (visitStack.length) {
const curItem = visitStack.shift();
if (paintProcessedNodes) {
item.nodeRef.style.border = "1px solid yellow";
}
const domNode = curItem.nodeRef;
if (!domNode) {
// Skipping this item due to a missing node.
continue;
}
if (!curItem[target]) {
// Translation not found for this item. This could be due to
// the translation not yet being available from the translation engine
// For example, if a translation was broken in various
// chunks, and not all of them has completed, the items from that
// chunk will be missing its "translation" field.
if (paintProcessedNodes) {
curItem.nodeRef.style.border = "1px solid red";
}
continue;
}
domNode.normalize();
if (paintProcessedNodes) {
curItem.nodeRef.style.border = "1px solid green";
}
// curNode points to the child nodes of the DOM node that we are
// modifying. During most of the process, while the target array is
// being iterated (in the for loop below), it should walk together with
// the array and be pointing to the correct node that needs to modified.
// If it's not pointing to it, that means some sort of node reordering
// will be necessary to produce the correct translation.
// Note that text nodes don't need to be reordered, as we can just replace
// the content of one text node with another.
//
// curNode starts in the firstChild...
let curNode = domNode.firstChild;
if (paintProcessedNodes && curNode instanceof HTMLElement) {
curNode.style.border = "1px solid blue";
}
// ... actually, let's make curNode start at the first useful node (either
// a non-blank text node or something else). This is not strictly necessary,
// as the reordering algorithm would correctly handle this case. However,
// this better aligns the resulting translation with the DOM content of the
// page, avoiding cases that would need to be unnecessarily reordered.
//
// An example of how this helps:
//
// ---- Original: <div> <b>Hello </b> world.</div>
// ^textnode 1 ^item 1 ^textnode 2
//
// - Translation: <div><b>Hallo </b> Welt.</div>
//
// Transformation process without this optimization:
// 1 - start pointer at textnode 1
// 2 - move item 1 to first position inside the <div>
//
// Node now looks like: <div><b>Hello </b>[ ][ world.]</div>
// textnode 1^ ^textnode 2
//
// 3 - replace textnode 1 with " Welt."
// 4 - clear remaining text nodes (in this case, textnode 2)
//
// Transformation process with this optimization:
// 1 - start pointer at item 1
// 2 - item 1 is already in position
// 3 - replace textnode 2 with " Welt."
//
// which completely avoids any node reordering, and requires only one
// text change instead of two (while also leaving the page closer to
// its original state).
while (curNode &&
curNode.nodeType === Node.TEXT_NODE &&
curNode.nodeValue.trim() === "") {
curNode = curNode.nextSibling;
}
// Now let's walk through all items in the `target` array of the
// TranslationItem. This means either the TranslationItem.original or
// TranslationItem.translation array.
for (const targetItem of curItem[target]) {
if (targetItem instanceof TranslationItem) {
// If the array element is another TranslationItem object, let's
// add it to the stack to be visited.
visitStack.push(targetItem);
const targetNode = targetItem.nodeRef;
// If the node is not in the expected position, let's reorder
// it into position...
if (curNode !== targetNode &&
// ...unless the page has reparented this node under a totally
// different node (or removed it). In this case, all bets are off
// on being able to do anything correctly, so it's better not to
// bring back the node to this parent.
// @ts-ignore
targetNode.parentNode === domNode) {
// We don't need to null-check curNode because insertBefore(..., null)
// does what we need in that case: reorder this node to the end
// of child nodes.
domNode.insertBefore(targetNode, curNode);
curNode = targetNode;
}
// Move pointer forward. Since we do not add empty text nodes to the
// list of translation items, we must skip them here too while
// traversing the DOM in order to get better alignment between the
// text nodes and the translation items.
if (curNode) {
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
}
}
else if (targetItem instanceof TranslationItem_NodePlaceholder) {
// If the current item is a placeholder node, we need to move
// our pointer "past" it, jumping from one side of a block of
// elements + empty text nodes to the other side. Even if
// non-placeholder elements exists inside the jumped block,
// they will be pulled correctly later in the process when the
// targetItem for those nodes are handled.
while (curNode &&
(curNode.nodeType !== Node.TEXT_NODE ||
curNode.nodeValue.trim() === "")) {
curNode = curNode.nextSibling;
}
}
else {
// Finally, if it's a text item, we just need to find the next
// text node to use. Text nodes don't need to be reordered, so
// the first one found can be used.
while (curNode && curNode.nodeType !== Node.TEXT_NODE) {
curNode = curNode.nextSibling;
}
// If none was found and we reached the end of the child nodes,
// let's create a new one.
if (!curNode) {
// We don't know if the original content had a space or not,
// so the best bet is to create the text node with " " which
// will add one space at the beginning and one at the end.
curNode = domNode.appendChild(domNode.ownerDocument.createTextNode(" "));
}
if (target === "translation") {
// A trailing and a leading space must be preserved because
// they are meaningful in HTML.
const preSpace = /^\s/.test(curNode.nodeValue) ? " " : "";
const endSpace = /\s$/.test(curNode.nodeValue) ? " " : "";
curNode.nodeValue = preSpace + targetItem + endSpace;
}
else {
curNode.nodeValue = targetItem;
}
if (["original", "translation"].includes(target)) {
// Workaround necessary when switching "back" from QE display
// since quality estimated annotated nodes
// replaced the simple text nodes of the original document
// @ts-ignore
for (const child of curNode.parentNode.childNodes) {
if (child.dataset &&
typeof child.dataset.translationQeScore !== "undefined") {
// There should be only 1 such node. Remove the curNode from the
// parent's children and replace the qe-annotated node with it to
// maintain the right order in original DOM tree of the document.
curNode.remove();
child.parentNode.replaceChild(curNode, child);
}
}
curNode = getNextSiblingSkippingEmptyTextNodes(curNode);
}
else if (target === "qeAnnotatedTranslation") {
const nextSibling = getNextSiblingSkippingEmptyTextNodes(curNode);
// Replace the text node with the qe-annotated node to maintain the
// right order in original DOM tree of the document.
curNode.parentNode.replaceChild(targetItem, curNode);
curNode = nextSibling;
}
}
}
// The translated version of a node might have less text nodes than its
// original version. If that's the case, let's clear the remaining nodes.
if (curNode) {
clearRemainingNonEmptyTextNodesFromElement(curNode);
}
// And remove any garbage "" nodes left after clearing.
domNode.normalize();
// Mark the translation item as displayed in the requested target
curItem.currentDisplayMode = target;
if (paintProcessedNodes) {
curItem.nodeRef.style.border = "2px solid green";
}
}
}
/* eslint-enable complexity */
function getNextSiblingSkippingEmptyTextNodes(startSibling) {
let item = startSibling.nextSibling;
while (item &&
item.nodeType === Node.TEXT_NODE &&
item.nodeValue.trim() === "") {
item = item.nextSibling;
}
return item;
}
function clearRemainingNonEmptyTextNodesFromElement(startSibling) {
let item = startSibling;
while (item) {
if (item.nodeType === Node.TEXT_NODE && item.nodeValue !== "") {
item.nodeValue = "";
}
item = item.nextSibling;
}
}
/***/ }),
/***/ 1435:
/*!************************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/dom-translators/BaseDomTranslator.ts ***!
\************************************************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.BaseDomTranslator = exports.DomTranslatorError = void 0;
const MinimalDomTranslator_1 = __webpack_require__(/*! ./MinimalDomTranslator */ 1975);
class DomTranslatorError extends Error {
constructor() {
super(...arguments);
this.name = "DomTranslatorError";
}
}
exports.DomTranslatorError = DomTranslatorError;
/**
* Base class for DOM translators that splits the document into several chunks
* respecting the data limits of the backing API.
*/
class BaseDomTranslator extends MinimalDomTranslator_1.MinimalDomTranslator {
/**
* @param translationDocument The TranslationDocument object that represents
* the webpage to be translated
* @param sourceLanguage The source language of the document
* @param targetLanguage The target language for the translation
* @param translationApiClient
* @param parseChunkResult
* @param translationApiLimits
* @param domTranslatorRequestFactory
*/
constructor(translationDocument, sourceLanguage, targetLanguage, translationApiClient, parseChunkResult, translationApiLimits, domTranslatorRequestFactory) {
super(translationDocument, sourceLanguage, targetLanguage);
this.translatedCharacterCount = 0;
this.errorsEncountered = [];
this.partialSuccess = false;
this.translationApiClient = translationApiClient;
this.parseChunkResult = parseChunkResult;
this.translationApiLimits = translationApiLimits;
this.domTranslatorRequestFactory = domTranslatorRequestFactory;
}
/**
* Performs the translation, splitting the document into several chunks
* respecting the data limits of the API.
*
* @returns {Promise} A promise that will resolve when the translation
* task is finished.
*/
translate(frameTranslationProgressCallback) {
return __awaiter(this, void 0, void 0, function* () {
const chunksBeingProcessed = [];
const { MAX_REQUESTS } = this.translationApiLimits;
const { translationRoots } = this.translationDocument;
const { translationRootsVisible, translationRootsVisibleInViewport, } = yield this.translationDocument.determineVisibilityOfTranslationRoots();
this.translationRootsPickedUpForTranslation = [];
const progressOfIndividualTranslationRequests = new Map();
// Split the document into various requests to be sent to the translation API
for (let currentRequestOrdinal = 0; currentRequestOrdinal < MAX_REQUESTS; currentRequestOrdinal++) {
// Determine the data for the next request.
const domTranslationChunk = this.generateNextDomTranslationChunk(translationRoots, translationRootsVisible, translationRootsVisibleInViewport);
// Break if there was nothing left to translate
if (domTranslationChunk.translationRoots.length === 0) {
break;
}
// Create a real request for the translation engine and add it to the pending requests list.
const translationRequestData = domTranslationChunk.translationRequestData;
const domTranslatorRequest = this.domTranslatorRequestFactory(translationRequestData, this.sourceLanguage, this.targetLanguage);
// Fire off the requests in parallel to existing requests
const chunkBeingProcessed = domTranslatorRequest.fireRequest(this.translationApiClient, (translationRequestProgress) => {
progressOfIndividualTranslationRequests.set(translationRequestProgress.requestId, translationRequestProgress);
frameTranslationProgressCallback({
progressOfIndividualTranslationRequests,
});
});
chunksBeingProcessed.push(chunkBeingProcessed);
chunkBeingProcessed
.then((translationResponseData) => {
if (translationResponseData) {
this.chunkCompleted(translationResponseData, domTranslationChunk, domTranslatorRequest);
}
else {
throw new Error("The returned translationResponseData was false/empty");
}
})
.catch(err => {
this.errorsEncountered.push(err);
});
console.info(`Fired off request with ${domTranslationChunk.translationRoots.length} translation roots to the translation backend`, { domTranslationChunk });
if (domTranslationChunk.isLastChunk) {
break;
}
}
// Return early with a noop if there is nothing to translate
if (chunksBeingProcessed.length === 0) {
console.info("Found nothing to translate");
return { characterCount: 0 };
}
console.info(`Fired off ${chunksBeingProcessed.length} requests to the translation backend`);
// Wait for all requests to settle
yield Promise.allSettled(chunksBeingProcessed);
// Surface encountered errors
if (this.errorsEncountered.length) {
console.warn("Errors were encountered during translation", this.errorsEncountered);
}
// If at least one chunk was successful, the
// translation should be displayed, albeit incomplete.
// Otherwise, the "Error" state will appear.
if (!this.partialSuccess) {
throw new DomTranslatorError("No content was translated");
}
return {
characterCount: this.translatedCharacterCount,
};
});
}
/**
* Function called when a request sent to the translation engine completed successfully.
* This function handles calling the function to parse the result and the
* function to resolve the promise returned by the public `translate()`
* method when there's no pending request left.
*/
chunkCompleted(translationResponseData, domTranslationChunk, domTranslatorRequest) {
if (this.parseChunkResult(translationResponseData, domTranslationChunk)) {
this.partialSuccess = true;
// Count the number of characters successfully translated.
this.translatedCharacterCount += domTranslatorRequest.characterCount;
// Show translated chunks as they arrive
console.info("Part of the web page document translated. Showing translations that have completed so far...");
this.translationDocument.showTranslation();
}
}
/**
* This function will determine what is the data to be used for
* the Nth request we are generating, based on the input params.
*/
generateNextDomTranslationChunk(translationRoots, translationRootsVisible, translationRootsVisibleInViewport) {
let currentDataSize = 0;
let currentChunks = 0;
const translationRequestData = {
markupsToTranslate: [],
};
const chunkTranslationRoots = [];
const { MAX_REQUEST_DATA, MAX_REQUEST_TEXTS } = this.translationApiLimits;
let translationRootsToConsider;
// Don't consider translation roots that are already picked up for translation
const notYetPickedUp = ($translationRoots) => $translationRoots.filter(value => !this.translationRootsPickedUpForTranslation.includes(value));
// Prioritize the translation roots visible in viewport
translationRootsToConsider = notYetPickedUp(translationRootsVisibleInViewport);
// Then prioritize the translation roots that are visible
if (translationRootsToConsider.length === 0) {
translationRootsToConsider = notYetPickedUp(translationRootsVisible);
}
// Then prioritize the remaining translation roots
if (translationRootsToConsider.length === 0) {
translationRootsToConsider = notYetPickedUp(translationRoots);
}
for (let i = 0; i < translationRootsToConsider.length; i++) {
const translationRoot = translationRootsToConsider[i];
const markupToTranslate = this.translationDocument.generateMarkupToTranslate(translationRoot);
const newCurSize = currentDataSize + markupToTranslate.length;
const newChunks = currentChunks + 1;
if (newCurSize > MAX_REQUEST_DATA || newChunks > MAX_REQUEST_TEXTS) {
// If we've reached the API limits, let's stop accumulating data
// for this request and return. We return information useful for
// the caller to pass back on the next call, so that the function
// can keep working from where it stopped.
console.info("We have reached the specified translation API limits and will process remaining translation roots in a separate request", {
newCurSize,
newChunks,
translationApiLimits: this.translationApiLimits,
});
return {
translationRequestData,
translationRoots: chunkTranslationRoots,
isLastChunk: false,
};
}
currentDataSize = newCurSize;
currentChunks = newChunks;
chunkTranslationRoots.push(translationRoot);
this.translationRootsPickedUpForTranslation.push(translationRoot);
translationRequestData.markupsToTranslate.push(markupToTranslate);
}
const remainingTranslationRoots = notYetPickedUp(translationRoots);
const isLastChunk = remainingTranslationRoots.length === 0;
return {
translationRequestData,
translationRoots: chunkTranslationRoots,
isLastChunk,
};
}
}
exports.BaseDomTranslator = BaseDomTranslator;
/***/ }),
/***/ 1518:
/*!****************************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/dom-translators/BergamotDomTranslator.ts ***!
\****************************************************************************************************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
/* 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/. */
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.BergamotDomTranslator = exports.MAX_REQUESTS = exports.MAX_REQUEST_TEXTS = exports.MAX_REQUEST_DATA = void 0;
const ContentScriptBergamotApiClient_1 = __webpack_require__(/*! ../../../shared-resources/ContentScriptBergamotApiClient */ 449);
const TranslationDocument_1 = __webpack_require__(/*! ../TranslationDocument */ 992);
const BaseDomTranslator_1 = __webpack_require__(/*! ./BaseDomTranslator */ 1435);
const BergamotDomTranslatorRequest_1 = __webpack_require__(/*! ./BergamotDomTranslatorRequest */ 9943);
// The maximum amount of net data allowed per request on Bergamot's API.
exports.MAX_REQUEST_DATA = 500000;
// The maximum number of texts allowed to be translated in a single request.
exports.MAX_REQUEST_TEXTS = 100;
// Self-imposed limit of requests. This means that a page that would need
// to be broken in more than this amount of requests won't be fully translated.
exports.MAX_REQUESTS = 15;
/**
* Translates a webpage using Bergamot's Translation backend.
*/
class BergamotDomTranslator extends BaseDomTranslator_1.BaseDomTranslator {
/**
* @param translationDocument The TranslationDocument object that represents
* the webpage to be translated
* @param sourceLanguage The source language of the document
* @param targetLanguage The target language for the translation
*/
constructor(translationDocument, sourceLanguage, targetLanguage) {
super(translationDocument, sourceLanguage, targetLanguage, new ContentScriptBergamotApiClient_1.ContentScriptBergamotApiClient(), parseChunkResult, { MAX_REQUEST_DATA: exports.MAX_REQUEST_DATA, MAX_REQUEST_TEXTS: exports.MAX_REQUEST_TEXTS, MAX_REQUESTS: exports.MAX_REQUESTS }, (translationRequestData, $sourceLanguage, $targetLanguage) => new BergamotDomTranslatorRequest_1.BergamotDomTranslatorRequest(translationRequestData, $sourceLanguage, $targetLanguage));
}
}
exports.BergamotDomTranslator = BergamotDomTranslator;
/**
* This function parses the result returned by Bergamot's Translation API for
* the translated text in the target language.
*
* @returns boolean True if parsing of this chunk was successful.
*/
function parseChunkResult(translationResponseData, domTranslationChunk) {
const len = translationResponseData.translatedMarkups.length;
if (len === 0) {
throw new Error("Translation response data has no translated strings");
}
if (len !== domTranslationChunk.translationRoots.length) {
// This should never happen, but if the service returns a different number
// of items (from the number of items submitted), we can't use this chunk
// because all items would be paired incorrectly.
throw new Error("Translation response data has a different number of items (from the number of items submitted)");
}
console.info(`Parsing translation chunk result with ${len} translation entries`);
let errorOccurred = false;
domTranslationChunk.translationRoots.forEach((translationRoot, index) => {
try {
const translatedMarkup = translationResponseData.translatedMarkups[index];
translationRoot.parseTranslationResult(translatedMarkup);
if (translationResponseData.qeAnnotatedTranslatedMarkups) {
let qeAnnotatedTranslatedMarkup = translationResponseData.qeAnnotatedTranslatedMarkups[index];
qeAnnotatedTranslatedMarkup = TranslationDocument_1.generateMarkupToTranslateForItem(translationRoot, qeAnnotatedTranslatedMarkup);
translationRoot.parseQeAnnotatedTranslationResult(qeAnnotatedTranslatedMarkup);
}
}
catch (e) {
errorOccurred = true;
console.error("Translation error: ", e);
}
});
console.info(`Parsed translation chunk result with ${len} translation entries`, { errorOccurred });
return !errorOccurred;
}
/***/ }),
/***/ 9943:
/*!***********************************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/dom-translators/BergamotDomTranslatorRequest.ts ***!
\***********************************************************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.BergamotDomTranslatorRequest = void 0;
const detagAndProject_1 = __webpack_require__(/*! ./detagAndProject */ 961);
/**
* Represents a request (for 1 chunk) sent off to Bergamot's translation backend.
*
* @params translationRequestData The data to be used for this translation.
* @param sourceLanguage The source language of the document.
* @param targetLanguage The target language for the translation.
* @param characterCount A counter for tracking the amount of characters translated.
*
*/
class BergamotDomTranslatorRequest {
constructor(translationRequestData, sourceLanguage, targetLanguage) {
this.translationRequestData = translationRequestData;
this.sourceLanguage = sourceLanguage;
this.targetLanguage = targetLanguage;
this.translationRequestData.markupsToTranslate.forEach(text => {
this.characterCount += text.length;
});
}
/**
* Initiates the request
*/
fireRequest(bergamotApiClient, translationRequestProgressCallback) {
return __awaiter(this, void 0, void 0, function* () {
// The server can only deal with pure text, so we detag the strings to
// translate and later project the tags back into the result
const detaggedStrings = this.translationRequestData.markupsToTranslate.map(detagAndProject_1.detag);
const plainStringsToTranslate = detaggedStrings.map(detaggedString => detaggedString.plainString);
const results = yield bergamotApiClient.sendTranslationRequest(plainStringsToTranslate, this.sourceLanguage, this.targetLanguage, translationRequestProgressCallback);
return Object.assign(Object.assign({}, this.parseResults(results, detaggedStrings)), { plainStringsToTranslate });
});
}
parseResults(results, detaggedStrings) {
const len = results.translatedTexts.length;
if (len !== this.translationRequestData.markupsToTranslate.length) {
// This should never happen, but if the service returns a different number
// of items (from the number of items submitted), we can't use this chunk
// because all items would be paired incorrectly.
throw new Error("Translation backend returned a different number of results (from the number of strings to translate)");
}
const translatedMarkups = [];
const translatedPlainTextStrings = [];
const qeAnnotatedTranslatedMarkups = results.qeAnnotatedTranslatedTexts;
// The 'text' field of results is a list of 'Paragraph'. Parse each 'Paragraph' entry
results.translatedTexts.forEach((translatedPlainTextString, index) => {
const detaggedString = detaggedStrings[index];
// Work around issue with doubled periods returned at the end of the translated string
const originalEndedWithASinglePeriod = /([^\.])\.(\s+)?$/gm.exec(detaggedString.plainString);
const translationEndsWithTwoPeriods = /([^\.])\.\.(\s+)?$/gm.exec(translatedPlainTextString);
if (originalEndedWithASinglePeriod && translationEndsWithTwoPeriods) {
translatedPlainTextString = translatedPlainTextString.replace(/([^\.])\.\.(\s+)?$/gm, "$1.$2");
}
let translatedMarkup;
// Use original rather than an empty or obviously invalid translation
// TODO: Address this upstream
if (["", "*", "* ()"].includes(translatedPlainTextString)) {
translatedMarkup = this.translationRequestData.markupsToTranslate[index];
}
else {
// Project original tags/markup onto translated plain text string
// TODO: Use alignment info returned from the translation engine when it becomes available
translatedMarkup = detagAndProject_1.project(detaggedString, translatedPlainTextString);
}
translatedMarkups.push(translatedMarkup);
translatedPlainTextStrings.push(translatedPlainTextString);
});
return {
translatedMarkups,
translatedPlainTextStrings,
qeAnnotatedTranslatedMarkups,
};
}
}
exports.BergamotDomTranslatorRequest = BergamotDomTranslatorRequest;
/***/ }),
/***/ 1975:
/*!***************************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/dom-translators/MinimalDomTranslator.ts ***!
\***************************************************************************************************************/
/***/ (function(__unused_webpack_module, exports) {
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.MinimalDomTranslator = void 0;
class MinimalDomTranslator {
/**
* @param translationDocument The TranslationDocument object that represents
* the webpage to be translated
* @param sourceLanguage The source language of the document
* @param targetLanguage The target language for the translation
*/
constructor(translationDocument, sourceLanguage, targetLanguage) {
this.translationDocument = translationDocument;
this.sourceLanguage = sourceLanguage;
this.targetLanguage = targetLanguage;
}
translate(_translationProgressCallback) {
return __awaiter(this, void 0, void 0, function* () {
return { characterCount: -1 };
});
}
}
exports.MinimalDomTranslator = MinimalDomTranslator;
/***/ }),
/***/ 961:
/*!**********************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/dom-translators/detagAndProject.ts ***!
\**********************************************************************************************************/
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.project = exports.detag = void 0;
const isTagTokenWithImpliedWhitespace = (token) => {
return token.type === "tag" && token.tagName === "br";
};
const detag = (originalString) => {
// console.info("detag", { originalString });
const originalStringDoc = new DOMParser().parseFromString(originalString, "text/html");
// console.debug("originalStringDoc.body", originalStringDoc.body);
const tokens = serializeNodeIntoTokens(originalStringDoc.body);
// console.debug({ tokens });
const plainString = tokens
.map(token => {
if (token.type === "tag") {
return isTagTokenWithImpliedWhitespace(token) ? " " : "";
}
return token.textRepresentation;
})
.join("");
return {
originalString,
tokens,
plainString,
};
};
exports.detag = detag;
function serializeNodeIntoTokens(node) {
const tokens = [];
try {
// @ts-ignore
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
const textChunk = child.nodeValue.trim();
// If this is only whitespace, only add such a token and then visit the next node
if (textChunk === "") {
tokens.push({
type: "whitespace",
textRepresentation: " ",
});
continue;
}
// Otherwise, parse the text content and add whitespace + word tokens as necessary
const leadingSpace = /^\s+/.exec(child.nodeValue);
const trailingSpace = /\s+$/.exec(child.nodeValue);
if (leadingSpace !== null) {
tokens.push({
type: "whitespace",
textRepresentation: leadingSpace[0],
});
}
const words = textChunk.split(" ");
words.forEach((word, wordIndex) => {
// Don't add empty words
if (word !== "") {
tokens.push({
type: "word",
textRepresentation: word,
});
}
// Add whitespace tokens for spaces in between words, eg not after the last word
if (wordIndex !== words.length - 1) {
tokens.push({
type: "whitespace",
textRepresentation: " ",
});
}
});
if (trailingSpace !== null) {
tokens.push({
type: "whitespace",
textRepresentation: trailingSpace[0],
});
}
}
else {
const startTagMatch = /^<[^>]*>/gm.exec(child.outerHTML);
const endTagMatch = /<\/[^>]*>$/gm.exec(child.outerHTML);
const tagName = child.tagName.toLowerCase();
tokens.push({
type: "tag",
tagName,
textRepresentation: startTagMatch[0],
});
const childTokens = serializeNodeIntoTokens(child);
tokens.push(...childTokens);
if (endTagMatch) {
tokens.push({
type: "tag",
tagName,
textRepresentation: endTagMatch[0],
});
}
}
}
}
catch (err) {
console.error(err);
throw err;
}
return tokens;
}
const project = (detaggedString, translatedString) => {
// console.info("project", { detaggedString, translatedString });
// Return the translated string as is if there were no tokens in the original string
if (detaggedString.tokens.filter(token => token.type === "tag").length === 0) {
return translatedString;
}
// If the last token is a tag, we pop it off the token array and re-add it
// last so that the last tag gets any additional translation content injected into it
const lastToken = detaggedString.tokens.slice(-1)[0];
if (lastToken.type === "tag") {
detaggedString.tokens.pop();
}
// Inject the tags naively in the translated string assuming a 1:1
// relationship between original text nodes and words in the translated string
const translatedStringWords = translatedString.split(" ");
const remainingTranslatedStringWords = [...translatedStringWords];
let whitespaceHaveBeenInjectedSinceTheLastWordWasInjected = true;
const projectedStringParts = detaggedString.tokens.map((token) => {
const determineProjectedStringPart = () => {
if (token.type === "word") {
const correspondingTranslatedWord = remainingTranslatedStringWords.shift();
// If we have run out of translated words, don't attempt to add to the projected string
if (correspondingTranslatedWord === undefined) {
return "";
}
// Otherwise, inject the translated word
// ... possibly with a space injected in case none has been injected since the last word
const $projectedStringPart = `${whitespaceHaveBeenInjectedSinceTheLastWordWasInjected ? "" : " "}${correspondingTranslatedWord}`;
whitespaceHaveBeenInjectedSinceTheLastWordWasInjected = false;
return $projectedStringPart;
}
else if (token.type === "whitespace") {
// Don't pad whitespace onto each other when there are no more words
if (remainingTranslatedStringWords.length === 0) {
return "";
}
whitespaceHaveBeenInjectedSinceTheLastWordWasInjected = true;
return token.textRepresentation;
}
else if (token.type === "tag") {
if (isTagTokenWithImpliedWhitespace(token)) {
whitespaceHaveBeenInjectedSinceTheLastWordWasInjected = true;
}
return token.textRepresentation;
}
throw new Error(`Unexpected token type: ${token.type}`);
};
const projectedStringPart = determineProjectedStringPart();
return projectedStringPart;
});
let projectedString = projectedStringParts.join("");
// Add any remaining translated words to the end
if (remainingTranslatedStringWords.length) {
// Add a whitespace to the end first in case there was none, or else two words will be joined together
if (lastToken.type !== "whitespace") {
projectedString += " ";
}
projectedString += remainingTranslatedStringWords.join(" ");
}
// If the last token is a tag, see above
if (lastToken.type === "tag") {
projectedString += lastToken.textRepresentation;
}
// console.debug({translatedStringWords, projectedString});
return projectedString;
};
exports.project = project;
/***/ }),
/***/ 5173:
/*!**********************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/getTranslationNodes.ts ***!
\**********************************************************************************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
/* 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/. */
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getTranslationNodes = void 0;
const hasTextForTranslation_1 = __webpack_require__(/*! ./hasTextForTranslation */ 4874);
const isBlockFrameOrSubclass = (element) => {
// TODO: Make this generalize like the corresponding C code invoked by:
/*
nsIFrame* frame = childElement->GetPrimaryFrame();
frame->IsBlockFrameOrSubclass();
*/
const nodeTagName = element.tagName.toLowerCase();
const blockLevelElementTagNames = [
"address",
"article",
"aside",
"blockquote",
"canvas",
"dd",
"div",
"dl",
"dt",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"li",
"main",
"nav",
"noscript",
"ol",
"p",
"pre",
"section",
"table",
"tfoot",
"ul",
"video",
];
const result = (blockLevelElementTagNames.includes(nodeTagName) ||
element.style.display === "block") &&
element.style.display !== "inline";
return result;
};
const getTranslationNodes = (rootElement, seenTranslationNodes = [], limit = 15000) => {
const translationNodes = [];
// Query child elements in order to explicitly skip the root element from being classified as a translation node
const childElements = rootElement.children;
for (let i = 0; i < limit && i < childElements.length; i++) {
const childElement = childElements[i];
const tagName = childElement.tagName.toLowerCase();
const isTextNode = childElement.nodeType === Node.TEXT_NODE;
if (isTextNode) {
console.warn(`We are not supposed to run into text nodes here. childElement.textContent: "${childElement.textContent}"`);
continue;
}
// Skip elements that usually contain non-translatable text content.
if ([
"script",
"iframe",
"frameset",
"frame",
"code",
"noscript",
"style",
"svg",
"math",
].includes(tagName)) {
continue;
}
const nodeHasTextForTranslation = hasTextForTranslation_1.hasTextForTranslation(childElement.textContent);
// Only empty or non-translatable content in this part of the tree
if (!nodeHasTextForTranslation) {
continue;
}
// An element is a translation node if it contains
// at least one text node that has meaningful data
// for translation
const childChildTextNodes = Array.from(childElement.childNodes).filter((childChildNode) => childChildNode.nodeType === Node.TEXT_NODE);
const childChildTextNodesWithTextForTranslation = childChildTextNodes
.map(textNode => textNode.textContent)
.filter(hasTextForTranslation_1.hasTextForTranslation);
const isTranslationNode = !!childChildTextNodesWithTextForTranslation.length;
if (isTranslationNode) {
// At this point, we know we have a translation node at hand, but we need
// to figure out it the node is a translation root or not
let isTranslationRoot;
// Block elements are translation roots
isTranslationRoot = isBlockFrameOrSubclass(childElement);
// If an element is not a block element, it still
// can be considered a translation root if all ancestors
// of this element are not translation nodes
seenTranslationNodes.push(childElement);
if (!isTranslationRoot) {
// Walk up tree and check if an ancestor was a translation node
let ancestorWasATranslationNode = false;
for (let ancestor = childElement.parentNode; ancestor; ancestor = ancestor.parentNode) {
if (seenTranslationNodes.includes(ancestor)) {
ancestorWasATranslationNode = true;
break;
}
}
isTranslationRoot = !ancestorWasATranslationNode;
}
const translationNode = {
content: childElement,
isTranslationRoot,
};
translationNodes.push(translationNode);
}
// Now traverse any element children to find nested translation nodes
if (childElement.firstElementChild) {
const childTranslationNodes = exports.getTranslationNodes(childElement, seenTranslationNodes, limit - translationNodes.length);
translationNodes.push(...childTranslationNodes);
}
}
return translationNodes;
};
exports.getTranslationNodes = getTranslationNodes;
/***/ }),
/***/ 4874:
/*!************************************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/hasTextForTranslation.ts ***!
\************************************************************************************************/
/***/ ((__unused_webpack_module, exports) => {
/* 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/. */
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.hasTextForTranslation = void 0;
const hasTextForTranslation = text => {
const trimmed = text.trim();
if (trimmed === "") {
return false;
}
/* tslint:disable:no-empty-character-class */
// https://github.com/buzinas/tslint-eslint-rules/issues/289
return /\p{L}/gu.test(trimmed);
};
exports.hasTextForTranslation = hasTextForTranslation;
/***/ }),
/***/ 5543:
/*!********************************************************************************!*\
!*** ./src/core/ts/content-scripts/dom-translation-content-script.js/index.ts ***!
\********************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const mobx_keystone_1 = __webpack_require__(/*! mobx-keystone */ 7680);
const DomTranslationManager_1 = __webpack_require__(/*! ./DomTranslationManager */ 9023);
const subscribeToExtensionState_1 = __webpack_require__(/*! ../../shared-resources/state-management/subscribeToExtensionState */ 6523);
const DocumentTranslationStateCommunicator_1 = __webpack_require__(/*! ../../shared-resources/state-management/DocumentTranslationStateCommunicator */ 2187);
const ContentScriptFrameInfo_1 = __webpack_require__(/*! ../../shared-resources/ContentScriptFrameInfo */ 9181);
const ExtensionState_1 = __webpack_require__(/*! ../../shared-resources/models/ExtensionState */ 65);
const BaseTranslationState_1 = __webpack_require__(/*! ../../shared-resources/models/BaseTranslationState */ 4779);
const TranslateOwnTextTranslationState_1 = __webpack_require__(/*! ../../shared-resources/models/TranslateOwnTextTranslationState */ 8238);
const DocumentTranslationState_1 = __webpack_require__(/*! ../../shared-resources/models/DocumentTranslationState */ 5482);
// Workaround for https://github.com/xaviergonz/mobx-keystone/issues/183
// We need to import some models explicitly lest they fail to be registered by mobx
new ExtensionState_1.ExtensionState({});
new TranslateOwnTextTranslationState_1.TranslateOwnTextTranslationState({});
/**
* Note this content script runs at "document_idle" ie after the DOM is complete
*/
const init = () => __awaiter(void 0, void 0, void 0, function* () {
/*
await initErrorReportingInContentScript(
"port-from-dom-translation-content-script:index",
);
*/
const contentScriptFrameInfo = new ContentScriptFrameInfo_1.ContentScriptFrameInfo();
// Get window, tab and frame id
const frameInfo = yield contentScriptFrameInfo.getCurrentFrameInfo();
const tabFrameReference = `${frameInfo.tabId}-${frameInfo.frameId}`;
const extensionState = yield subscribeToExtensionState_1.subscribeToExtensionState();
const documentTranslationStateCommunicator = new DocumentTranslationStateCommunicator_1.DocumentTranslationStateCommunicator(frameInfo, extensionState);
const domTranslationManager = new DomTranslationManager_1.DomTranslationManager(documentTranslationStateCommunicator, document, window);
const documentTranslationStatistics = yield domTranslationManager.getDocumentTranslationStatistics();
console.log({ documentTranslationStatistics });
// TODO: Prevent multiple translations from occurring simultaneously + enable cancellations of existing translation jobs
// Any subsequent actions are determined by document translation state changes
mobx_keystone_1.onSnapshot(extensionState.$.documentTranslationStates, (documentTranslationStates, previousDocumentTranslationStates) => __awaiter(void 0, void 0, void 0, function* () {
// console.debug("dom-translation-content-script.js - documentTranslationStates snapshot HAS CHANGED", {documentTranslationStates});
var _a, _b;
const currentTabFrameDocumentTranslationState = documentTranslationStates[tabFrameReference];
const previousTabFrameDocumentTranslationState = previousDocumentTranslationStates[tabFrameReference];
// console.log({ currentTabFrameDocumentTranslationState });
// TODO: Possibly react to no current state in some other way
if (!currentTabFrameDocumentTranslationState) {
return;
}
const hasChanged = property => {
return (!previousTabFrameDocumentTranslationState ||
currentTabFrameDocumentTranslationState[property] !==
previousTabFrameDocumentTranslationState[property]);
};
if (hasChanged("translationRequested")) {
if (currentTabFrameDocumentTranslationState.translationRequested) {
/* TODO: Do not translate if already translated
if (
domTranslationManager?.contentWindow?.translationDocument &&
currentTabFrameDocumentTranslationState.translateFrom !==
domTranslationManager.contentWindow.translationDocument.sourceLanguage
) {
*/
const translationPromise = domTranslationManager.doTranslation(currentTabFrameDocumentTranslationState.translateFrom, currentTabFrameDocumentTranslationState.translateTo);
extensionState.patchDocumentTranslationStateByFrameInfo(frameInfo, [
{
op: "replace",
path: ["translationRequested"],
value: false,
},
]);
yield translationPromise;
}
}
if (hasChanged("translationStatus")) {
if (currentTabFrameDocumentTranslationState.translationStatus ===
BaseTranslationState_1.TranslationStatus.UNKNOWN) {
yield domTranslationManager.attemptToDetectLanguage();
}
if (currentTabFrameDocumentTranslationState.translationStatus ===
BaseTranslationState_1.TranslationStatus.TRANSLATING) {
if (currentTabFrameDocumentTranslationState.cancellationRequested) {
console.debug("Cancellation requested");
console.debug("TODO: Implement");
}
}
}
if ((_a = domTranslationManager === null || domTranslationManager === void 0 ? void 0 : domTranslationManager.contentWindow) === null || _a === void 0 ? void 0 : _a.translationDocument) {
const translationDocument = (_b = domTranslationManager === null || domTranslationManager === void 0 ? void 0 : domTranslationManager.contentWindow) === null || _b === void 0 ? void 0 : _b.translationDocument;
if (hasChanged("showOriginal")) {
if (currentTabFrameDocumentTranslationState.showOriginal !==
translationDocument.originalShown) {
if (translationDocument.originalShown) {
translationDocument.showTranslation();
}
else {
translationDocument.showOriginal();
}
}
}
if (hasChanged("displayQualityEstimation")) {
if (currentTabFrameDocumentTranslationState.displayQualityEstimation !==
translationDocument.qualityEstimationShown) {
if (translationDocument.qualityEstimationShown) {
translationDocument.showTranslation();
}
else {
translationDocument.showQualityEstimation();
}
}
}
}
}));
// Add an initial document translation state
try {
extensionState.setDocumentTranslationState(new DocumentTranslationState_1.DocumentTranslationState(Object.assign(Object.assign({}, frameInfo), { translationStatus: BaseTranslationState_1.TranslationStatus.UNKNOWN, url: window.location.href, wordCount: documentTranslationStatistics.wordCount, wordCountVisible: documentTranslationStatistics.wordCountVisible, wordCountVisibleInViewport: documentTranslationStatistics.wordCountVisibleInViewport })));
// Schedule removal of this document translation state when the document is closed
const onBeforeunloadEventListener = function (e) {
extensionState.deleteDocumentTranslationStateByFrameInfo(frameInfo);
// the absence of a returnValue property on the event will guarantee the browser unload happens
delete e.returnValue;
// balanced-listeners
window.removeEventListener("beforeunload", onBeforeunloadEventListener);
};
window.addEventListener("beforeunload", onBeforeunloadEventListener);
}
catch (err) {
console.error("Instantiate DocumentTranslationState error", err);
}
});
// noinspection JSIgnoredPromiseFromCall
init();
/***/ }),
/***/ 449:
/*!************************************************************************!*\
!*** ./src/core/ts/shared-resources/ContentScriptBergamotApiClient.ts ***!
\************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ContentScriptBergamotApiClient = void 0;
const webextension_polyfill_ts_1 = __webpack_require__(/*! webextension-polyfill-ts */ 3624);
const nanoid_1 = __webpack_require__(/*! nanoid */ 350);
const ErrorReporting_1 = __webpack_require__(/*! ./ErrorReporting */ 3345);
class ContentScriptBergamotApiClient {
constructor() {
// console.debug("ContentScriptBergamotApiClient: Connecting to the background script");
this.backgroundContextPort = webextension_polyfill_ts_1.browser.runtime.connect(webextension_polyfill_ts_1.browser.runtime.id, {
name: "port-from-content-script-bergamot-api-client",
});
}
sendTranslationRequest(texts, from, to, translationRequestProgressCallback) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const requestId = nanoid_1.nanoid();
const resultsMessageListener = (m) => __awaiter(this, void 0, void 0, function* () {
if (m.translationRequestUpdate) {
const { translationRequestUpdate } = m;
if (translationRequestUpdate.requestId !== requestId) {
return;
}
// console.debug("ContentScriptBergamotApiClient received translationRequestUpdate", { translationRequestUpdate });
const { results, translationRequestProgress, error, } = translationRequestUpdate;
if (translationRequestProgress) {
translationRequestProgressCallback(translationRequestProgress);
return;
}
if (results) {
this.backgroundContextPort.onMessage.removeListener(resultsMessageListener);
resolve(translationRequestUpdate.results);
return;
}
if (error) {
this.backgroundContextPort.onMessage.removeListener(resultsMessageListener);
reject(error);
return;
}
}
ErrorReporting_1.captureExceptionWithExtras(new Error("Unexpected message structure"), {
m,
});
console.error("Unexpected message structure", { m });
reject({ m });
});
this.backgroundContextPort.onMessage.addListener(resultsMessageListener);
// console.debug("ContentScriptBergamotApiClient: Sending translation request", {texts});
this.backgroundContextPort.postMessage({
texts,
from,
to,
requestId,
});
});
});
}
}
exports.ContentScriptBergamotApiClient = ContentScriptBergamotApiClient;
/***/ }),
/***/ 9181:
/*!****************************************************************!*\
!*** ./src/core/ts/shared-resources/ContentScriptFrameInfo.ts ***!
\****************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ContentScriptFrameInfo = void 0;
const webextension_polyfill_ts_1 = __webpack_require__(/*! webextension-polyfill-ts */ 3624);
const ErrorReporting_1 = __webpack_require__(/*! ./ErrorReporting */ 3345);
const nanoid_1 = __webpack_require__(/*! nanoid */ 350);
class ContentScriptFrameInfo {
constructor() {
// console.debug("ContentScriptFrameInfo: Connecting to the background script");
this.backgroundContextPort = webextension_polyfill_ts_1.browser.runtime.connect(webextension_polyfill_ts_1.browser.runtime.id, {
name: "port-from-content-script-frame-info",
});
}
getCurrentFrameInfo() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const requestId = nanoid_1.nanoid();
const resultsMessageListener = (m) => __awaiter(this, void 0, void 0, function* () {
if (m.frameInfo) {
const { frameInfo } = m;
if (m.requestId !== requestId) {
return;
}
// console.debug("ContentScriptFrameInfo received results", {frameInfo});
this.backgroundContextPort.onMessage.removeListener(resultsMessageListener);
resolve(frameInfo);
return;
}
ErrorReporting_1.captureExceptionWithExtras(new Error("Unexpected message"), { m });
console.error("Unexpected message", { m });
reject({ m });
});
this.backgroundContextPort.onMessage.addListener(resultsMessageListener);
this.backgroundContextPort.postMessage({
requestId,
});
});
});
}
}
exports.ContentScriptFrameInfo = ContentScriptFrameInfo;
/***/ }),
/***/ 6336:
/*!****************************************************************************!*\
!*** ./src/core/ts/shared-resources/ContentScriptLanguageDetectorProxy.ts ***!
\****************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ContentScriptLanguageDetectorProxy = void 0;
const webextension_polyfill_ts_1 = __webpack_require__(/*! webextension-polyfill-ts */ 3624);
const ErrorReporting_1 = __webpack_require__(/*! ./ErrorReporting */ 3345);
const nanoid_1 = __webpack_require__(/*! nanoid */ 350);
class ContentScriptLanguageDetectorProxy {
constructor() {
// console.debug("ContentScriptLanguageDetectorProxy: Connecting to the background script");
this.backgroundContextPort = webextension_polyfill_ts_1.browser.runtime.connect(webextension_polyfill_ts_1.browser.runtime.id, {
name: "port-from-content-script-language-detector-proxy",
});
}
detectLanguage(str) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const requestId = nanoid_1.nanoid();
const resultsMessageListener = (m) => __awaiter(this, void 0, void 0, function* () {
if (m.languageDetectorResults) {
const { languageDetectorResults } = m;
if (languageDetectorResults.requestId !== requestId) {
return;
}
// console.debug("ContentScriptLanguageDetectorProxy received language detector results", {languageDetectorResults});
this.backgroundContextPort.onMessage.removeListener(resultsMessageListener);
resolve(languageDetectorResults.results);
return;
}
ErrorReporting_1.captureExceptionWithExtras(new Error("Unexpected message"), { m });
console.error("Unexpected message", { m });
reject({ m });
});
this.backgroundContextPort.onMessage.addListener(resultsMessageListener);
// console.debug("Attempting detectLanguage via content script proxy", {str});
this.backgroundContextPort.postMessage({
str,
requestId,
});
});
});
}
}
exports.ContentScriptLanguageDetectorProxy = ContentScriptLanguageDetectorProxy;
/***/ }),
/***/ 2187:
/*!***********************************************************************************************!*\
!*** ./src/core/ts/shared-resources/state-management/DocumentTranslationStateCommunicator.ts ***!
\***********************************************************************************************/
/***/ ((__unused_webpack_module, exports) => {
/* 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/. */
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DocumentTranslationStateCommunicator = void 0;
/**
* Helper class to communicate updated document translation states.
*
* State patching code is wrapped in setTimeout to prevent
* automatic (by mobx) batching of updates which leads to much less frequent
* state updates communicated to subscribers. No state updates during a translation
* session is not useful since we want to communicate the translation progress)
*/
class DocumentTranslationStateCommunicator {
constructor(frameInfo, extensionState) {
this.frameInfo = frameInfo;
this.extensionState = extensionState;
}
patchDocumentTranslationState(patches) {
setTimeout(() => {
this.extensionState.patchDocumentTranslationStateByFrameInfo(this.frameInfo, patches);
}, 0);
}
broadcastUpdatedAttributeValue(attribute, value) {
this.patchDocumentTranslationState([
{
op: "replace",
path: [attribute],
value,
},
]);
}
broadcastUpdatedTranslationStatus(translationStatus) {
this.broadcastUpdatedAttributeValue("translationStatus", translationStatus);
}
/**
* This method was chosen as the place to sum up the progress of individual translation
* requests into the similar translation progress attributes present at the frame level
* in document translation state objects (totalTranslationWallTimeMs, totalTranslationEngineRequestCount etc).
*
* Another natural place to do this conversion would be as computed properties in the mobx models
* but it proved problematic to maintain/patch/sync map attributes (such as progressOfIndividualTranslationRequests)
* in document translation state objects, so reduction to simpler attributes is done here instead.
*
* @param frameTranslationProgress
*/
broadcastUpdatedFrameTranslationProgress(frameTranslationProgress) {
const { progressOfIndividualTranslationRequests, } = frameTranslationProgress;
const translationRequestProgressEntries = Array.from(progressOfIndividualTranslationRequests).map(([, translationRequestProgress]) => translationRequestProgress);
const translationInitiationTimestamps = translationRequestProgressEntries.map((trp) => trp.initiationTimestamp);
const translationInitiationTimestamp = Math.min(...translationInitiationTimestamps);
const totalModelLoadWallTimeMs = translationRequestProgressEntries
.map((trp) => trp.modelLoadWallTimeMs || 0)
.reduce((a, b) => a + b, 0);
const totalTranslationWallTimeMs = translationRequestProgressEntries
.map((trp) => trp.translationWallTimeMs || 0)
.reduce((a, b) => a + b, 0);
const totalTranslationEngineRequestCount = translationRequestProgressEntries.length;
const queuedTranslationEngineRequestCount = translationRequestProgressEntries.filter((trp) => trp.queued).length;
// Merge translation-progress-related booleans
const modelLoadNecessary = !!translationRequestProgressEntries.filter((trp) => trp.modelLoadNecessary).length;
const modelDownloadNecessary = !!translationRequestProgressEntries.filter((trp) => trp.modelDownloadNecessary).length;
const modelDownloading = !!translationRequestProgressEntries.filter((trp) => trp.modelDownloading).length;
const modelLoading = modelLoadNecessary
? !!translationRequestProgressEntries.find((trp) => trp.modelLoading)
: undefined;
const modelLoaded = modelLoadNecessary
? !!translationRequestProgressEntries
.filter((trp) => trp.modelLoadNecessary)
.find((trp) => trp.modelLoaded)
: undefined;
const translationFinished = translationRequestProgressEntries.filter((trp) => !trp.translationFinished).length === 0;
// Merge model download progress
const emptyDownloadProgress = {
bytesDownloaded: 0,
bytesToDownload: 0,
startTs: undefined,
durationMs: 0,
endTs: undefined,
};
const modelDownloadProgress = translationRequestProgressEntries
.map((trp) => trp.modelDownloadProgress)
.filter((mdp) => mdp)
.reduce((a, b) => {
const startTs = a.startTs && a.startTs <= b.startTs ? a.startTs : b.startTs;
const endTs = a.endTs && a.endTs >= b.endTs ? a.endTs : b.endTs;
return {
bytesDownloaded: a.bytesDownloaded + b.bytesDownloaded,
bytesToDownload: a.bytesToDownload + b.bytesToDownload,
startTs,
durationMs: endTs ? endTs - startTs : Date.now() - startTs,
endTs,
};
}, emptyDownloadProgress);
this.patchDocumentTranslationState([
{
op: "replace",
path: ["translationInitiationTimestamp"],
value: translationInitiationTimestamp,
},
{
op: "replace",
path: ["totalModelLoadWallTimeMs"],
value: totalModelLoadWallTimeMs,
},
{
op: "replace",
path: ["modelDownloadNecessary"],
value: modelDownloadNecessary,
},
{
op: "replace",
path: ["modelDownloading"],
value: modelDownloading,
},
{
op: "replace",
path: ["modelDownloadProgress"],
value: modelDownloadProgress,
},
{
op: "replace",
path: ["totalTranslationWallTimeMs"],
value: totalTranslationWallTimeMs,
},
{
op: "replace",
path: ["totalTranslationEngineRequestCount"],
value: totalTranslationEngineRequestCount,
},
{
op: "replace",
path: ["queuedTranslationEngineRequestCount"],
value: queuedTranslationEngineRequestCount,
},
{
op: "replace",
path: ["modelLoadNecessary"],
value: modelLoadNecessary,
},
{
op: "replace",
path: ["modelLoading"],
value: modelLoading,
},
{
op: "replace",
path: ["modelLoaded"],
value: modelLoaded,
},
{
op: "replace",
path: ["translationFinished"],
value: translationFinished,
},
]);
}
clear() {
setTimeout(() => {
this.extensionState.deleteDocumentTranslationStateByFrameInfo(this.frameInfo);
}, 0);
}
updatedDetectedLanguageResults(detectedLanguageResults) {
this.broadcastUpdatedAttributeValue("detectedLanguageResults", detectedLanguageResults);
}
}
exports.DocumentTranslationStateCommunicator = DocumentTranslationStateCommunicator;
/***/ }),
/***/ 6523:
/*!************************************************************************************!*\
!*** ./src/core/ts/shared-resources/state-management/subscribeToExtensionState.ts ***!
\************************************************************************************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
/* 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/. */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.subscribeToExtensionState = void 0;
const mobx_keystone_1 = __webpack_require__(/*! mobx-keystone */ 7680);
const webextension_polyfill_ts_1 = __webpack_require__(/*! webextension-polyfill-ts */ 3624);
const ErrorReporting_1 = __webpack_require__(/*! ../ErrorReporting */ 3345);
const nanoid_1 = __webpack_require__(/*! nanoid */ 350);
// disable runtime data checking (we rely on TypeScript at compile time so that our model definitions can be cleaner)
mobx_keystone_1.setGlobalConfig({
modelAutoTypeChecking: mobx_keystone_1.ModelAutoTypeCheckingMode.AlwaysOff,
});
class MobxKeystoneProxy {
constructor(msgListeners) {
this.msgListeners = [];
this.msgListeners = msgListeners;
// console.debug("MobxKeystoneProxy: Connecting to the background script");
this.backgroundContextPort = webextension_polyfill_ts_1.browser.runtime.connect(webextension_polyfill_ts_1.browser.runtime.id, {
name: "port-from-mobx-keystone-proxy",
});
// listen to updates from host
const actionCallResultsMessageListener = (m) => __awaiter(this, void 0, void 0, function* () {
if (m.serializedActionCallToReplicate) {
const { serializedActionCallToReplicate } = m;
// console.log("MobxKeystoneProxy received applyActionResult", {serializedActionCallToReplicate});
this.msgListeners.forEach(listener => listener(serializedActionCallToReplicate));
return;
}
if (m.initialState) {
// handled in another listener, do nothing here
return;
}
ErrorReporting_1.captureExceptionWithExtras(new Error("Unexpected message"), { m });
console.error("Unexpected message", { m });
});
this.backgroundContextPort.onMessage.addListener(actionCallResultsMessageListener);
}
requestInitialState() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const requestId = nanoid_1.nanoid();
const resultsMessageListener = (m) => __awaiter(this, void 0, void 0, function* () {
if (m.initialState) {
const { initialState } = m;
if (m.requestId !== requestId) {
return;
}
// console.debug("MobxKeystoneProxy received initialState", {initialState});
this.backgroundContextPort.onMessage.removeListener(resultsMessageListener);
resolve(initialState);
return;
}
if (m.serializedActionCallToReplicate) {
// handled in another listener, do nothing here
return;
}
ErrorReporting_1.captureExceptionWithExtras(new Error("Unexpected message"), { m });
console.error("Unexpected message", { m });
reject({ m });
});
this.backgroundContextPort.onMessage.addListener(resultsMessageListener);
// console.debug("requestInitialState via content script mobx keystone proxy", {});
this.backgroundContextPort.postMessage({
requestInitialState: true,
requestId,
});
});
});
}
actionCall(actionCall) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, _reject) => {
const requestId = nanoid_1.nanoid();
// console.debug("MobxKeystoneProxy (Content Script Context): actionCall via content script mobx keystone proxy", { actionCall });
this.backgroundContextPort.postMessage({
actionCall,
requestId,
});
resolve();
});
});
}
}
class BackgroundContextCommunicator {
constructor() {
this.msgListeners = [];
this.mobxKeystoneProxy = new MobxKeystoneProxy(this.msgListeners);
}
requestInitialState() {
return __awaiter(this, void 0, void 0, function* () {
return this.mobxKeystoneProxy.requestInitialState();
});
}
onMessage(listener) {
this.msgListeners.push(listener);
}
sendMessage(actionCall) {
// send the action to be taken to the host
this.mobxKeystoneProxy.actionCall(actionCall);
}
}
const server = new BackgroundContextCommunicator();
function subscribeToExtensionState() {
return __awaiter(this, void 0, void 0, function* () {
// we get the snapshot from the server, which is a serializable object
const rootStoreSnapshot = yield server.requestInitialState();
// and hydrate it into a proper object
const rootStore = mobx_keystone_1.fromSnapshot(rootStoreSnapshot);
let serverAction = false;
const runServerActionLocally = (actionCall) => {
const wasServerAction = serverAction;
serverAction = true;
try {
// in clients we use the sync new model ids version to make sure that
// any model ids that were generated in the server side end up being
// the same in the client side
mobx_keystone_1.applySerializedActionAndSyncNewModelIds(rootStore, actionCall);
}
finally {
serverAction = wasServerAction;
}
};
// listen to action messages to be replicated into the local root store
server.onMessage(actionCall => {
runServerActionLocally(actionCall);
});
// also listen to local actions, cancel them and send them to the server (background context)
mobx_keystone_1.onActionMiddleware(rootStore, {
onStart(actionCall, ctx) {
if (!serverAction) {
// if the action does not come from the server (background context) cancel it silently
// and send it to the server (background context)
// it will then be replicated by the server (background context) and properly executed
server.sendMessage(mobx_keystone_1.serializeActionCall(actionCall, rootStore));
ctx.data.cancelled = true; // just for logging purposes
// "cancel" the action by returning undefined
return {
result: mobx_keystone_1.ActionTrackingResult.Return,
value: undefined,
};
}
// run actions that comes from the server (background context) unmodified
/* eslint-disable consistent-return */
return undefined;
/* eslint-enable consistent-return */
},
});
// recommended by mobx-keystone (allows the model hook `onAttachedToRootStore` to work)
mobx_keystone_1.registerRootStore(rootStore);
return rootStore;
});
}
exports.subscribeToExtensionState = subscribeToExtensionState;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(__webpack_module_cache__[moduleId]) {
/******/ return __webpack_module_cache__[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/******/ // the startup function
/******/ // It's empty as some runtime module handles the default behavior
/******/ __webpack_require__.x = x => {};
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/harmony module decorator */
/******/ (() => {
/******/ __webpack_require__.hmd = (module) => {
/******/ module = Object.create(module);
/******/ if (!module.children) module.children = [];
/******/ Object.defineProperty(module, 'exports', {
/******/ enumerable: true,
/******/ set: () => {
/******/ throw new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);
/******/ }
/******/ });
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ 840: 0
/******/ };
/******/
/******/ var deferredModules = [
/******/ [5543,351]
/******/ ];
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ var checkDeferredModules = x => {};
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime, executeModules] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) runtime(__webpack_require__);
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ if(executeModules) deferredModules.push.apply(deferredModules, executeModules);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkbergamot_browser_extension"] = self["webpackChunkbergamot_browser_extension"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/
/******/ function checkDeferredModulesImpl() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
/******/ var depId = deferredModule[j];
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/ if(deferredModules.length === 0) {
/******/ __webpack_require__.x();
/******/ __webpack_require__.x = x => {};
/******/ }
/******/ return result;
/******/ }
/******/ var startup = __webpack_require__.x;
/******/ __webpack_require__.x = () => {
/******/ // reset startup function so it can be called again when more startup code is added
/******/ __webpack_require__.x = startup || (x => {});
/******/ return (checkDeferredModules = checkDeferredModulesImpl)();
/******/ };
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // run startup
/******/ var __webpack_exports__ = __webpack_require__.x();
/******/
/******/ })()
;
//# sourceMappingURL=dom-translation-content-script.js.map