Bug 1756218 - run android reftest/crashtest as no-fission. r=releng-reviewers,gbrown
[gecko.git] / browser / components / translation / YandexTranslator.jsm
blob0114b5541a0da3b9b1eeef606713c5ab964e8dba
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/. */
5 "use strict";
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
26 // request.
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,
51 /**
52  * Translates a webpage using Yandex's Translation API.
53  *
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
58  *
59  * @returns {Promise}          A promise that will resolve when the translation
60  *                             task is finished.
61  */
62 var YandexTranslator = function(
63   translationDocument,
64   sourceLanguage,
65   targetLanguage
66 ) {
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 = {
77   /**
78    * Performs the translation, splitting the document into several chunks
79    * respecting the data limits of the API.
80    *
81    * @returns {Promise}          A promise that will resolve when the translation
82    *                             task is finished.
83    */
84   translate() {
85     return (async () => {
86       let currentIndex = 0;
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(
104           request.data,
105           this.sourceLanguage,
106           this.targetLanguage
107         );
108         this._pendingRequests++;
109         yandexRequest
110           .fireRequest()
111           .then(this._chunkCompleted.bind(this), this._chunkFailed.bind(this));
113         currentIndex = request.lastIndex;
114         if (request.finished) {
115           break;
116         }
117       }
119       return this._onFinishedDeferred.promise;
120     })();
121   },
123   /**
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.
128    *
129    * @param   request   The YandexRequest sent to the server
130    */
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;
136     }
138     this._checkIfFinished();
139   },
141   /**
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
147    * requests left.
148    *
149    * @param   aError   [optional] The XHR object of the request that failed.
150    */
151   _chunkFailed(aError) {
152     if (aError instanceof XMLHttpRequest) {
153       let body = aError.responseText;
154       let json = { code: 0 };
155       try {
156         json = JSON.parse(body);
157       } catch (e) {}
159       if (json.code && YANDEX_PERMANENT_ERRORS.includes(json.code)) {
160         this._serviceUnavailable = true;
161       }
162     }
164     this._checkIfFinished();
165   },
167   /**
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.
171    */
172   _checkIfFinished() {
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,
183         });
184       } else {
185         let error = this._serviceUnavailable ? "unavailable" : "failure";
186         this._onFinishedDeferred.reject(error);
187       }
188     }
189   },
191   /**
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
196    *
197    * @param   request      The request sent to the server.
198    * @returns boolean      True if parsing of this chunk was successful.
199    */
200   _parseChunkResult(yandexRequest) {
201     let results;
202     try {
203       let result = JSON.parse(yandexRequest.networkRequest.responseText);
204       if (result.code != 200) {
205         Services.console.logStringMessage(
206           "YandexTranslator: Result is " + result.code
207         );
208         return false;
209       }
210       results = result.text;
211     } catch (e) {
212       return false;
213     }
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.
220       return false;
221     }
223     let error = false;
224     for (let i = 0; i < len; i++) {
225       try {
226         let result = results[i];
227         let root = yandexRequest.translationData[i][0];
228         root.parseResult(result);
229       } catch (e) {
230         error = true;
231       }
232     }
234     return !error;
235   },
237   /**
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.
240    *
241    * @param startIndex What is the index, in the roots list, that the
242    *                   chunk should start.
243    */
244   _generateNextTranslationRequest(startIndex) {
245     let currentDataSize = 0;
246     let currentChunks = 0;
247     let output = [];
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);
253       if (!text) {
254         continue;
255       }
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.
265         return {
266           data: output,
267           finished: false,
268           lastIndex: i,
269         };
270       }
272       currentDataSize = newCurSize;
273       currentChunks = newChunks;
274       output.push([root, text]);
275     }
277     return {
278       data: output,
279       finished: true,
280       lastIndex: 0,
281     };
282   },
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...
290  *                          function.
291  * @param sourceLanguage    The source language of the document.
292  * @param targetLanguage    The target language for the translation.
294  */
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 = {
303   /**
304    * Initiates the request
305    */
306   fireRequest() {
307     return (async () => {
308       // Prepare URL.
309       let url = getUrlParam(
310         "https://translate.yandex.net/api/v1.5/tr.json/translate",
311         "browser.translation.yandex.translateURLOverride"
312       );
314       // Prepare the request body.
315       let apiKey = getUrlParam(
316         "%YANDEX_API_KEY%",
317         "browser.translation.yandex.apiKeyOverride"
318       );
319       let params = [
320         ["key", apiKey],
321         ["format", "html"],
322         ["lang", this.sourceLanguage + "-" + this.targetLanguage],
323       ];
325       for (let [, text] of this.translationData) {
326         params.push(["text", text]);
327         this.characterCount += text.length;
328       }
330       // Set up request options.
331       return new Promise((resolve, reject) => {
332         let options = {
333           onLoad: (responseText, xhr) => {
334             resolve(this);
335           },
336           onError(e, responseText, xhr) {
337             reject(xhr);
338           },
339           postData: params,
340         };
342         // Fire the request.
343         this.networkRequest = httpRequest(url, options);
344       });
345     })();
346   },
350  * Fetch an auth token (clientID or client secret), which may be overridden by
351  * a pref if it's set.
352  */
353 function getUrlParam(paramValue, prefName) {
354   if (Services.prefs.getPrefType(prefName)) {
355     paramValue = Services.prefs.getCharPref(prefName);
356   }
357   paramValue = Services.urlFormatter.formatURL(paramValue);
358   return paramValue;