1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["YandexTranslator"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const { PromiseUtils } = ChromeUtils.import(
11 "resource://gre/modules/PromiseUtils.jsm"
13 const { Async } = ChromeUtils.import("resource://services-common/async.js");
14 const { httpRequest } = ChromeUtils.import("resource://gre/modules/Http.jsm");
15 const { XPCOMUtils } = ChromeUtils.import(
16 "resource://gre/modules/XPCOMUtils.jsm"
19 XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
21 // The maximum amount of net data allowed per request on Bing's API.
22 const MAX_REQUEST_DATA = 5000; // Documentation says 10000 but anywhere
23 // close to that is refused by the service.
25 // The maximum number of chunks allowed to be translated in a single
27 const MAX_REQUEST_CHUNKS = 1000; // Documentation says 2000.
29 // Self-imposed limit of 15 requests. This means that a page that would need
30 // to be broken in more than 15 requests won't be fully translated.
31 // The maximum amount of data that we will translate for a single page
32 // is MAX_REQUESTS * MAX_REQUEST_DATA.
33 const MAX_REQUESTS = 15;
35 const YANDEX_ERR_KEY_INVALID = 401; // Invalid API key
36 const YANDEX_ERR_KEY_BLOCKED = 402; // This API key has been blocked
37 const YANDEX_ERR_DAILY_REQ_LIMIT_EXCEEDED = 403; // Daily limit for requests reached
38 const YANDEX_ERR_DAILY_CHAR_LIMIT_EXCEEDED = 404; // Daily limit of chars reached
39 // const YANDEX_ERR_TEXT_TOO_LONG = 413; // The text size exceeds the maximum
40 // const YANDEX_ERR_UNPROCESSABLE_TEXT = 422; // The text could not be translated
41 // const YANDEX_ERR_LANG_NOT_SUPPORTED = 501; // The specified translation direction is not supported
43 // Errors that should activate the service unavailable handling
44 const YANDEX_PERMANENT_ERRORS = [
45 YANDEX_ERR_KEY_INVALID,
46 YANDEX_ERR_KEY_BLOCKED,
47 YANDEX_ERR_DAILY_REQ_LIMIT_EXCEEDED,
48 YANDEX_ERR_DAILY_CHAR_LIMIT_EXCEEDED,
52 * Translates a webpage using Yandex's Translation API.
54 * @param translationDocument The TranslationDocument object that represents
55 * the webpage to be translated
56 * @param sourceLanguage The source language of the document
57 * @param targetLanguage The target language for the translation
59 * @returns {Promise} A promise that will resolve when the translation
62 var YandexTranslator = function(
67 this.translationDocument = translationDocument;
68 this.sourceLanguage = sourceLanguage;
69 this.targetLanguage = targetLanguage;
70 this._pendingRequests = 0;
71 this._partialSuccess = false;
72 this._serviceUnavailable = false;
73 this._translatedCharacterCount = 0;
76 YandexTranslator.prototype = {
78 * Performs the translation, splitting the document into several chunks
79 * respecting the data limits of the API.
81 * @returns {Promise} A promise that will resolve when the translation
87 this._onFinishedDeferred = PromiseUtils.defer();
89 // Let's split the document into various requests to be sent to
90 // Yandex's Translation API.
91 for (let requestCount = 0; requestCount < MAX_REQUESTS; requestCount++) {
92 // Generating the text for each request can be expensive, so
93 // let's take the opportunity of the chunkification process to
94 // allow for the event loop to attend other pending events
95 // before we continue.
96 await Async.promiseYield();
98 // Determine the data for the next request.
99 let request = this._generateNextTranslationRequest(currentIndex);
101 // Create a real request to the server, and put it on the
102 // pending requests list.
103 let yandexRequest = new YandexRequest(
108 this._pendingRequests++;
111 .then(this._chunkCompleted.bind(this), this._chunkFailed.bind(this));
113 currentIndex = request.lastIndex;
114 if (request.finished) {
119 return this._onFinishedDeferred.promise;
124 * Function called when a request sent to the server completed successfully.
125 * This function handles calling the function to parse the result and the
126 * function to resolve the promise returned by the public `translate()`
127 * method when there are no pending requests left.
129 * @param request The YandexRequest sent to the server
131 _chunkCompleted(yandexRequest) {
132 if (this._parseChunkResult(yandexRequest)) {
133 this._partialSuccess = true;
134 // Count the number of characters successfully translated.
135 this._translatedCharacterCount += yandexRequest.characterCount;
138 this._checkIfFinished();
142 * Function called when a request sent to the server has failed.
143 * This function handles deciding if the error is transient or means the
144 * service is unavailable (zero balance on the key or request credentials are
145 * not in an active state) and calling the function to resolve the promise
146 * returned by the public `translate()` method when there are no pending
149 * @param aError [optional] The XHR object of the request that failed.
151 _chunkFailed(aError) {
152 if (aError instanceof XMLHttpRequest) {
153 let body = aError.responseText;
154 let json = { code: 0 };
156 json = JSON.parse(body);
159 if (json.code && YANDEX_PERMANENT_ERRORS.includes(json.code)) {
160 this._serviceUnavailable = true;
164 this._checkIfFinished();
168 * Function called when a request sent to the server has completed.
169 * This function handles resolving the promise
170 * returned by the public `translate()` method when all chunks are completed.
173 // Check if all pending requests have been
174 // completed and then resolves the promise.
175 // If at least one chunk was successful, the
176 // promise will be resolved positively which will
177 // display the "Success" state for the infobar. Otherwise,
178 // the "Error" state will appear.
179 if (--this._pendingRequests == 0) {
180 if (this._partialSuccess) {
181 this._onFinishedDeferred.resolve({
182 characterCount: this._translatedCharacterCount,
185 let error = this._serviceUnavailable ? "unavailable" : "failure";
186 this._onFinishedDeferred.reject(error);
192 * This function parses the result returned by Yandex's Translation API,
193 * which returns a JSON result that contains a number of elements. The
194 * API is documented here:
195 * http://api.yandex.com/translate/doc/dg/reference/translate.xml
197 * @param request The request sent to the server.
198 * @returns boolean True if parsing of this chunk was successful.
200 _parseChunkResult(yandexRequest) {
203 let result = JSON.parse(yandexRequest.networkRequest.responseText);
204 if (result.code != 200) {
205 Services.console.logStringMessage(
206 "YandexTranslator: Result is " + result.code
210 results = result.text;
215 let len = results.length;
216 if (len != yandexRequest.translationData.length) {
217 // This should never happen, but if the service returns a different number
218 // of items (from the number of items submitted), we can't use this chunk
219 // because all items would be paired incorrectly.
224 for (let i = 0; i < len; i++) {
226 let result = results[i];
227 let root = yandexRequest.translationData[i][0];
228 root.parseResult(result);
238 * This function will determine what is the data to be used for
239 * the Nth request we are generating, based on the input params.
241 * @param startIndex What is the index, in the roots list, that the
242 * chunk should start.
244 _generateNextTranslationRequest(startIndex) {
245 let currentDataSize = 0;
246 let currentChunks = 0;
248 let rootsList = this.translationDocument.roots;
250 for (let i = startIndex; i < rootsList.length; i++) {
251 let root = rootsList[i];
252 let text = this.translationDocument.generateTextForItem(root);
257 let newCurSize = currentDataSize + text.length;
258 let newChunks = currentChunks + 1;
260 if (newCurSize > MAX_REQUEST_DATA || newChunks > MAX_REQUEST_CHUNKS) {
261 // If we've reached the API limits, let's stop accumulating data
262 // for this request and return. We return information useful for
263 // the caller to pass back on the next call, so that the function
264 // can keep working from where it stopped.
272 currentDataSize = newCurSize;
273 currentChunks = newChunks;
274 output.push([root, text]);
286 * Represents a request (for 1 chunk) sent off to Yandex's service.
288 * @params translationData The data to be used for this translation,
289 * generated by the generateNextTranslationRequest...
291 * @param sourceLanguage The source language of the document.
292 * @param targetLanguage The target language for the translation.
295 function YandexRequest(translationData, sourceLanguage, targetLanguage) {
296 this.translationData = translationData;
297 this.sourceLanguage = sourceLanguage;
298 this.targetLanguage = targetLanguage;
299 this.characterCount = 0;
302 YandexRequest.prototype = {
304 * Initiates the request
307 return (async () => {
309 let url = getUrlParam(
310 "https://translate.yandex.net/api/v1.5/tr.json/translate",
311 "browser.translation.yandex.translateURLOverride"
314 // Prepare the request body.
315 let apiKey = getUrlParam(
317 "browser.translation.yandex.apiKeyOverride"
322 ["lang", this.sourceLanguage + "-" + this.targetLanguage],
325 for (let [, text] of this.translationData) {
326 params.push(["text", text]);
327 this.characterCount += text.length;
330 // Set up request options.
331 return new Promise((resolve, reject) => {
333 onLoad: (responseText, xhr) => {
336 onError(e, responseText, xhr) {
343 this.networkRequest = httpRequest(url, options);
350 * Fetch an auth token (clientID or client secret), which may be overridden by
351 * a pref if it's set.
353 function getUrlParam(paramValue, prefName) {
354 if (Services.prefs.getPrefType(prefName)) {
355 paramValue = Services.prefs.getCharPref(prefName);
357 paramValue = Services.urlFormatter.formatURL(paramValue);