//
// Node now looks like:
Hello [ ][ world.]
// 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