// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This code is used in conjunction with the Google Translate Element script.
// It is executed in an isolated world of a page to translate it from one
// language to another.
// It should be included in the page before the Translate Element script.
// eslint-disable-next-line no-var
var cr = cr || {};
/**
* An object to provide functions to interact with the Translate library.
* @type {object}
*/
cr.googleTranslate = (function() {
/**
* The Translate Element library's instance.
* @type {object}
*/
let lib;
/**
* A flag representing if the Translate Element library is initialized.
* @type {boolean}
*/
let libReady = false;
/**
* Error definitions for |errorCode|. See chrome/common/translate_errors.h
* to modify the definition.
* @const
*/
const ERROR = {
'NONE': 0,
'INITIALIZATION_ERROR': 2,
'UNSUPPORTED_LANGUAGE': 4,
'TRANSLATION_ERROR': 6,
'TRANSLATION_TIMEOUT': 7,
'UNEXPECTED_SCRIPT_ERROR': 8,
'BAD_ORIGIN': 9,
'SCRIPT_LOAD_ERROR': 10,
};
/**
* Error code map from te.dom.DomTranslator.Error to |errorCode|.
* See also go/dom_translator.js in google3.
* @const
*/
const TRANSLATE_ERROR_TO_ERROR_CODE_MAP = {
0: ERROR['NONE'],
1: ERROR['TRANSLATION_ERROR'],
2: ERROR['UNSUPPORTED_LANGUAGE'],
};
/**
* An error code happened in translate.js and the Translate Element library.
*/
let errorCode = ERROR['NONE'];
/**
* A flag representing if the Translate Element has finished a translation.
* @type {boolean}
*/
let finished = false;
/**
* Counts how many times the checkLibReady function is called. The function
* is called in every 100 msec and counted up to 6.
* @type {number}
*/
let checkReadyCount = 0;
/**
* Time in msec when this script is injected.
* @type {number}
*/
const injectedTime = performance.now();
/**
* Time in msec when the Translate Element library is loaded completely.
* @type {number}
*/
let loadedTime = 0.0;
/**
* Time in msec when the Translate Element library is initialized and ready
* for performing translation.
* @type {number}
*/
let readyTime = 0.0;
/**
* Time in msec when the Translate Element library starts a translation.
* @type {number}
*/
let startTime = 0.0;
/**
* Time in msec when the Translate Element library ends a translation.
* @type {number}
*/
let endTime = 0.0;
/**
* Callback invoked when Translate Element's ready state is known.
* Will only be invoked once to indicate successful or failed initialization.
* In the failure case, errorCode() and error() will indicate the reason.
* Only used on iOS.
* @type {function}
*/
let readyCallback;
/**
* Callback invoked when Translate Element's translation result is known.
* Will only be invoked once to indicate successful or failed translation.
* In the failure case, errorCode() and error() will indicate the reason.
* Only used on iOS.
* @type {function}
*/
let resultCallback;
function checkLibReady() {
if (lib.isAvailable()) {
readyTime = performance.now();
libReady = true;
invokeReadyCallback();
return;
}
if (checkReadyCount++ > 5) {
errorCode = ERROR['TRANSLATION_TIMEOUT'];
invokeReadyCallback();
return;
}
setTimeout(checkLibReady, 100);
}
function onTranslateProgress(progress, opt_finished, opt_error) {
finished = opt_finished;
// opt_error can be 'undefined'.
if (typeof opt_error === 'boolean' && opt_error) {
// TODO(toyoshim): Remove boolean case once a server is updated.
errorCode = ERROR['TRANSLATION_ERROR'];
// We failed to translate, restore so the page is in a consistent state.
lib.restore();
invokeResultCallback();
} else if (typeof opt_error === 'number' && opt_error !== 0) {
errorCode = TRANSLATE_ERROR_TO_ERROR_CODE_MAP[opt_error];
lib.restore();
invokeResultCallback();
}
// Translate works differently depending on the prescence of the native
// IntersectionObserver APIs.
// If it is available, translate will occur incrementally as the user
// scrolls elements into view, and this method will be called continuously
// with |opt_finished| always set as true.
// On the other hand, if it is unavailable, the entire page will be
// translated at once in a piece meal manner, and this method may still be
// called several times, though only the last call will have |opt_finished|
// set as true.
if (finished) {
endTime = performance.now();
invokeResultCallback();
}
}
function invokeReadyCallback() {
if (readyCallback) {
readyCallback();
readyCallback = null;
}
}
function invokeResultCallback() {
if (resultCallback) {
resultCallback();
resultCallback = null;
}
}
window.addEventListener('pagehide', function(event) {
if (libReady && event.persisted) {
lib.restore();
}
});
// Public API.
return {
/**
* Setter for readyCallback. No op if already set.
* @param {function} callback The function to be invoked.
*/
set readyCallback(callback) {
if (!readyCallback) {
readyCallback = callback;
}
},
/**
* Setter for resultCallback. No op if already set.
* @param {function} callback The function to be invoked.
*/
set resultCallback(callback) {
if (!resultCallback) {
resultCallback = callback;
}
},
/**
* Whether the library is ready.
* The translate function should only be called when |libReady| is true.
* @type {boolean}
*/
get libReady() {
return libReady;
},
/**
* Whether the current translate has finished successfully.
* @type {boolean}
*/
get finished() {
return finished;
},
/**
* Whether an error occured initializing the library of translating the
* page.
* @type {boolean}
*/
get error() {
return errorCode !== ERROR['NONE'];
},
/**
* Returns a number to represent error type.
* @type {number}
*/
get errorCode() {
return errorCode;
},
/**
* The language the page translated was in. Is valid only after the page
* has been successfully translated and the original language specified to
* the translate function was 'auto'. Is empty otherwise.
* Some versions of Element library don't provide |getDetectedLanguage|
* function. In that case, this function returns 'und'.
* @type {boolean}
*/
get sourceLang() {
if (!libReady || !finished || errorCode !== ERROR['NONE']) {
return '';
}
if (!lib.getDetectedLanguage) {
return 'und';
} // Defined as translate::kUnknownLanguageCode in C++.
return lib.getDetectedLanguage();
},
/**
* Time in msec from this script being injected to all server side scripts
* being loaded.
* @type {number}
*/
get loadTime() {
if (loadedTime === 0) {
return 0;
}
return loadedTime - injectedTime;
},
/**
* Time in msec from this script being injected to the Translate Element
* library being ready.
* @type {number}
*/
get readyTime() {
if (!libReady) {
return 0;
}
return readyTime - injectedTime;
},
/**
* Time in msec to perform translation.
* @type {number}
*/
get translationTime() {
if (!finished) {
return 0;
}
return endTime - startTime;
},
/**
* Translate the page contents. Note that the translation is asynchronous.
* You need to regularly check the state of |finished| and |errorCode| to
* know if the translation finished or if there was an error.
* @param {string} sourceLang The language the page is in.
* @param {string} targetLang The language the page should be translated to.
* @return {boolean} False if the translate library was not ready, in which
* case the translation is not started. True otherwise.
*/
translate(sourceLang, targetLang) {
finished = false;
errorCode = ERROR['NONE'];
if (!libReady) {
return false;
}
startTime = performance.now();
try {
lib.translatePage(sourceLang, targetLang, onTranslateProgress);
} catch (err) {
console.error('Translate: ' + err);
errorCode = ERROR['UNEXPECTED_SCRIPT_ERROR'];
invokeResultCallback();
return false;
}
return true;
},
/**
* Reverts the page contents to its original value, effectively reverting
* any performed translation. Does nothing if the page was not translated.
*/
revert() {
lib.restore();
},
/**
* Called when an error is caught while executing script fetched in
* translate_script.cc.
*/
onTranslateElementError(error) {
errorCode = ERROR['UNEXPECTED_SCRIPT_ERROR'];
invokeReadyCallback();
},
/**
* Entry point called by the Translate Element once it has been injected in
* the page.
*/
onTranslateElementLoad() {
loadedTime = performance.now();
try {
lib = google.translate.TranslateService({
// translateApiKey is predefined by translate_script.cc.
'key': translateApiKey,
'serverParams': serverParams,
'timeInfo': gtTimeInfo,
'useSecureConnection': true,
});
translateApiKey = undefined;
serverParams = undefined;
gtTimeInfo = undefined;
} catch (err) {
errorCode = ERROR['INITIALIZATION_ERROR'];
translateApiKey = undefined;
serverParams = undefined;
gtTimeInfo = undefined;
invokeReadyCallback();
return;
}
// The TranslateService is not available immediately as it needs to start
// Flash. Let's wait until it is ready.
checkLibReady();
},
/**
* Entry point called by the Translate Element when it want to load an
* external CSS resource into the page.
* @param {string} url URL of an external CSS resource to load.
*/
onLoadCSS(url) {
const element = document.createElement('link');
element.type = 'text/css';
element.rel = 'stylesheet';
element.charset = 'UTF-8';
element.href = url;
document.head.appendChild(element);
},
/**
* Entry point called by the Translate Element when it want to load and run
* an external JavaScript on the page.
* @param {string} url URL of an external JavaScript to load.
*/
onLoadJavascript(url) {
// securityOrigin is predefined by translate_script.cc.
if (!url.startsWith(securityOrigin)) {
console.error('Translate: ' + url + ' is not allowed to load.');
errorCode = ERROR['BAD_ORIGIN'];
return;
}
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (this.readyState !== this.DONE) {
return;
}
if (this.status !== 200) {
errorCode = ERROR['SCRIPT_LOAD_ERROR'];
return;
}
// Execute translate script using an anonymous function on the window,
// this prevents issues with the code being inside of the scope of the
// XHR request.
new Function(this.responseText).call(window);
};
xhr.send();
},
};
})();