fullPageTranslationsPanel.js (53786B)
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/. */ 4 5 /* eslint-disable jsdoc/valid-types */ 6 /** 7 * @typedef {import("../../../../toolkit/components/translations/translations").LangTags} LangTags 8 */ 9 /* eslint-enable jsdoc/valid-types */ 10 11 ChromeUtils.defineESModuleGetters(this, { 12 PageActions: "resource:///modules/PageActions.sys.mjs", 13 TranslationsUtils: 14 "chrome://global/content/translations/TranslationsUtils.mjs", 15 TranslationsPanelShared: 16 "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs", 17 }); 18 19 /** 20 * The set of actions that can occur from interaction with the 21 * translations panel. 22 */ 23 const PageAction = Object.freeze({ 24 NO_CHANGE: "NO_CHANGE", 25 RESTORE_PAGE: "RESTORE_PAGE", 26 TRANSLATE_PAGE: "TRANSLATE_PAGE", 27 CLOSE_PANEL: "CLOSE_PANEL", 28 }); 29 30 /** 31 * A mechanism for determining the next relevant page action 32 * based on the current translated state of the page and the state 33 * of the persistent options in the translations panel settings. 34 */ 35 class CheckboxPageAction { 36 /** 37 * Whether or not translations is active on the page. 38 * 39 * @type {boolean} 40 */ 41 #translationsActive = false; 42 43 /** 44 * Whether the always-translate-language menuitem is checked 45 * in the translations panel settings menu. 46 * 47 * @type {boolean} 48 */ 49 #alwaysTranslateLanguage = false; 50 51 /** 52 * Whether the never-translate-language menuitem is checked 53 * in the translations panel settings menu. 54 * 55 * @type {boolean} 56 */ 57 #neverTranslateLanguage = false; 58 59 /** 60 * Whether the never-translate-site menuitem is checked 61 * in the translations panel settings menu. 62 * 63 * @type {boolean} 64 */ 65 #neverTranslateSite = false; 66 67 /** 68 * @param {boolean} translationsActive 69 * @param {boolean} alwaysTranslateLanguage 70 * @param {boolean} neverTranslateLanguage 71 * @param {boolean} neverTranslateSite 72 */ 73 constructor( 74 translationsActive, 75 alwaysTranslateLanguage, 76 neverTranslateLanguage, 77 neverTranslateSite 78 ) { 79 this.#translationsActive = translationsActive; 80 this.#alwaysTranslateLanguage = alwaysTranslateLanguage; 81 this.#neverTranslateLanguage = neverTranslateLanguage; 82 this.#neverTranslateSite = neverTranslateSite; 83 } 84 85 /** 86 * Accepts four integers that are either 0 or 1 and returns 87 * a single, unique number for each possible combination of 88 * values. 89 * 90 * @param {number} translationsActive 91 * @param {number} alwaysTranslateLanguage 92 * @param {number} neverTranslateLanguage 93 * @param {number} neverTranslateSite 94 * 95 * @returns {number} - An integer representation of the state 96 */ 97 static #computeState( 98 translationsActive, 99 alwaysTranslateLanguage, 100 neverTranslateLanguage, 101 neverTranslateSite 102 ) { 103 return ( 104 (translationsActive << 3) | 105 (alwaysTranslateLanguage << 2) | 106 (neverTranslateLanguage << 1) | 107 neverTranslateSite 108 ); 109 } 110 111 /** 112 * Returns the current state of the data members as a single number. 113 * 114 * @returns {number} - An integer representation of the state 115 */ 116 #state() { 117 return CheckboxPageAction.#computeState( 118 Number(this.#translationsActive), 119 Number(this.#alwaysTranslateLanguage), 120 Number(this.#neverTranslateLanguage), 121 Number(this.#neverTranslateSite) 122 ); 123 } 124 125 /** 126 * Returns the next page action to take when the always-translate-language 127 * menuitem is toggled in the translations panel settings menu. 128 * 129 * @returns {PageAction} 130 */ 131 alwaysTranslateLanguage() { 132 switch (this.#state()) { 133 case CheckboxPageAction.#computeState(1, 1, 0, 1): 134 case CheckboxPageAction.#computeState(1, 1, 0, 0): 135 return PageAction.RESTORE_PAGE; 136 case CheckboxPageAction.#computeState(0, 0, 1, 0): 137 case CheckboxPageAction.#computeState(0, 0, 0, 0): 138 return PageAction.TRANSLATE_PAGE; 139 } 140 return PageAction.NO_CHANGE; 141 } 142 143 /** 144 * Returns the next page action to take when the never-translate-language 145 * menuitem is toggled in the translations panel settings menu. 146 * 147 * @returns {PageAction} 148 */ 149 neverTranslateLanguage() { 150 switch (this.#state()) { 151 case CheckboxPageAction.#computeState(1, 1, 0, 1): 152 case CheckboxPageAction.#computeState(1, 1, 0, 0): 153 case CheckboxPageAction.#computeState(1, 0, 0, 1): 154 case CheckboxPageAction.#computeState(1, 0, 0, 0): 155 return PageAction.RESTORE_PAGE; 156 case CheckboxPageAction.#computeState(0, 1, 0, 0): 157 case CheckboxPageAction.#computeState(0, 0, 0, 1): 158 case CheckboxPageAction.#computeState(0, 1, 0, 1): 159 case CheckboxPageAction.#computeState(0, 0, 0, 0): 160 return PageAction.CLOSE_PANEL; 161 } 162 return PageAction.NO_CHANGE; 163 } 164 165 /** 166 * Returns the next page action to take when the never-translate-site 167 * menuitem is toggled in the translations panel settings menu. 168 * 169 * @returns {PageAction} 170 */ 171 neverTranslateSite() { 172 switch (this.#state()) { 173 case CheckboxPageAction.#computeState(1, 1, 0, 0): 174 case CheckboxPageAction.#computeState(1, 0, 1, 0): 175 case CheckboxPageAction.#computeState(1, 0, 0, 0): 176 return PageAction.RESTORE_PAGE; 177 case CheckboxPageAction.#computeState(0, 1, 0, 1): 178 return PageAction.TRANSLATE_PAGE; 179 case CheckboxPageAction.#computeState(0, 0, 1, 0): 180 case CheckboxPageAction.#computeState(0, 1, 0, 0): 181 case CheckboxPageAction.#computeState(0, 0, 0, 0): 182 return PageAction.CLOSE_PANEL; 183 } 184 return PageAction.NO_CHANGE; 185 } 186 } 187 188 /** 189 * This singleton class controls the FullPageTranslations panel. 190 * 191 * This component is a `/browser` component, and the actor is a `/toolkit` actor, so care 192 * must be taken to keep the presentation (this component) from the state management 193 * (the Translations actor). This class reacts to state changes coming from the 194 * Translations actor. 195 * 196 * A global instance of this class is created once per top ChromeWindow and is initialized 197 * when the new window is created. 198 * 199 * See the comment above TranslationsParent for more details. 200 * 201 * @see TranslationsParent 202 */ 203 var FullPageTranslationsPanel = new (class { 204 /** @type {Console?} */ 205 #console; 206 207 /** 208 * The cached detected languages for both the document and the user. 209 * 210 * @type {null | LangTags} 211 */ 212 detectedLanguages = null; 213 214 /** 215 * Lazily get a console instance. Note that this script is loaded in very early to 216 * the browser loading process, and may run before the console is avialable. In 217 * this case the console will return as `undefined`. 218 * 219 * @returns {Console | void} 220 */ 221 get console() { 222 if (!this.#console) { 223 try { 224 this.#console = console.createInstance({ 225 maxLogLevelPref: "browser.translations.logLevel", 226 prefix: "Translations", 227 }); 228 } catch { 229 // The console may not be initialized yet. 230 } 231 } 232 return this.#console; 233 } 234 235 /** 236 * Tracks if the popup is open, or scheduled to be open. 237 * 238 * @type {boolean} 239 */ 240 #isPopupOpen = false; 241 242 /** 243 * Where the lazy elements are stored. 244 * 245 * @type {Record<string, Element>?} 246 */ 247 #lazyElements; 248 249 /** 250 * Lazily creates the dom elements, and lazily selects them. 251 * 252 * @returns {Record<string, Element>} 253 */ 254 get elements() { 255 if (!this.#lazyElements) { 256 // Lazily turn the template into a DOM element. 257 /** @type {HTMLTemplateElement} */ 258 const wrapper = document.getElementById("template-translations-panel"); 259 const panel = wrapper.content.firstElementChild; 260 wrapper.replaceWith(wrapper.content); 261 262 panel.addEventListener("command", this); 263 panel.addEventListener("click", this); 264 panel.addEventListener("popupshown", this); 265 panel.addEventListener("popuphidden", this); 266 267 const settingsButton = document.getElementById( 268 "translations-panel-settings" 269 ); 270 // Clone the settings toolbarbutton across all the views. 271 for (const header of panel.querySelectorAll(".panel-header")) { 272 if (header.contains(settingsButton)) { 273 continue; 274 } 275 const settingsButtonClone = settingsButton.cloneNode(true); 276 settingsButtonClone.removeAttribute("id"); 277 header.appendChild(settingsButtonClone); 278 } 279 280 // Lazily select the elements. 281 this.#lazyElements = { 282 panel, 283 settingsButton, 284 // The rest of the elements are set by the getter below. 285 }; 286 287 TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, { 288 alwaysTranslateLanguageMenuItem: ".always-translate-language-menuitem", 289 appMenuButton: "PanelUI-menu-button", 290 cancelButton: "full-page-translations-panel-cancel", 291 changeSourceLanguageButton: 292 "full-page-translations-panel-change-source-language", 293 dismissErrorButton: "full-page-translations-panel-dismiss-error", 294 error: "full-page-translations-panel-error", 295 errorMessage: "full-page-translations-panel-error-message", 296 errorMessageHint: "full-page-translations-panel-error-message-hint", 297 errorHintAction: "full-page-translations-panel-translate-hint-action", 298 fromLabel: "full-page-translations-panel-from-label", 299 fromMenuList: "full-page-translations-panel-from", 300 fromMenuPopup: "full-page-translations-panel-from-menupopup", 301 header: "full-page-translations-panel-header", 302 intro: "full-page-translations-panel-intro", 303 introLearnMoreLink: 304 "full-page-translations-panel-intro-learn-more-link", 305 langSelection: "full-page-translations-panel-lang-selection", 306 manageLanguagesMenuItem: ".manage-languages-menuitem", 307 multiview: "full-page-translations-panel-multiview", 308 neverTranslateLanguageMenuItem: ".never-translate-language-menuitem", 309 neverTranslateSiteMenuItem: ".never-translate-site-menuitem", 310 restoreButton: "full-page-translations-panel-restore-button", 311 toLabel: "full-page-translations-panel-to-label", 312 toMenuList: "full-page-translations-panel-to", 313 toMenuPopup: "full-page-translations-panel-to-menupopup", 314 translateButton: "full-page-translations-panel-translate", 315 unsupportedHeader: 316 "full-page-translations-panel-unsupported-language-header", 317 unsupportedHint: "full-page-translations-panel-error-unsupported-hint", 318 unsupportedLearnMoreLink: 319 "full-page-translations-panel-unsupported-learn-more-link", 320 }); 321 } 322 323 return this.#lazyElements; 324 } 325 326 #lazyButtonElements = null; 327 328 /** 329 * When accessing `this.elements` the first time, it de-lazifies the custom components 330 * that are needed for the popup. Avoid that by having a second element lookup 331 * just for modifying the button. 332 */ 333 get buttonElements() { 334 if (!this.#lazyButtonElements) { 335 this.#lazyButtonElements = { 336 button: document.getElementById("translations-button"), 337 buttonLocale: document.getElementById("translations-button-locale"), 338 buttonCircleArrows: document.getElementById( 339 "translations-button-circle-arrows" 340 ), 341 }; 342 } 343 return this.#lazyButtonElements; 344 } 345 346 /** 347 * Cache the last command used for error hints so that it can be later removed. 348 */ 349 #lastHintCommand = null; 350 351 /** 352 * @param {object} options 353 * @param {string} options.message - l10n id 354 * @param {string} options.hint - l10n id 355 * @param {string} options.actionText - l10n id 356 * @param {Function} options.actionCommand - The action to perform. 357 */ 358 #showError({ 359 message, 360 hint, 361 actionText: hintCommandText, 362 actionCommand: hintCommand, 363 }) { 364 const { error, errorMessage, errorMessageHint, errorHintAction, intro } = 365 this.elements; 366 error.hidden = false; 367 intro.hidden = true; 368 document.l10n.setAttributes(errorMessage, message); 369 370 if (hint) { 371 errorMessageHint.hidden = false; 372 document.l10n.setAttributes(errorMessageHint, hint); 373 } else { 374 errorMessageHint.hidden = true; 375 } 376 377 if (hintCommand && hintCommandText) { 378 errorHintAction.removeEventListener("command", this.#lastHintCommand); 379 this.#lastHintCommand = hintCommand; 380 errorHintAction.addEventListener("command", hintCommand); 381 errorHintAction.hidden = false; 382 document.l10n.setAttributes(errorHintAction, hintCommandText); 383 } else { 384 errorHintAction.hidden = true; 385 } 386 } 387 388 /** 389 * Fetches the language tags for the document and the user and caches the results 390 * Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched. 391 * This requires a bit of work to do, so prefer the cached version when possible. 392 * 393 * @returns {Promise<LangTags>} 394 */ 395 async #fetchDetectedLanguages() { 396 this.detectedLanguages = await TranslationsParent.getTranslationsActor( 397 gBrowser.selectedBrowser 398 ).getDetectedLanguages(); 399 return this.detectedLanguages; 400 } 401 402 /** 403 * If the detected language tags have been retrieved previously, return the cached 404 * version. Otherwise do a fresh lookup of the document's language tag. 405 * 406 * @returns {Promise<LangTags>} 407 */ 408 async #getCachedDetectedLanguages() { 409 if (!this.detectedLanguages) { 410 return this.#fetchDetectedLanguages(); 411 } 412 return this.detectedLanguages; 413 } 414 415 /** 416 * Builds the <menulist> of languages for both the "from" and "to". This can be 417 * called every time the popup is shown, as it will retry when there is an error 418 * (such as a network error) or be a noop if it's already initialized. 419 */ 420 async #ensureLangListsBuilt() { 421 try { 422 await TranslationsPanelShared.ensureLangListsBuilt(document, this); 423 } catch (error) { 424 this.console?.error(error); 425 } 426 } 427 428 /** 429 * Reactively sets the views based on the async state changes of the engine, and 430 * other component state changes. 431 * 432 * @param {TranslationsLanguageState} languageState 433 */ 434 #updateViewFromTranslationStatus( 435 languageState = TranslationsParent.getTranslationsActor( 436 gBrowser.selectedBrowser 437 ).languageState 438 ) { 439 const { translateButton, toMenuList, fromMenuList, header, cancelButton } = 440 this.elements; 441 const { requestedLanguagePair, isEngineReady } = languageState; 442 443 // Remove the model variant. e.g. "ru,base" -> "ru" 444 const selectedFrom = fromMenuList.value.split(",")[0]; 445 const selectedTo = toMenuList.value.split(",")[0]; 446 447 if ( 448 requestedLanguagePair && 449 !isEngineReady && 450 TranslationsUtils.langTagsMatch( 451 selectedFrom, 452 requestedLanguagePair.sourceLanguage 453 ) && 454 TranslationsUtils.langTagsMatch( 455 selectedTo, 456 requestedLanguagePair.targetLanguage 457 ) 458 ) { 459 // A translation has been requested, but is not ready yet. 460 document.l10n.setAttributes( 461 translateButton, 462 "translations-panel-translate-button-loading" 463 ); 464 translateButton.disabled = true; 465 cancelButton.hidden = false; 466 this.updateUIForReTranslation(false /* isReTranslation */); 467 } else { 468 document.l10n.setAttributes( 469 translateButton, 470 "translations-panel-translate-button" 471 ); 472 translateButton.disabled = 473 // No "to" language was provided. 474 !toMenuList.value || 475 // No "from" language was provided. 476 !fromMenuList.value || 477 // The translation languages are the same, don't allow this translation. 478 TranslationsUtils.langTagsMatch(selectedFrom, selectedTo) || 479 // This is the requested language pair. 480 (requestedLanguagePair && 481 TranslationsUtils.langTagsMatch( 482 requestedLanguagePair.sourceLanguage, 483 selectedFrom 484 ) && 485 TranslationsUtils.langTagsMatch( 486 requestedLanguagePair.targetLanguage, 487 selectedTo 488 )); 489 } 490 491 if (requestedLanguagePair && isEngineReady) { 492 const { sourceLanguage, targetLanguage } = requestedLanguagePair; 493 const languageDisplayNames = 494 TranslationsParent.createLanguageDisplayNames(); 495 cancelButton.hidden = true; 496 this.updateUIForReTranslation(true /* isReTranslation */); 497 498 document.l10n.setAttributes(header, "translations-panel-revisit-header", { 499 fromLanguage: languageDisplayNames.of(sourceLanguage), 500 toLanguage: languageDisplayNames.of(targetLanguage), 501 }); 502 } else { 503 document.l10n.setAttributes(header, "translations-panel-header"); 504 } 505 } 506 507 /** 508 * @param {boolean} isReTranslation 509 */ 510 updateUIForReTranslation(isReTranslation) { 511 const { restoreButton, fromLabel, fromMenuList, toLabel } = this.elements; 512 restoreButton.hidden = !isReTranslation; 513 // When offering to re-translate a page, hide the "from" language so users don't 514 // get confused. 515 fromLabel.hidden = isReTranslation; 516 fromMenuList.hidden = isReTranslation; 517 if (isReTranslation) { 518 fromLabel.style.marginBlockStart = ""; 519 toLabel.style.marginBlockStart = 0; 520 } else { 521 fromLabel.style.marginBlockStart = 0; 522 toLabel.style.marginBlockStart = ""; 523 } 524 } 525 526 /** 527 * Returns true if the panel is currently showing the default view, otherwise false. 528 * 529 * @returns {boolean} 530 */ 531 #isShowingDefaultView() { 532 if (!this.#lazyElements) { 533 // Nothing has been initialized. 534 return false; 535 } 536 const { multiview } = this.elements; 537 return ( 538 multiview.getAttribute("mainViewId") === 539 "full-page-translations-panel-view-default" 540 ); 541 } 542 543 /** 544 * Show the default view of choosing a source and target language. 545 * 546 * @param {TranslationsParent} actor 547 * @param {boolean} force - Force the page to show translation options. 548 */ 549 async #showDefaultView(actor, force = false) { 550 const { 551 fromMenuList, 552 multiview, 553 panel, 554 error, 555 toMenuList, 556 translateButton, 557 langSelection, 558 intro, 559 header, 560 } = this.elements; 561 562 this.#updateViewFromTranslationStatus(); 563 564 // Unconditionally hide the intro text in case the panel is re-shown. 565 intro.hidden = true; 566 567 if (TranslationsPanelShared.getLangListsInitState(this) === "error") { 568 // There was an error, display it in the view rather than the language 569 // dropdowns. 570 const { cancelButton, errorHintAction } = this.elements; 571 572 this.#showError({ 573 message: "translations-panel-error-load-languages", 574 hint: "translations-panel-error-load-languages-hint", 575 actionText: "translations-panel-error-load-languages-hint-button", 576 actionCommand: () => this.#reloadLangList(actor), 577 }); 578 579 translateButton.disabled = true; 580 this.updateUIForReTranslation(false /* isReTranslation */); 581 cancelButton.hidden = false; 582 langSelection.hidden = true; 583 errorHintAction.disabled = false; 584 return; 585 } 586 587 // Remove any old selected values synchronously before asking for new ones. 588 fromMenuList.value = ""; 589 error.hidden = true; 590 langSelection.hidden = false; 591 // Remove the model variant. e.g. "ru,base" -> "ru" 592 const selectedSource = fromMenuList.value.split(",")[0]; 593 594 const { userLangTag, docLangTag, isDocLangTagSupported } = 595 await this.#fetchDetectedLanguages().then(langTags => langTags ?? {}); 596 597 if (isDocLangTagSupported || force) { 598 // Show the default view with the language selection 599 const { cancelButton } = this.elements; 600 601 if (isDocLangTagSupported) { 602 fromMenuList.value = docLangTag ?? ""; 603 } else { 604 fromMenuList.value = ""; 605 } 606 607 if ( 608 this.#manuallySelectedToLanguage && 609 !TranslationsUtils.langTagsMatch( 610 docLangTag, 611 this.#manuallySelectedToLanguage 612 ) 613 ) { 614 // Use the manually selected language if available 615 toMenuList.value = this.#manuallySelectedToLanguage; 616 } else if ( 617 userLangTag && 618 !TranslationsUtils.langTagsMatch(userLangTag, docLangTag) 619 ) { 620 // The userLangTag is specified and does not match the doc lang tag, so we should use it. 621 toMenuList.value = userLangTag; 622 } else { 623 // No userLangTag is specified in the cache and no #manuallySelectedToLanguage is available, 624 // so we will attempt to find a suitable one. 625 toMenuList.value = 626 await TranslationsParent.getTopPreferredSupportedToLang({ 627 excludeLangTags: [ 628 // Avoid offering to translate into the original source language. 629 docLangTag, 630 // Avoid same-language to same-language translations if possible. 631 selectedSource, 632 ], 633 }); 634 } 635 636 const resolvedSource = fromMenuList.value.split(",")[0]; 637 const resolvedTarget = toMenuList.value.split(",")[0]; 638 639 if (TranslationsUtils.langTagsMatch(resolvedSource, resolvedTarget)) { 640 // The best possible user-preferred language tag that we were able to find for the 641 // toMenuList is the same as the fromMenuList, but same-language to same-language 642 // translations are not allowed in Full Page Translations, so we will just show the 643 // "Choose a language" option in this case. 644 toMenuList.value = ""; 645 } 646 647 this.onChangeLanguages(); 648 649 this.updateUIForReTranslation(false /* isReTranslation */); 650 cancelButton.hidden = false; 651 multiview.setAttribute( 652 "mainViewId", 653 "full-page-translations-panel-view-default" 654 ); 655 656 if (TranslationsParent.hasUserEverTranslated()) { 657 intro.hidden = true; 658 document.l10n.setAttributes(header, "translations-panel-header"); 659 } else { 660 intro.hidden = false; 661 document.l10n.setAttributes(header, "translations-panel-intro-header"); 662 } 663 } else { 664 // Show the "unsupported language" view. 665 const { unsupportedHint } = this.elements; 666 multiview.setAttribute( 667 "mainViewId", 668 "full-page-translations-panel-view-unsupported-language" 669 ); 670 let language; 671 if (docLangTag) { 672 const languageDisplayNames = 673 TranslationsParent.createLanguageDisplayNames({ 674 fallback: "none", 675 }); 676 language = languageDisplayNames.of(docLangTag); 677 } 678 if (language) { 679 document.l10n.setAttributes( 680 unsupportedHint, 681 "translations-panel-error-unsupported-hint-known", 682 { language } 683 ); 684 } else { 685 document.l10n.setAttributes( 686 unsupportedHint, 687 "translations-panel-error-unsupported-hint-unknown" 688 ); 689 } 690 } 691 692 // Focus the "from" language, as it is the only field not set. 693 panel.addEventListener( 694 "ViewShown", 695 () => { 696 if (!fromMenuList.value) { 697 fromMenuList.focus(); 698 } 699 if (!toMenuList.value) { 700 toMenuList.focus(); 701 } 702 }, 703 { once: true } 704 ); 705 } 706 707 /** 708 * Updates the checked states of the settings menu checkboxes that 709 * pertain to languages. 710 */ 711 async #updateSettingsMenuLanguageCheckboxStates() { 712 const langTags = await this.#getCachedDetectedLanguages(); 713 const { docLangTag, isDocLangTagSupported } = langTags; 714 715 const { panel } = this.elements; 716 const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll( 717 ".always-translate-language-menuitem" 718 ); 719 const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll( 720 ".never-translate-language-menuitem" 721 ); 722 const alwaysOfferTranslationsMenuItems = 723 panel.ownerDocument.querySelectorAll( 724 ".always-offer-translations-menuitem" 725 ); 726 727 const alwaysOfferTranslations = 728 TranslationsParent.shouldAlwaysOfferTranslations(); 729 const alwaysTranslateLanguage = 730 TranslationsParent.shouldAlwaysTranslateLanguage(langTags); 731 const neverTranslateLanguage = 732 TranslationsParent.shouldNeverTranslateLanguage(docLangTag); 733 const shouldDisable = 734 !docLangTag || 735 !isDocLangTagSupported || 736 docLangTag === 737 (await TranslationsParent.getTopPreferredSupportedToLang()); 738 739 for (const menuitem of alwaysOfferTranslationsMenuItems) { 740 menuitem.toggleAttribute("checked", alwaysOfferTranslations); 741 } 742 for (const menuitem of alwaysTranslateMenuItems) { 743 menuitem.toggleAttribute("checked", alwaysTranslateLanguage); 744 menuitem.disabled = shouldDisable; 745 } 746 for (const menuitem of neverTranslateMenuItems) { 747 menuitem.toggleAttribute("checked", neverTranslateLanguage); 748 menuitem.disabled = shouldDisable; 749 } 750 } 751 752 /** 753 * Updates the checked states of the settings menu checkboxes that 754 * pertain to site permissions. 755 */ 756 async #updateSettingsMenuSiteCheckboxStates() { 757 const { panel } = this.elements; 758 const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll( 759 ".never-translate-site-menuitem" 760 ); 761 const neverTranslateSite = await TranslationsParent.getTranslationsActor( 762 gBrowser.selectedBrowser 763 ).shouldNeverTranslateSite(); 764 765 for (const menuitem of neverTranslateSiteMenuItems) { 766 menuitem.toggleAttribute("checked", neverTranslateSite); 767 } 768 } 769 770 /** 771 * Populates the language-related settings menuitems by adding the 772 * localized display name of the document's detected language tag. 773 */ 774 async #populateSettingsMenuItems() { 775 const { docLangTag } = await this.#getCachedDetectedLanguages(); 776 777 const { panel } = this.elements; 778 779 const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll( 780 ".always-translate-language-menuitem" 781 ); 782 const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll( 783 ".never-translate-language-menuitem" 784 ); 785 786 /** @type {string | undefined} */ 787 let docLangDisplayName; 788 if (docLangTag) { 789 const languageDisplayNames = 790 TranslationsParent.createLanguageDisplayNames({ 791 fallback: "none", 792 }); 793 // The display name will still be empty if the docLangTag is not known. 794 docLangDisplayName = languageDisplayNames.of(docLangTag); 795 } 796 797 for (const menuitem of alwaysTranslateMenuItems) { 798 if (docLangDisplayName) { 799 document.l10n.setAttributes( 800 menuitem, 801 "translations-panel-settings-always-translate-language", 802 { language: docLangDisplayName } 803 ); 804 } else { 805 document.l10n.setAttributes( 806 menuitem, 807 "translations-panel-settings-always-translate-unknown-language" 808 ); 809 } 810 } 811 812 for (const menuitem of neverTranslateMenuItems) { 813 if (docLangDisplayName) { 814 document.l10n.setAttributes( 815 menuitem, 816 "translations-panel-settings-never-translate-language", 817 { language: docLangDisplayName } 818 ); 819 } else { 820 document.l10n.setAttributes( 821 menuitem, 822 "translations-panel-settings-never-translate-unknown-language" 823 ); 824 } 825 } 826 827 await Promise.all([ 828 this.#updateSettingsMenuLanguageCheckboxStates(), 829 this.#updateSettingsMenuSiteCheckboxStates(), 830 ]); 831 } 832 833 /** 834 * Configures the panel for the user to reset the page after it has been translated. 835 * 836 * @param {LanguagePair} languagePair 837 */ 838 async #showRevisitView({ sourceLanguage, targetLanguage, sourceVariant }) { 839 const { fromMenuList, toMenuList, intro } = this.elements; 840 if (!this.#isShowingDefaultView()) { 841 await this.#showDefaultView( 842 TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser) 843 ); 844 } 845 intro.hidden = true; 846 if (sourceVariant) { 847 fromMenuList.value = `${sourceLanguage},${sourceVariant}`; 848 } else { 849 fromMenuList.value = sourceLanguage; 850 } 851 toMenuList.value = await TranslationsParent.getTopPreferredSupportedToLang({ 852 excludeLangTags: [ 853 // Avoid offering to translate into the original source language. 854 sourceLanguage, 855 // Avoid offering to translate into current active target language. 856 targetLanguage, 857 ], 858 }); 859 this.onChangeLanguages(); 860 } 861 862 /** 863 * Handle the disable logic for when the menulist is changed for the "Translate to" 864 * on the "revisit" subview. 865 */ 866 onChangeRevisitTo() { 867 const { revisitTranslate, revisitMenuList } = this.elements; 868 revisitTranslate.disabled = !revisitMenuList.value; 869 } 870 871 /** 872 * Handle logic and telemetry for changing the selected from-language option. 873 * 874 * @param {Event} event 875 */ 876 onChangeFromLanguage(event) { 877 const { target } = event; 878 if (target?.value) { 879 TranslationsParent.telemetry() 880 .fullPagePanel() 881 .onChangeFromLanguage(target.value); 882 } 883 this.onChangeLanguages(); 884 } 885 886 /** 887 * Handle logic and telemetry for changing the selected to-language option. 888 * 889 * @param {Event} event 890 */ 891 onChangeToLanguage(event) { 892 const { target } = event; 893 if (target?.value) { 894 TranslationsParent.telemetry() 895 .fullPagePanel() 896 .onChangeToLanguage(target.value); 897 } 898 this.onChangeLanguages(); 899 // Update the manually selected language when the user changes the target language. 900 this.#manuallySelectedToLanguage = target.value; 901 } 902 903 /** 904 * When changing the language selection, the translate button will need updating. 905 */ 906 onChangeLanguages() { 907 this.#updateViewFromTranslationStatus(); 908 } 909 910 /** 911 * Hide the pop up (for event handlers). 912 */ 913 close() { 914 PanelMultiView.hidePopup(this.elements.panel); 915 } 916 917 /* 918 * Handler for clicking the learn more link from linked text 919 * within the translations panel. 920 */ 921 onLearnMoreLink() { 922 TranslationsParent.telemetry().fullPagePanel().onLearnMoreLink(); 923 FullPageTranslationsPanel.close(); 924 } 925 926 /* 927 * Handler for clicking the learn more link from the gear menu. 928 */ 929 onAboutTranslations() { 930 TranslationsParent.telemetry().fullPagePanel().onAboutTranslations(); 931 PanelMultiView.hidePopup(this.elements.panel); 932 const window = 933 gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal; 934 window.openTrustedLinkIn( 935 "https://support.mozilla.org/kb/website-translation", 936 "tab", 937 { 938 forceForeground: true, 939 triggeringPrincipal: 940 Services.scriptSecurityManager.getSystemPrincipal(), 941 } 942 ); 943 } 944 945 /** 946 * When a language is not supported and the menu is manually invoked, an error message 947 * is shown. This method switches the panel back to the language selection view. 948 * Note that this bypasses the showSubView method since the main view doesn't support 949 * a subview. 950 */ 951 async onChangeSourceLanguage(event) { 952 const { panel } = this.elements; 953 PanelMultiView.hidePopup(panel); 954 955 await this.#showDefaultView( 956 TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser), 957 true /* force this view to be shown */ 958 ); 959 960 await this.#openPanelPopup(this.elements.appMenuButton, { 961 event, 962 viewName: "defaultView", 963 maintainFlow: true, 964 }); 965 } 966 967 /** 968 * @param {TranslationsActor} actor 969 */ 970 async #reloadLangList(actor) { 971 try { 972 await this.#ensureLangListsBuilt(); 973 await this.#showDefaultView(actor); 974 } catch (error) { 975 this.elements.errorHintAction.disabled = false; 976 } 977 } 978 979 /** 980 * Handle telemetry events when buttons are invoked in the panel. 981 * 982 * @param {Event} event 983 */ 984 handlePanelButtonEvent(event) { 985 const { 986 cancelButton, 987 changeSourceLanguageButton, 988 dismissErrorButton, 989 restoreButton, 990 translateButton, 991 } = this.elements; 992 switch (event.target.id) { 993 case cancelButton.id: { 994 TranslationsParent.telemetry().fullPagePanel().onCancelButton(); 995 break; 996 } 997 case changeSourceLanguageButton.id: { 998 TranslationsParent.telemetry() 999 .fullPagePanel() 1000 .onChangeSourceLanguageButton(); 1001 break; 1002 } 1003 case dismissErrorButton.id: { 1004 TranslationsParent.telemetry().fullPagePanel().onDismissErrorButton(); 1005 break; 1006 } 1007 case restoreButton.id: { 1008 TranslationsParent.telemetry().fullPagePanel().onRestorePageButton(); 1009 break; 1010 } 1011 case translateButton.id: { 1012 TranslationsParent.telemetry().fullPagePanel().onTranslateButton(); 1013 break; 1014 } 1015 } 1016 } 1017 1018 /** 1019 * Handle telemetry events when popups are shown in the panel. 1020 * 1021 * @param {Event} event 1022 */ 1023 handlePanelPopupShownEvent(event) { 1024 const { panel, fromMenuList, toMenuList } = this.elements; 1025 switch (event.target.id) { 1026 case panel.id: { 1027 // This telemetry event is invoked externally because it requires 1028 // extra logic about from where the panel was opened and whether 1029 // or not the flow should be maintained or started anew. 1030 break; 1031 } 1032 case fromMenuList.firstChild.id: { 1033 TranslationsParent.telemetry().fullPagePanel().onOpenFromLanguageMenu(); 1034 break; 1035 } 1036 case toMenuList.firstChild.id: { 1037 TranslationsParent.telemetry().fullPagePanel().onOpenToLanguageMenu(); 1038 break; 1039 } 1040 } 1041 } 1042 1043 /** 1044 * Handle telemetry events when popups are hidden in the panel. 1045 * 1046 * @param {Event} event 1047 */ 1048 handlePanelPopupHiddenEvent(event) { 1049 const { panel, fromMenuList, toMenuList } = this.elements; 1050 switch (event.target.id) { 1051 case panel.id: { 1052 TranslationsParent.telemetry().fullPagePanel().onClose(); 1053 this.#isPopupOpen = false; 1054 this.elements.error.hidden = true; 1055 break; 1056 } 1057 case fromMenuList.firstChild.id: { 1058 TranslationsParent.telemetry() 1059 .fullPagePanel() 1060 .onCloseFromLanguageMenu(); 1061 break; 1062 } 1063 case toMenuList.firstChild.id: { 1064 TranslationsParent.telemetry().fullPagePanel().onCloseToLanguageMenu(); 1065 break; 1066 } 1067 } 1068 } 1069 1070 /** 1071 * Handle telemetry events when the settings menu is shown. 1072 */ 1073 handleSettingsPopupShownEvent() { 1074 TranslationsParent.telemetry().fullPagePanel().onOpenSettingsMenu(); 1075 } 1076 1077 /** 1078 * Handle telemetry events when the settings menu is hidden. 1079 */ 1080 handleSettingsPopupHiddenEvent() { 1081 TranslationsParent.telemetry().fullPagePanel().onCloseSettingsMenu(); 1082 } 1083 1084 /** 1085 * Opens the Translations panel popup at the given target. 1086 * 1087 * @param {object} target - The target element at which to open the popup. 1088 * @param {object} telemetryData 1089 * @param {string} telemetryData.event 1090 * The trigger event for opening the popup. 1091 * @param {string} telemetryData.viewName 1092 * The name of the view shown by the panel. 1093 * @param {boolean} telemetryData.autoShow 1094 * True if the panel was automatically opened, otherwise false. 1095 * @param {boolean} telemetryData.maintainFlow 1096 * Whether or not to maintain the flow of telemetry. 1097 */ 1098 async #openPanelPopup( 1099 target, 1100 { event = null, viewName = null, autoShow = false, maintainFlow = false } 1101 ) { 1102 const { panel, appMenuButton } = this.elements; 1103 const openedFromAppMenu = target.id === appMenuButton.id; 1104 const { docLangTag } = await this.#getCachedDetectedLanguages(); 1105 1106 TranslationsParent.telemetry().fullPagePanel().onOpen({ 1107 viewName, 1108 autoShow, 1109 docLangTag, 1110 maintainFlow, 1111 openedFromAppMenu, 1112 }); 1113 1114 this.#isPopupOpen = true; 1115 1116 PanelMultiView.openPopup(panel, target, { 1117 position: "bottomright topright", 1118 triggerEvent: event, 1119 }).catch(error => this.console?.error(error)); 1120 } 1121 1122 /** 1123 * Keeps track of open requests to guard against race conditions. 1124 * 1125 * @type {Promise<void> | null} 1126 */ 1127 #openPromise = null; 1128 1129 /** 1130 * Opens the FullPageTranslationsPanel. 1131 * 1132 * @param {Event} event 1133 * @param {boolean} reportAsAutoShow 1134 * True to report to telemetry that the panel was opened automatically, otherwise false. 1135 */ 1136 async open(event, reportAsAutoShow = false) { 1137 if (this.#openPromise) { 1138 // There is already an open event happening, do not open. 1139 return; 1140 } 1141 1142 this.#openPromise = this.#openImpl(event, reportAsAutoShow); 1143 this.#openPromise.finally(() => { 1144 this.#openPromise = null; 1145 }); 1146 } 1147 1148 /** 1149 * The language tag that was manually selected by the user. 1150 * This is used to remember the selection if the user happens to close and then reopen the panel prior to translating. 1151 * 1152 * @type {string | null} 1153 */ 1154 #manuallySelectedToLanguage = null; 1155 1156 /** 1157 * Implementation function for opening the panel. Prefer FullPageTranslationsPanel.open. 1158 * 1159 * @param {Event} event 1160 */ 1161 async #openImpl(event, reportAsAutoShow) { 1162 event.stopPropagation(); 1163 if ( 1164 (event.type == "click" && event.button != 0) || 1165 (event.type == "keypress" && 1166 event.charCode != KeyEvent.DOM_VK_SPACE && 1167 event.keyCode != KeyEvent.DOM_VK_RETURN) 1168 ) { 1169 // Allow only left click, space, or enter. 1170 return; 1171 } 1172 1173 const { button } = this.buttonElements; 1174 1175 const { requestedLanguagePair } = TranslationsParent.getTranslationsActor( 1176 gBrowser.selectedBrowser 1177 ).languageState; 1178 1179 await this.#ensureLangListsBuilt(); 1180 1181 if (requestedLanguagePair) { 1182 await this.#showRevisitView(requestedLanguagePair).catch(error => { 1183 this.console?.error(error); 1184 }); 1185 } else { 1186 await this.#showDefaultView( 1187 TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser) 1188 ).catch(error => { 1189 this.console?.error(error); 1190 }); 1191 } 1192 1193 this.#populateSettingsMenuItems(); 1194 1195 const targetButton = 1196 button.contains(event.target) || 1197 event.type === "TranslationsParent:OfferTranslation" 1198 ? button 1199 : this.elements.appMenuButton; 1200 1201 this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec); 1202 1203 await this.#openPanelPopup(targetButton, { 1204 event, 1205 autoShow: reportAsAutoShow, 1206 viewName: requestedLanguagePair ? "revisitView" : "defaultView", 1207 maintainFlow: false, 1208 }); 1209 } 1210 1211 /** 1212 * Returns true if translations is currently active, otherwise false. 1213 * 1214 * @returns {boolean} 1215 */ 1216 #isTranslationsActive() { 1217 const { requestedLanguagePair } = TranslationsParent.getTranslationsActor( 1218 gBrowser.selectedBrowser 1219 ).languageState; 1220 return requestedLanguagePair !== null; 1221 } 1222 1223 /** 1224 * Handle the translation button being clicked when there are two language options. 1225 */ 1226 async onTranslate() { 1227 this.#manuallySelectedToLanguage = null; 1228 PanelMultiView.hidePopup(this.elements.panel); 1229 1230 const actor = TranslationsParent.getTranslationsActor( 1231 gBrowser.selectedBrowser 1232 ); 1233 const [sourceLanguage, sourceVariant] = 1234 this.elements.fromMenuList.value.split(","); 1235 const [targetLanguage, targetVariant] = 1236 this.elements.toMenuList.value.split(","); 1237 1238 actor.translate( 1239 { sourceLanguage, targetLanguage, sourceVariant, targetVariant }, 1240 false // reportAsAutoTranslate 1241 ); 1242 } 1243 1244 /** 1245 * Handle the cancel button being clicked. 1246 */ 1247 onCancel() { 1248 PanelMultiView.hidePopup(this.elements.panel); 1249 } 1250 1251 /** 1252 * A handler for opening the settings context menu. 1253 */ 1254 openSettingsPopup(button) { 1255 this.#updateSettingsMenuLanguageCheckboxStates(); 1256 this.#updateSettingsMenuSiteCheckboxStates(); 1257 const popup = button.ownerDocument.getElementById( 1258 "full-page-translations-panel-settings-menupopup" 1259 ); 1260 popup.openPopup(button, "after_end"); 1261 } 1262 1263 /** 1264 * Creates a new CheckboxPageAction based on the current translated 1265 * state of the page and the state of the persistent options in the 1266 * translations panel settings. 1267 * 1268 * @returns {CheckboxPageAction} 1269 */ 1270 getCheckboxPageActionFor() { 1271 const { 1272 alwaysTranslateLanguageMenuItem, 1273 neverTranslateLanguageMenuItem, 1274 neverTranslateSiteMenuItem, 1275 } = this.elements; 1276 1277 const alwaysTranslateLanguage = 1278 alwaysTranslateLanguageMenuItem.hasAttribute("checked"); 1279 const neverTranslateLanguage = 1280 neverTranslateLanguageMenuItem.hasAttribute("checked"); 1281 const neverTranslateSite = 1282 neverTranslateSiteMenuItem.hasAttribute("checked"); 1283 1284 return new CheckboxPageAction( 1285 this.#isTranslationsActive(), 1286 alwaysTranslateLanguage, 1287 neverTranslateLanguage, 1288 neverTranslateSite 1289 ); 1290 } 1291 1292 /** 1293 * Redirect the user to about:preferences 1294 */ 1295 openManageLanguages() { 1296 TranslationsParent.telemetry().fullPagePanel().onManageLanguages(); 1297 const window = 1298 gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal; 1299 window.openTrustedLinkIn("about:preferences#general-translations", "tab"); 1300 } 1301 1302 /** 1303 * Performs the given page action. 1304 * 1305 * @param {PageAction} pageAction 1306 */ 1307 async #doPageAction(pageAction) { 1308 switch (pageAction) { 1309 case PageAction.NO_CHANGE: { 1310 break; 1311 } 1312 case PageAction.RESTORE_PAGE: { 1313 await this.onRestore(); 1314 break; 1315 } 1316 case PageAction.TRANSLATE_PAGE: { 1317 await this.onTranslate(); 1318 break; 1319 } 1320 case PageAction.CLOSE_PANEL: { 1321 PanelMultiView.hidePopup(this.elements.panel); 1322 break; 1323 } 1324 } 1325 } 1326 1327 /** 1328 * Updates the always-translate-language menuitem prefs and checked state. 1329 * If auto-translate is currently active for the doc language, deactivates it. 1330 * If auto-translate is currently inactive for the doc language, activates it. 1331 */ 1332 async onAlwaysTranslateLanguage() { 1333 const langTags = await this.#getCachedDetectedLanguages(); 1334 const { docLangTag } = langTags; 1335 if (!docLangTag) { 1336 throw new Error("Expected to have a document language tag."); 1337 } 1338 const pageAction = 1339 this.getCheckboxPageActionFor().alwaysTranslateLanguage(); 1340 const toggledOn = 1341 TranslationsParent.toggleAlwaysTranslateLanguagePref(langTags); 1342 TranslationsParent.telemetry() 1343 .fullPagePanel() 1344 .onAlwaysTranslateLanguage(docLangTag, toggledOn); 1345 this.#updateSettingsMenuLanguageCheckboxStates(); 1346 await this.#doPageAction(pageAction); 1347 } 1348 1349 /** 1350 * Toggle offering translations. 1351 */ 1352 async onAlwaysOfferTranslations() { 1353 const toggledOn = TranslationsParent.toggleAutomaticallyPopupPref(); 1354 TranslationsParent.telemetry() 1355 .fullPagePanel() 1356 .onAlwaysOfferTranslations(toggledOn); 1357 } 1358 1359 /** 1360 * Updates the never-translate-language menuitem prefs and checked state. 1361 * If never-translate is currently active for the doc language, deactivates it. 1362 * If never-translate is currently inactive for the doc language, activates it. 1363 */ 1364 async onNeverTranslateLanguage() { 1365 const { docLangTag } = await this.#getCachedDetectedLanguages(); 1366 if (!docLangTag) { 1367 throw new Error("Expected to have a document language tag."); 1368 } 1369 const pageAction = this.getCheckboxPageActionFor().neverTranslateLanguage(); 1370 const toggledOn = 1371 TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag); 1372 TranslationsParent.telemetry() 1373 .fullPagePanel() 1374 .onNeverTranslateLanguage(docLangTag, toggledOn); 1375 this.#updateSettingsMenuLanguageCheckboxStates(); 1376 await this.#doPageAction(pageAction); 1377 } 1378 1379 /** 1380 * Updates the never-translate-site menuitem permissions and checked state. 1381 * If never-translate is currently active for the site, deactivates it. 1382 * If never-translate is currently inactive for the site, activates it. 1383 */ 1384 async onNeverTranslateSite() { 1385 const pageAction = this.getCheckboxPageActionFor().neverTranslateSite(); 1386 const toggledOn = await TranslationsParent.getTranslationsActor( 1387 gBrowser.selectedBrowser 1388 ).toggleNeverTranslateSitePermissions(); 1389 TranslationsParent.telemetry() 1390 .fullPagePanel() 1391 .onNeverTranslateSite(toggledOn); 1392 this.#updateSettingsMenuSiteCheckboxStates(); 1393 await this.#doPageAction(pageAction); 1394 } 1395 1396 /** 1397 * Handle the restore button being clicked. 1398 */ 1399 async onRestore() { 1400 const { panel } = this.elements; 1401 PanelMultiView.hidePopup(panel); 1402 const { docLangTag } = await this.#getCachedDetectedLanguages(); 1403 if (!docLangTag) { 1404 throw new Error("Expected to have a document language tag."); 1405 } 1406 1407 TranslationsParent.getTranslationsActor( 1408 gBrowser.selectedBrowser 1409 ).restorePage(docLangTag); 1410 } 1411 1412 /** 1413 * An event handler that allows the FullPageTranslationsPanel object 1414 * to be compatible with the addTabsProgressListener function. 1415 * 1416 * @param {tabbrowser} browser 1417 */ 1418 onLocationChange(browser) { 1419 if (browser.currentURI.spec.startsWith("about:reader")) { 1420 // Hide the translations button when entering reader mode. 1421 this.buttonElements.button.hidden = true; 1422 } 1423 } 1424 1425 /** 1426 * Update the view to show an error. 1427 * 1428 * @param {TranslationParent} actor 1429 */ 1430 async #showEngineError(actor) { 1431 const { button } = this.buttonElements; 1432 await this.#ensureLangListsBuilt(); 1433 await this.#showDefaultView(actor).catch(e => { 1434 this.console?.error(e); 1435 }); 1436 this.elements.error.hidden = false; 1437 this.#showError({ 1438 message: "translations-panel-error-translating", 1439 }); 1440 const targetButton = button.hidden ? this.elements.appMenuButton : button; 1441 1442 // Re-open the menu on an error. 1443 await this.#openPanelPopup(targetButton, { 1444 autoShow: true, 1445 viewName: "errorView", 1446 maintainFlow: true, 1447 }); 1448 } 1449 1450 /** 1451 * Set the state of the translations button in the URL bar. 1452 * 1453 * @param {CustomEvent} event 1454 */ 1455 handleEvent = event => { 1456 const target = event.target; 1457 let { id } = target; 1458 1459 // If a menuitem within a menulist is the target, it will not have an id, 1460 // so we want to grab the closest relevant id. 1461 if (!id) { 1462 id = target.closest("[id]")?.id; 1463 } 1464 1465 switch (event.type) { 1466 case "command": { 1467 switch (id) { 1468 case "translations-panel-settings": 1469 this.openSettingsPopup(target); 1470 break; 1471 case "full-page-translations-panel-from-menupopup": 1472 this.onChangeFromLanguage(event); 1473 break; 1474 case "full-page-translations-panel-to-menupopup": 1475 this.onChangeToLanguage(event); 1476 break; 1477 case "full-page-translations-panel-restore-button": 1478 this.onRestore(event); 1479 break; 1480 case "full-page-translations-panel-cancel": 1481 case "full-page-translations-panel-dismiss-error": 1482 this.onCancel(event); 1483 break; 1484 case "full-page-translations-panel-translate": 1485 this.onTranslate(event); 1486 break; 1487 case "full-page-translations-panel-change-source-language": 1488 this.onChangeSourceLanguage(event); 1489 break; 1490 } 1491 break; 1492 } 1493 case "click": { 1494 switch (id) { 1495 case "full-page-translations-panel-intro-learn-more-link": 1496 case "full-page-translations-panel-unsupported-learn-more-link": 1497 this.onLearnMoreLink(); 1498 break; 1499 default: 1500 this.handlePanelButtonEvent(event); 1501 } 1502 break; 1503 } 1504 case "popupshown": 1505 this.handlePanelPopupShownEvent(event); 1506 break; 1507 case "popuphidden": 1508 this.handlePanelPopupHiddenEvent(event); 1509 break; 1510 case "TranslationsParent:OfferTranslation": { 1511 if (Services.wm.getMostRecentBrowserWindow()?.gBrowser === gBrowser) { 1512 this.open(event, /* reportAsAutoShow */ true); 1513 } 1514 break; 1515 } 1516 case "TranslationsParent:LanguageState": { 1517 const { actor, reason } = event.detail; 1518 1519 const innerWindowId = 1520 gBrowser.selectedBrowser.browsingContext.top.embedderElement 1521 .innerWindowID; 1522 1523 this.console?.debug("TranslationsParent:LanguageState", { 1524 reason, 1525 currentId: innerWindowId, 1526 originatorId: actor.innerWindowId, 1527 }); 1528 1529 if (innerWindowId !== actor.innerWindowId) { 1530 // The id of the currently active tab does not match the id of the tab that was active when the event was dispatched. 1531 // This likely means that the tab was changed after the event was dispatched, but before it was received by this class. 1532 // 1533 // Keep in mind that there is only one instance of this class (FullPageTranslationsPanel) for each open Firefox window, 1534 // but there is one instance of the TranslationsParent actor for each open tab within a Firefox window. As such, it is 1535 // possible for a tab-specific actor to fire an event that is received by the window-global panel after switching tabs. 1536 // 1537 // Since the dispatched event did not originate in the currently active tab, we should not react to it any further. 1538 return; 1539 } 1540 1541 const { 1542 detectedLanguages, 1543 requestedLanguagePair, 1544 error, 1545 isEngineReady, 1546 } = actor.languageState; 1547 1548 const { button, buttonLocale, buttonCircleArrows } = 1549 this.buttonElements; 1550 1551 const hasSupportedLanguage = 1552 detectedLanguages?.docLangTag && 1553 detectedLanguages?.userLangTag && 1554 detectedLanguages?.isDocLangTagSupported; 1555 1556 if (detectedLanguages) { 1557 // Ensure the cached detected languages are up to date, for instance whenever 1558 // the user switches tabs. 1559 FullPageTranslationsPanel.detectedLanguages = detectedLanguages; 1560 // Reset the manually selected language when the user switches tabs. 1561 this.#manuallySelectedToLanguage = null; 1562 } 1563 1564 if (this.#isPopupOpen) { 1565 // Make sure to use the language state that is passed by the event.detail, and 1566 // don't read it from the actor here, as it's possible the actor isn't available 1567 // via the gBrowser.selectedBrowser. 1568 this.#updateViewFromTranslationStatus(actor.languageState); 1569 } 1570 1571 if ( 1572 // We've already requested to translate this page, so always show the icon. 1573 requestedLanguagePair || 1574 // There was an error translating, so always show the icon. This can happen 1575 // when a user manually invokes the translation and we wouldn't normally show 1576 // the icon. 1577 error || 1578 // Finally check that we can translate this language. 1579 (hasSupportedLanguage && 1580 TranslationsParent.getIsTranslationsEngineSupported()) 1581 ) { 1582 // Keep track if the button was originally hidden, because it will be shown now. 1583 const wasButtonHidden = button.hidden; 1584 1585 button.hidden = false; 1586 if (requestedLanguagePair) { 1587 // The translation is active, update the urlbar button. 1588 button.setAttribute("translationsactive", true); 1589 if (isEngineReady) { 1590 const languageDisplayNames = 1591 TranslationsParent.createLanguageDisplayNames(); 1592 1593 document.l10n.setAttributes( 1594 button, 1595 "urlbar-translations-button-translated", 1596 { 1597 fromLanguage: languageDisplayNames.of( 1598 requestedLanguagePair.sourceLanguage 1599 ), 1600 toLanguage: languageDisplayNames.of( 1601 requestedLanguagePair.targetLanguage 1602 ), 1603 } 1604 ); 1605 // Show the language tag of translated the page in the button. 1606 buttonLocale.hidden = false; 1607 buttonCircleArrows.hidden = true; 1608 buttonLocale.innerText = 1609 requestedLanguagePair.targetLanguage.split("-")[0]; 1610 } else { 1611 document.l10n.setAttributes( 1612 button, 1613 "urlbar-translations-button-loading" 1614 ); 1615 // Show the spinning circle arrows to indicate that the engine is 1616 // still loading. 1617 buttonCircleArrows.hidden = false; 1618 buttonLocale.hidden = true; 1619 } 1620 } else { 1621 // The translation is not active, update the urlbar button. 1622 button.removeAttribute("translationsactive"); 1623 buttonLocale.hidden = true; 1624 buttonCircleArrows.hidden = true; 1625 1626 // Follow the same rules for displaying the first-run intro text for the 1627 // button's accessible tooltip label. 1628 if (TranslationsParent.hasUserEverTranslated()) { 1629 document.l10n.setAttributes( 1630 button, 1631 "urlbar-translations-button2" 1632 ); 1633 } else { 1634 document.l10n.setAttributes( 1635 button, 1636 "urlbar-translations-button-intro" 1637 ); 1638 } 1639 } 1640 1641 // The button was hidden, but now it is shown. 1642 if (wasButtonHidden) { 1643 PageActions.sendPlacedInUrlbarTrigger(button); 1644 } 1645 } else if (!button.hidden) { 1646 // There are no translations visible, hide the button. 1647 button.hidden = true; 1648 } 1649 1650 switch (error) { 1651 case null: 1652 break; 1653 case "engine-load-failure": 1654 this.#showEngineError(actor).catch(viewError => 1655 this.console?.error(viewError) 1656 ); 1657 break; 1658 default: 1659 console.error("Unknown translation error", error); 1660 } 1661 break; 1662 } 1663 } 1664 }; 1665 })();