GeckoViewTranslations.sys.mjs (19905B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", 9 TranslationsUtils: 10 "chrome://global/content/translations/TranslationsUtils.mjs", 11 }); 12 13 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; 14 15 export class GeckoViewTranslations extends GeckoViewModule { 16 onInit() { 17 debug`onInit`; 18 this.registerListener([ 19 "GeckoView:Translations:Translate", 20 "GeckoView:Translations:RestorePage", 21 "GeckoView:Translations:GetNeverTranslateSite", 22 "GeckoView:Translations:SetNeverTranslateSite", 23 ]); 24 } 25 26 onEnable() { 27 debug`onEnable`; 28 this.window.addEventListener("TranslationsParent:OfferTranslation", this); 29 this.window.addEventListener("TranslationsParent:LanguageState", this); 30 } 31 32 onDisable() { 33 debug`onDisable`; 34 this.window.removeEventListener( 35 "TranslationsParent:OfferTranslation", 36 this 37 ); 38 this.window.removeEventListener("TranslationsParent:LanguageState", this); 39 } 40 41 onEvent(aEvent, aData, aCallback) { 42 debug`onEvent: event=${aEvent}, data=${aData}`; 43 switch (aEvent) { 44 case "GeckoView:Translations:Translate": { 45 try { 46 const { sourceLanguage, targetLanguage } = aData; 47 48 if ( 49 lazy.TranslationsUtils.isLangTagValid(sourceLanguage) && 50 lazy.TranslationsUtils.isLangTagValid(targetLanguage) 51 ) { 52 this.getActor("Translations") 53 .translate( 54 { 55 sourceLanguage, 56 targetLanguage, 57 // Model variants are not currently supported. See Bug 1943444. 58 }, 59 /* reportAsAutoTranslate */ false 60 ) 61 .then( 62 () => aCallback.onSuccess(), 63 error => aCallback.onError(`Could not translate: ${error}`) 64 ); 65 } else { 66 aCallback.onError( 67 `The language tag ${sourceLanguage} or ${targetLanguage} is not valid.` 68 ); 69 } 70 } catch (error) { 71 aCallback.onError(`Could not translate: ${error}`); 72 } 73 break; 74 } 75 76 case "GeckoView:Translations:RestorePage": 77 try { 78 this.getActor("Translations").restorePage(); 79 aCallback.onSuccess(); 80 } catch (error) { 81 aCallback.onError(`Could not restore page: ${error}`); 82 } 83 break; 84 85 case "GeckoView:Translations:GetNeverTranslateSite": 86 try { 87 const value = 88 this.getActor("Translations").shouldNeverTranslateSite(); 89 aCallback.onSuccess(value); 90 } catch (error) { 91 aCallback.onError(`Could not set site setting: ${error}`); 92 } 93 break; 94 95 case "GeckoView:Translations:SetNeverTranslateSite": 96 try { 97 this.getActor("Translations").setNeverTranslateSitePermissions( 98 aData.neverTranslate 99 ); 100 aCallback.onSuccess(); 101 } catch (error) { 102 aCallback.onError(`Could not set site setting: ${error}`); 103 } 104 break; 105 } 106 } 107 108 handleEvent(aEvent) { 109 debug`handleEvent: ${aEvent.type}`; 110 switch (aEvent.type) { 111 case "TranslationsParent:OfferTranslation": 112 this.eventDispatcher.sendRequest({ 113 type: "GeckoView:Translations:Offer", 114 }); 115 break; 116 case "TranslationsParent:LanguageState": { 117 const { 118 detectedLanguages, 119 requestedLanguagePair, 120 hasVisibleChange, 121 error, 122 isEngineReady, 123 } = aEvent.detail.actor.languageState; 124 125 const data = { 126 detectedLanguages, 127 requestedLanguagePair, 128 hasVisibleChange, 129 error, 130 isEngineReady, 131 }; 132 133 this.eventDispatcher.sendRequest({ 134 type: "GeckoView:Translations:StateChange", 135 data, 136 }); 137 138 break; 139 } 140 } 141 } 142 } 143 144 // Runtime functionality 145 export const GeckoViewTranslationsSettings = { 146 // Helper method for retrieving language setting state and corresponding string name. 147 _getLanguageSettingName(langTag) { 148 const isAlways = lazy.TranslationsParent.shouldAlwaysTranslateLanguage({ 149 docLangTag: langTag, 150 userLangTag: new Intl.Locale(Services.locale.appLocaleAsBCP47).language, 151 }); 152 const isNever = 153 lazy.TranslationsParent.shouldNeverTranslateLanguage(langTag); 154 // Default setting is offer. 155 let setting = "offer"; 156 157 if (isAlways & !isNever) { 158 setting = "always"; 159 } 160 161 if (isNever & !isAlways) { 162 setting = "never"; 163 } 164 return setting; 165 }, 166 /* eslint-disable complexity */ 167 async onEvent(aEvent, aData, aCallback) { 168 debug`onEvent ${aEvent} ${aData}`; 169 170 switch (aEvent) { 171 case "GeckoView:Translations:IsTranslationEngineSupported": { 172 try { 173 aCallback.onSuccess( 174 lazy.TranslationsParent.getIsTranslationsEngineSupported() 175 ); 176 } catch (error) { 177 aCallback.onError( 178 `An issue occurred while checking the translations engine: ${error}` 179 ); 180 } 181 return; 182 } 183 case "GeckoView:Translations:PreferredLanguages": { 184 aCallback.onSuccess({ 185 preferredLanguages: lazy.TranslationsParent.getPreferredLanguages(), 186 }); 187 return; 188 } 189 case "GeckoView:Translations:ManageModel": { 190 const { language, operation, operationLevel } = aData; 191 if (operation === "delete") { 192 if (operationLevel === "all") { 193 lazy.TranslationsParent.deleteAllLanguageFiles().then( 194 function () { 195 aCallback.onSuccess(); 196 }, 197 function (error) { 198 aCallback.onError( 199 `COULD_NOT_DELETE - An issue occurred while deleting all language files: ${error}` 200 ); 201 } 202 ); 203 return; 204 } 205 if (operationLevel === "language") { 206 if (language === undefined) { 207 aCallback.onError( 208 `LANGUAGE_REQUIRED - A specified language is required language level operations.` 209 ); 210 return; 211 } 212 lazy.TranslationsParent.deleteLanguageFiles(language).then( 213 function () { 214 aCallback.onSuccess(); 215 }, 216 function (error) { 217 aCallback.onError( 218 `COULD_NOT_DELETE - An issue occurred while deleting a language file: ${error}` 219 ); 220 } 221 ); 222 } 223 if (operationLevel === "cache") { 224 await lazy.TranslationsParent.deleteCachedLanguageFiles().then( 225 function () { 226 aCallback.onSuccess(); 227 }, 228 function (error) { 229 aCallback.onError( 230 `COULD_NOT_DELETE - An issue occurred while deleting the cache: ${error}` 231 ); 232 } 233 ); 234 } 235 } else if (operation === "download") { 236 if (operationLevel === "all") { 237 lazy.TranslationsParent.downloadAllFiles().then( 238 function () { 239 aCallback.onSuccess(); 240 }, 241 function (error) { 242 aCallback.onError( 243 `COULD_NOT_DOWNLOAD - An issue occurred while downloading all language files: ${error}` 244 ); 245 } 246 ); 247 return; 248 } 249 if (operationLevel === "language") { 250 if (language === undefined) { 251 aCallback.onError( 252 `LANGUAGE_REQUIRED - A specified language is required language level operations.` 253 ); 254 return; 255 } 256 lazy.TranslationsParent.downloadLanguageFiles(language).then( 257 function () { 258 aCallback.onSuccess(); 259 }, 260 function (error) { 261 aCallback.onError( 262 `COULD_NOT_DOWNLOAD - An issue occurred while downloading a language files: ${error}` 263 ); 264 } 265 ); 266 } 267 if (operationLevel === "cache") { 268 aCallback.onError( 269 `COULD_NOT_DOWNLOAD - Downloading the cache is not a valid option. Please check the parameters and try again. 270 Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}` 271 ); 272 } 273 } else { 274 aCallback.onError( 275 `ERROR_UNKNOWN - The request to manage models appears to be malformed. Please check the parameters and try again. 276 Language: ${language}, Operation: ${operation}, Operation Level: ${operationLevel}` 277 ); 278 } 279 break; 280 } 281 case "GeckoView:Translations:TranslationInformation": { 282 if ( 283 Cu.isInAutomation && 284 Services.prefs.getBoolPref( 285 "browser.translations.geckoview.enableAllTestMocks", 286 false 287 ) 288 ) { 289 const mockResult = { 290 languagePairs: [ 291 { sourceLanguage: "en", targetLanguage: "es" }, 292 { sourceLanguage: "es", targetLanguage: "en" }, 293 { sourceLanguage: "en", targetLanguage: "es", variant: "base" }, 294 ], 295 sourceLanguages: [ 296 { langTag: "en", langTagKey: "en", displayName: "English" }, 297 { langTag: "es", langTagKey: "es", displayName: "Spanish" }, 298 ], 299 targetLanguages: [ 300 { langTag: "en", langTagKey: "en", displayName: "English" }, 301 { langTag: "es", langTagKey: "en", displayName: "Spanish" }, 302 ], 303 }; 304 aCallback.onSuccess(mockResult); 305 return; 306 } 307 308 lazy.TranslationsParent.getSupportedLanguages().then( 309 function (value) { 310 aCallback.onSuccess(value); 311 }, 312 function (error) { 313 aCallback.onError( 314 `Could not retrieve requested information: ${error}` 315 ); 316 } 317 ); 318 break; 319 } 320 case "GeckoView:Translations:ModelInformation": { 321 if ( 322 Cu.isInAutomation && 323 Services.prefs.getBoolPref( 324 "browser.translations.geckoview.enableAllTestMocks", 325 false 326 ) 327 ) { 328 const mockResult = { 329 models: [ 330 { 331 langTag: "es", 332 displayName: "Spanish", 333 isDownloaded: false, 334 size: 12345, 335 }, 336 { 337 langTag: "de", 338 displayName: "German", 339 isDownloaded: false, 340 size: 12345, 341 }, 342 ], 343 }; 344 aCallback.onSuccess(mockResult); 345 return; 346 } 347 348 // Helper function to process remote server records size and download state for GV use 349 async function _processLanguageModelData(language, remoteRecords) { 350 // Aggregate size of downloads, e.g., one language has many model binary files 351 let size = 0; 352 remoteRecords.forEach(item => { 353 size += parseInt(item.attachment.size); 354 }); 355 // Check if required files are downloaded 356 const isDownloaded = 357 await lazy.TranslationsParent.hasAllFilesForLanguage( 358 language.langTag 359 ); 360 const model = { 361 langTag: language.langTag, 362 displayName: language.displayName, 363 isDownloaded, 364 size, 365 }; 366 return model; 367 } 368 369 // Main call to toolkit 370 lazy.TranslationsParent.getSupportedLanguages().then( 371 // Retrieve supported languages 372 async function (supportedLanguages) { 373 // Get language display information, 374 const languageList = 375 lazy.TranslationsParent.getLanguageList(supportedLanguages); 376 const results = []; 377 const pivotIsDownloaded = 378 await lazy.TranslationsParent.hasAllFilesForLanguage( 379 lazy.TranslationsParent.PIVOT_LANGUAGE 380 ); 381 382 // For each language, process the related remote server model records 383 languageList.forEach(language => { 384 // No need to include the pivot in the download size, once it has been downloaded. 385 const recordsResult = 386 lazy.TranslationsParent.getRecordsForTranslatingToAndFromAppLanguage( 387 language.langTag, 388 /* includePivotRecords */ !pivotIsDownloaded 389 ).then( 390 async function (records) { 391 return _processLanguageModelData(language, records); 392 }, 393 function (recordError) { 394 aCallback.onError( 395 `An issue occurred while aggregating information: ${recordError}` 396 ); 397 } 398 ); 399 results.push(recordsResult); 400 }); 401 // Aggregate records 402 Promise.all(results).then(models => { 403 const response = []; 404 models.forEach(item => { 405 // Ensures unactionable models do not appear in the list 406 if (parseInt(item.size) !== 0) { 407 response.push(item); 408 } 409 }); 410 aCallback.onSuccess({ models: response }); 411 }); 412 }, 413 function (languageError) { 414 aCallback.onError( 415 `An issue occurred while retrieving the supported languages: ${languageError}` 416 ); 417 } 418 ); 419 break; 420 } 421 422 case "GeckoView:Translations:GetLanguageSetting": { 423 if ( 424 Cu.isInAutomation && 425 Services.prefs.getBoolPref( 426 "browser.translations.geckoview.enableAllTestMocks", 427 false 428 ) 429 ) { 430 aCallback.onSuccess("always"); 431 return; 432 } 433 434 try { 435 const setting = this._getLanguageSettingName(aData.language); 436 aCallback.onSuccess(setting); 437 } catch (error) { 438 aCallback.onError(`Could not get language setting: ${error}`); 439 } 440 break; 441 } 442 443 case "GeckoView:Translations:GetLanguageSettings": { 444 if ( 445 Cu.isInAutomation && 446 Services.prefs.getBoolPref( 447 "browser.translations.geckoview.enableAllTestMocks", 448 false 449 ) 450 ) { 451 const mockResult = { 452 settings: [ 453 { langTag: "fr", displayName: "French", setting: "always" }, 454 { langTag: "de", displayName: "German", setting: "offer" }, 455 { langTag: "es", displayName: "Spanish", setting: "never" }, 456 ], 457 }; 458 aCallback.onSuccess(mockResult); 459 return; 460 } 461 462 lazy.TranslationsParent.getSupportedLanguages().then( 463 function (supportedLanguages) { 464 const languageList = 465 lazy.TranslationsParent.getLanguageList(supportedLanguages); 466 467 languageList.forEach(language => { 468 language.setting = this._getLanguageSettingName(language.langTag); 469 }); 470 471 aCallback.onSuccess({ settings: languageList }); 472 }.bind(this), 473 function (error) { 474 aCallback.onError( 475 `Could not retrieve language setting information: ${error}` 476 ); 477 } 478 ); 479 break; 480 } 481 482 case "GeckoView:Translations:SetLanguageSettings": { 483 let { language, languageSetting } = aData; 484 languageSetting = languageSetting.toLowerCase(); 485 486 if (!lazy.TranslationsUtils.isLangTagValid(aData.language)) { 487 aCallback.onError(`The language tag ${language} is not valid.`); 488 return; 489 } 490 491 const ALWAYS = lazy.TranslationsParent.ALWAYS_TRANSLATE_LANGS_PREF; 492 const NEVER = lazy.TranslationsParent.NEVER_TRANSLATE_LANGS_PREF; 493 494 switch (languageSetting) { 495 case "always": { 496 try { 497 lazy.TranslationsParent.removeLangTagFromPref(language, NEVER); 498 lazy.TranslationsParent.addLangTagToPref(language, ALWAYS); 499 aCallback.onSuccess(); 500 } catch (error) { 501 aCallback.onError( 502 `Could not set language preference to always: ${error}` 503 ); 504 } 505 break; 506 } 507 508 case "never": { 509 try { 510 lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS); 511 lazy.TranslationsParent.addLangTagToPref(language, NEVER); 512 aCallback.onSuccess(); 513 } catch (error) { 514 aCallback.onError( 515 `Could not set language preference to never: ${error}` 516 ); 517 } 518 break; 519 } 520 521 case "offer": { 522 try { 523 // Reverting to default settings, so ensure nothing is set. 524 lazy.TranslationsParent.removeLangTagFromPref(language, NEVER); 525 lazy.TranslationsParent.removeLangTagFromPref(language, ALWAYS); 526 aCallback.onSuccess(); 527 } catch (error) { 528 aCallback.onError( 529 `Could not set language preference to offer: ${error}` 530 ); 531 } 532 break; 533 } 534 } 535 break; 536 } 537 538 case "GeckoView:Translations:GetNeverTranslateSpecifiedSites": 539 try { 540 const neverTranslateList = 541 lazy.TranslationsParent.listNeverTranslateSites(); 542 aCallback.onSuccess({ sites: neverTranslateList }); 543 } catch (error) { 544 aCallback.onError( 545 `Could not get list of never translate sites: ${error}` 546 ); 547 } 548 break; 549 550 case "GeckoView:Translations:SetNeverTranslateSpecifiedSite": 551 try { 552 lazy.TranslationsParent.setNeverTranslateSiteByOrigin( 553 aData.neverTranslate, 554 aData.origin 555 ); 556 aCallback.onSuccess(); 557 } catch (error) { 558 aCallback.onError( 559 `Could not set never translate site setting: ${error}` 560 ); 561 } 562 break; 563 case "GeckoView:Translations:GetTranslateDownloadSize": { 564 if ( 565 Cu.isInAutomation && 566 Services.prefs.getBoolPref( 567 "browser.translations.geckoview.enableAllTestMocks", 568 false 569 ) 570 ) { 571 aCallback.onSuccess({ bytes: 1234567 }); 572 return; 573 } 574 575 const fromLangValid = lazy.TranslationsUtils.isLangTagValid( 576 aData.fromLanguage 577 ); 578 const toLangValid = lazy.TranslationsUtils.isLangTagValid( 579 aData.toLanguage 580 ); 581 if (!fromLangValid || !toLangValid) { 582 aCallback.onError( 583 `The language tag ${aData.fromLanguage} or ${aData.toLanguage} is not valid.` 584 ); 585 return; 586 } 587 588 lazy.TranslationsParent.getExpectedTranslationDownloadSize( 589 aData.fromLanguage, 590 aData.toLanguage 591 ).then( 592 function (bytes) { 593 aCallback.onSuccess({ bytes }); 594 }, 595 function (error) { 596 aCallback.onError(`Could not get the download size: ${error}`); 597 } 598 ); 599 break; 600 } 601 } 602 }, 603 }; 604 605 const { debug, warn } = GeckoViewTranslations.initLogging( 606 "GeckoViewTranslations" 607 );