Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / preferences / translations.js
blob9e8348aff18a001a13015a2037a36b60f9b068cd
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/. */
5 /* import-globals-from preferences.js */
7 /**
8 * @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages
9 */
11 /**
12 * The permission type to give to Services.perms for Translations.
14 const TRANSLATIONS_PERMISSION = "translations";
16 /**
17 * The list of BCP-47 language tags that will trigger auto-translate.
19 const ALWAYS_TRANSLATE_LANGS_PREF =
20 "browser.translations.alwaysTranslateLanguages";
22 /**
23 * The list of BCP-47 language tags that will prevent auto-translate.
25 const NEVER_TRANSLATE_LANGS_PREF =
26 "browser.translations.neverTranslateLanguages";
28 /**
29 * The topic fired to observers when a pref related to Translations changes.
31 const TOPIC_TRANSLATIONS_PREF_CHANGED = "translations:pref-changed";
33 let gTranslationsPane = {
34 /**
35 * List of languages set in the Always Translate Preferences
36 * @type Array<string>
38 alwaysTranslateLanguages: [],
40 /**
41 * List of languages set in the Never Translate Preferences
42 * @type Array<string>
44 neverTranslateLanguages: [],
46 /**
47 * List of languages set in the Never Translate Site Preferences
48 * @type Array<string>
50 neverTranslateSites: [],
52 /**
53 * A mapping from the language tag to the current download phase for that language
54 * and it's download size.
55 * @type {Map<string, {downloadPhase: "downloaded" | "removed" | "loading", size: number}>}
57 downloadPhases: new Map(),
59 /**
60 * Object with details of languages supported by the browser.
62 * @type {SupportedLanguages}
64 supportedLanguages: {},
66 /**
67 * List of languages names supported along with their tags (BCP 47 locale identifiers).
68 * @type Array<{ langTag: string, displayName: string}>
70 supportedLanguageTagsNames: [],
72 /**
73 * Add Lazy getter for document elements
75 elements: undefined,
77 async init() {
78 if (!this.elements) {
79 this._defineLazyElements(document, {
80 downloadLanguageSection: "translations-settings-download-section",
81 alwaysTranslateMenuList: "translations-settings-always-translate-list",
82 neverTranslateMenuList: "translations-settings-never-translate-list",
83 alwaysTranslateMenuPopup:
84 "translations-settings-always-translate-popup",
85 neverTranslateMenuPopup: "translations-settings-never-translate-popup",
86 downloadLanguageList: "translations-settings-download-language-list",
87 alwaysTranslateLanguageList:
88 "translations-settings-always-translate-language-list",
89 neverTranslateLanguageList:
90 "translations-settings-never-translate-language-list",
91 neverTranslateSiteList:
92 "translations-settings-never-translate-site-list",
93 translationsSettingsBackButton: "translations-settings-back-button",
94 translationsSettingsHeader: "translations-settings-header",
95 translationsSettingsDescription: "translations-settings-description",
96 translateAlwaysHeader: "translations-settings-always-translate",
97 translateNeverHeader: "translations-settings-never-translate",
98 translateNeverSiteHeader: "translations-settings-never-sites-header",
99 translateNeverSiteDesc: "translations-settings-never-sites",
100 translateDownloadLanguagesLearnMore: "download-languages-learn-more",
103 this.elements.translationsSettingsBackButton.addEventListener(
104 "click",
105 function () {
106 gotoPref("general");
110 // Keyboard navigation support.
111 this.elements.alwaysTranslateMenuList.addEventListener("keydown", this);
112 this.elements.alwaysTranslateMenuPopup.addEventListener(
113 "popuphidden",
114 this
116 this.elements.neverTranslateMenuList.addEventListener("keydown", this);
117 this.elements.neverTranslateMenuPopup.addEventListener("popuphidden", this);
119 // Get the settings from the preferences into the translations.js
120 this.supportedLanguages = await TranslationsParent.getSupportedLanguages();
121 this.supportedLanguageTagsNames = TranslationsParent.getLanguageList(
122 this.supportedLanguages
125 this.neverTranslateSites = TranslationsParent.listNeverTranslateSites();
127 // Deploy observers
128 Services.obs.addObserver(this, "perm-changed");
129 Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED);
130 window.addEventListener("unload", () => this.removeObservers());
132 // Build the HTML elements
133 this.buildLanguageDropDowns();
134 // Keyboard navigation support.
135 this.elements.alwaysTranslateLanguageList.addEventListener("keydown", this);
136 this.elements.neverTranslateLanguageList.addEventListener("keydown", this);
137 this.elements.neverTranslateSiteList.addEventListener("keydown", this);
138 this.populateLanguageList(ALWAYS_TRANSLATE_LANGS_PREF);
139 this.populateLanguageList(NEVER_TRANSLATE_LANGS_PREF);
140 this.populateSiteList();
142 await this.initDownloadInfo();
143 this.buildDownloadLanguageList();
145 // The translations settings page takes a long time to initialize
146 // This event can be used to wait until the initialization is done.
147 document.dispatchEvent(
148 new CustomEvent("translationsSettingsInit", {
149 bubbles: true,
150 cancelable: true,
155 _defineLazyElements(document, entries) {
156 this.elements = {};
157 for (const [name, elementId] of Object.entries(entries)) {
158 ChromeUtils.defineLazyGetter(this.elements, name, () => {
159 const element = document.getElementById(elementId);
160 if (!element) {
161 throw new Error(`Could not find "${name}" at "#${elementId}".`);
163 return element;
169 * Populate the Drop down list in <menupopup> with the list of supported languages
170 * for the user to choose languages to add to Always translate and
171 * Never translate settings list.
173 buildLanguageDropDowns() {
174 const { sourceLanguages } = this.supportedLanguages;
175 const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements;
177 for (const { langTag, displayName } of sourceLanguages) {
178 const alwaysLang = document.createXULElement("menuitem");
179 alwaysLang.setAttribute("value", langTag);
180 alwaysLang.setAttribute("label", displayName);
181 alwaysTranslateMenuPopup.appendChild(alwaysLang);
182 const neverLang = document.createXULElement("menuitem");
183 neverLang.setAttribute("value", langTag);
184 neverLang.setAttribute("label", displayName);
185 neverTranslateMenuPopup.appendChild(neverLang);
190 * Initializes the downloadPhases by checking the download status of each language.
192 * @see gTranslationsPane.downloadPhases
194 async initDownloadInfo() {
195 let downloadCount = 0;
196 let allDownloadSize = 0;
198 this.downloadPhases = new Map();
199 for (const language of this.supportedLanguageTagsNames) {
200 let downloadSize = await TranslationsParent.getLanguageSize(
201 language.langTag
203 allDownloadSize += downloadSize;
204 const hasAllFilesForLanguage =
205 await TranslationsParent.hasAllFilesForLanguage(language.langTag);
206 const downloadPhase = hasAllFilesForLanguage ? "downloaded" : "removed";
207 this.downloadPhases.set(language.langTag, {
208 downloadPhase,
209 size: downloadSize,
211 downloadCount += downloadPhase === "downloaded" ? 1 : 0;
213 const allDownloadPhase =
214 downloadCount === this.supportedLanguageTagsNames.length
215 ? "downloaded"
216 : "removed";
217 this.downloadPhases.set("all", {
218 downloadPhase: allDownloadPhase,
219 size: allDownloadSize,
224 * Show a list of languages for the user to be able to download
225 * and remove language models for local translation.
227 buildDownloadLanguageList() {
228 const { downloadLanguageList } = this.elements;
230 function createSizeElement(downloadSize) {
231 const languageSize = document.createElement("span");
232 languageSize.classList.add("translations-settings-download-size");
233 const [size, units] = DownloadUtils.convertByteUnits(downloadSize);
235 document.l10n.setAttributes(
236 languageSize,
237 "translations-settings-download-size",
239 size: size + " " + units,
242 return languageSize;
245 // The option to download "All languages" is added in xhtml.
246 // Here the option to download individual languages is dynamically added
247 // based on the supported language list
248 const allLangElement = downloadLanguageList.firstElementChild;
249 let allLangButton = allLangElement.querySelector("moz-button");
251 // The first element is selected by default when keyboard navigation enters this list
252 downloadLanguageList.setAttribute(
253 "aria-activedescendant",
254 allLangElement.id
256 // Keyboard navigation support.
257 downloadLanguageList.addEventListener("keydown", this);
258 allLangButton.addEventListener("click", this);
259 allLangElement.addEventListener("keydown", this);
261 for (const language of this.supportedLanguageTagsNames) {
262 const downloadSize = this.downloadPhases.get(language.langTag).size;
264 const languageSize = createSizeElement(downloadSize);
266 const languageLabel = this.createLangLabel(
267 language.displayName,
268 language.langTag,
269 "translations-settings-download-" + language.langTag
272 const isDownloaded =
273 this.downloadPhases.get(language.langTag).downloadPhase ===
274 "downloaded";
276 const mozButton = isDownloaded
277 ? this.createIconButton(
279 "translations-settings-remove-icon",
280 "translations-settings-manage-downloaded-language-button",
282 "translations-settings-remove-button",
283 language.displayName
285 : this.createIconButton(
287 "translations-settings-download-icon",
288 "translations-settings-manage-downloaded-language-button",
290 "translations-settings-download-button",
291 language.displayName
294 const languageElement = this.createLangElement(
295 [mozButton, languageLabel, languageSize],
296 "translations-settings-download-" + language.langTag + "-language-id"
298 downloadLanguageList.appendChild(languageElement);
301 // Updating "All Language" download button according to the state
302 if (this.downloadPhases.get("all").downloadPhase === "downloaded") {
303 this.changeButtonState({
304 langButton: allLangButton,
305 langTag: "all",
306 langState: "downloaded",
310 const allDownloadSize = this.downloadPhases.get("all").size;
311 const languageSize = createSizeElement(allDownloadSize);
313 allLangElement.appendChild(languageSize);
316 handleEvent(event) {
317 const eventNode = event.target;
318 const eventNodeParent = eventNode.parentNode;
319 const eventNodeClassList = eventNode.classList;
320 for (const err of document.querySelectorAll(
321 ".translations-settings-language-error"
322 )) {
323 this.removeError(err);
326 switch (event.type) {
327 case "keydown":
328 // Keyboard navigation support.
329 this.handleKeys(event);
330 break;
331 case "popuphidden":
332 // Handle Menulist selection through pointing device
333 if (
334 eventNodeParent.id === "translations-settings-always-translate-list"
336 this.handleAddAlwaysTranslateLanguage(
337 event.target.parentNode.getAttribute("value")
339 } else if (
340 eventNodeParent.id === "translations-settings-never-translate-list"
342 this.handleAddNeverTranslateLanguage(
343 event.target.parentNode.getAttribute("value")
346 break;
347 case "click":
348 if (eventNodeClassList.contains("translations-settings-site-button")) {
349 this.handleRemoveNeverTranslateSite(event);
350 } else if (
351 eventNodeClassList.contains(
352 "translations-settings-language-never-button"
355 this.handleRemoveNeverTranslateLanguage(event);
356 } else if (
357 eventNodeClassList.contains(
358 "translations-settings-language-always-button"
361 this.handleRemoveAlwaysTranslateLanguage(event);
362 } else if (
363 eventNodeClassList.contains(
364 "translations-settings-manage-downloaded-language-button"
367 if (
368 eventNodeClassList.contains("translations-settings-download-icon")
370 if (
371 eventNodeParent.querySelector("label").id ===
372 "translations-settings-download-all-languages"
374 this.handleDownloadAllLanguages(event);
375 } else {
376 this.handleDownloadLanguage(event);
378 } else if (
379 eventNodeClassList.contains("translations-settings-remove-icon")
381 if (
382 eventNodeParent.querySelector("label").id ===
383 "translations-settings-download-all-languages"
385 this.handleRemoveAllDownloadLanguages(event);
386 } else {
387 this.handleRemoveDownloadLanguage(event);
391 break;
395 // Keyboard navigation support.
396 handleKeys(event) {
397 switch (event.key) {
398 case "Enter":
399 // Handle Menulist selection through keyboard
400 if (event.target.id === "translations-settings-always-translate-list") {
401 this.handleAddAlwaysTranslateLanguage(
402 event.target.getAttribute("value")
404 } else if (
405 event.target.id === "translations-settings-never-translate-list"
407 this.handleAddNeverTranslateLanguage(
408 event.target.getAttribute("value")
411 break;
412 case "ArrowUp":
413 if (
414 event.target.classList.contains("translations-settings-language-list")
416 event.target.children[0].querySelector("moz-button").focus();
417 // Update the selected element on the list according to the keyboard navigation by the user
418 event.target.setAttribute(
419 "aria-activedescendant",
420 event.target.children[0].id
422 } else if (event.target.tagName === "moz-button") {
423 if (event.target.parentNode.previousElementSibling) {
424 event.target.parentNode.previousElementSibling
425 .querySelector("moz-button")
426 .focus();
427 // Update the selected element on the list according to the keyboard navigation by the user
428 event.target.parentNode.parentNode.setAttribute(
429 "aria-activedescendant",
430 event.target.parentNode.previousElementSibling.id
432 event.preventDefault();
435 break;
436 case "ArrowDown":
437 if (
438 event.target.classList.contains("translations-settings-language-list")
440 event.target.children[0].querySelector("moz-button").focus();
441 // Update the selected element on the list according to the keyboard navigation by the user
442 event.target.setAttribute(
443 "aria-activedescendant",
444 event.target.children[0].id
446 } else if (event.target.tagName === "moz-button") {
447 if (event.target.parentNode.nextElementSibling) {
448 event.target.parentNode.nextElementSibling
449 .querySelector("moz-button")
450 .focus();
451 // Update the selected element on the list according to the keyboard navigation by the user
452 event.target.parentNode.parentNode.setAttribute(
453 "aria-activedescendant",
454 event.target.parentNode.nextElementSibling.id
456 event.preventDefault();
459 break;
464 * Event handler when the user wants to add a language to
465 * Always translate settings preferences list.
466 * @param {Event} event
468 async handleAddAlwaysTranslateLanguage(langTag) {
469 // After a language is selected the menulist button display will be set to the
470 // selected langauge. After processing the button event the
471 // data-l10n-id of the menulist button is restored to "Add Language"
473 const { alwaysTranslateMenuList } = this.elements;
474 TranslationsParent.addLangTagToPref(langTag, ALWAYS_TRANSLATE_LANGS_PREF);
475 await document.l10n.translateElements([alwaysTranslateMenuList]);
479 * Event handler when the user wants to add a language to
480 * Never translate settings preferences list.
481 * @param {Event} event
483 async handleAddNeverTranslateLanguage(langTag) {
484 // After a language is selected the menulist button display will be set to the
485 // selected langauge. After processing the button event the
486 // data-l10n-id of the menulist button is restored to "Add Language"
488 const { neverTranslateMenuList } = this.elements;
490 TranslationsParent.addLangTagToPref(langTag, NEVER_TRANSLATE_LANGS_PREF);
491 await document.l10n.translateElements([neverTranslateMenuList]);
495 * Finds the langauges added and/or removed in the
496 * Always/Never translate lists.
497 * @param {Array<string>} currentSet
498 * @param {Array<string>} newSet
499 * @returns {Object} {Array<string>, Array<string>}
501 setDifference(currentSet, newSet) {
502 const added = newSet.filter(lang => !currentSet.includes(lang));
503 const removed = currentSet.filter(lang => !newSet.includes(lang));
504 return { added, removed };
508 * Builds HTML elements for the Always/Never translate list
509 * According to the preference setting
510 * @param {string} pref - name of the preference for which the HTML is built
511 * NEVER_TRANSLATE_LANGS_PREF / ALWAYS_TRANSLATE_LANGS_PREF
513 populateLanguageList(pref) {
514 // languageList: <div> of the Always/Never translate section, which is a list of languages added by the user
515 // curLangTags: List of Language tag set in the the preference, Always/Never translate to be populated
516 // otherPref: name of the preference other than "pref" Never/Always
517 // when a language is added to "pref" remove the same from otherPref(if it exists)
518 // prefix: "always"/"never" string used to create ids for the language HTML elements for respective lists.
520 const { languageList, curLangTags, otherPref, prefix } =
521 pref === NEVER_TRANSLATE_LANGS_PREF
523 languageList: this.elements.neverTranslateLanguageList,
524 curLangTags: Array.from(this.neverTranslateLanguages),
525 otherPref: ALWAYS_TRANSLATE_LANGS_PREF,
526 prefix: "never",
529 languageList: this.elements.alwaysTranslateLanguageList,
530 curLangTags: Array.from(this.alwaysTranslateLanguages),
531 otherPref: NEVER_TRANSLATE_LANGS_PREF,
532 prefix: "always",
535 const updatedLangTags =
536 pref === NEVER_TRANSLATE_LANGS_PREF
537 ? Array.from(TranslationsParent.getNeverTranslateLanguages())
538 : Array.from(TranslationsParent.getAlwaysTranslateLanguages());
540 const { added, removed } = this.setDifference(curLangTags, updatedLangTags);
542 for (const lang of removed) {
543 this.removeTranslateLanguage(lang, languageList);
546 // When the preferences is opened for the first time
547 // the translations settings HTML page is initialized with
548 // the existing settings by adding all languages from the latest preferences
549 for (const lang of added) {
550 this.addTranslateLanguage(lang, languageList, prefix);
551 // if a language is added to Always translate list,
552 // remove it from Never translate list and vice-versa
553 TranslationsParent.removeLangTagFromPref(lang, otherPref);
556 // Update state for neverTranslateLanguages/alwaysTranslateLanguages
557 if (pref === NEVER_TRANSLATE_LANGS_PREF) {
558 this.neverTranslateLanguages = updatedLangTags;
559 } else {
560 this.alwaysTranslateLanguages = updatedLangTags;
565 * Adds a site to Never translate site list
566 * @param {string} site
568 addSite(site) {
569 const { neverTranslateSiteList } = this.elements;
571 // Label and textContent of the added site element is the same
572 const languageLabel = this.createLangLabel(
573 site,
574 site,
575 "translations-settings-" + site
578 const mozButton = this.createIconButton(
580 "translations-settings-remove-icon",
581 "translations-settings-site-button",
583 "translations-settings-remove-site-button-2",
584 site
587 // Create unique id using site name
588 const languageElement = this.createLangElement(
589 [mozButton, languageLabel],
590 "translations-settings-" + site + "-id"
592 neverTranslateSiteList.insertBefore(
593 languageElement,
594 neverTranslateSiteList.firstElementChild
596 // The first element is selected by default when keyboard navigation enters this list
597 neverTranslateSiteList.setAttribute(
598 "aria-activedescendant",
599 languageElement.id
601 if (neverTranslateSiteList.childElementCount) {
602 neverTranslateSiteList.parentNode.hidden = false;
607 * Removes a site from Never translate site list
608 * @param {string} site
610 removeSite(site) {
611 const { neverTranslateSiteList } = this.elements;
613 const langSite = neverTranslateSiteList.querySelector(
614 `label[value="${site}"]`
617 langSite.parentNode.remove();
618 if (!neverTranslateSiteList.childElementCount) {
619 neverTranslateSiteList.parentNode.hidden = true;
624 * Builds HTML elements for the Never translate Site list
625 * According to the permissions setting
627 populateSiteList() {
628 const siteList = TranslationsParent.listNeverTranslateSites();
629 for (const site of siteList) {
630 this.addSite(site);
632 this.neverTranslateSites = siteList;
636 * Oberver
637 * @param {string} subject Notification specific interface pointer.
638 * @param {string} topic nsPref:changed/perm-changed
639 * @param {string} data cleared/changed/added/deleted
641 observe(subject, topic, data) {
642 if (topic === "perm-changed") {
643 if (data === "cleared") {
644 const { neverTranslateSiteList } = this.elements;
645 this.neverTranslateSites = [];
646 for (const elem of neverTranslateSiteList.children) {
647 elem.remove();
649 if (!neverTranslateSiteList.childElementCount) {
650 neverTranslateSiteList.parentNode.hidden = true;
652 } else {
653 const perm = subject.QueryInterface(Ci.nsIPermission);
654 if (perm.type != TRANSLATIONS_PERMISSION) {
655 // The updated permission was not for Translations, nothing to do.
656 return;
658 if (data === "added") {
659 if (perm.capability != Services.perms.DENY_ACTION) {
660 // We are only showing data for sites we should never translate.
661 // If the permission is not DENY_ACTION, we don't care about it here.
662 return;
664 this.neverTranslateSites =
665 TranslationsParent.listNeverTranslateSites();
666 this.addSite(perm.principal.origin);
667 } else if (data === "deleted") {
668 this.neverTranslateSites =
669 TranslationsParent.listNeverTranslateSites();
670 this.removeSite(perm.principal.origin);
673 } else if (topic === TOPIC_TRANSLATIONS_PREF_CHANGED) {
674 switch (data) {
675 case ALWAYS_TRANSLATE_LANGS_PREF:
676 case NEVER_TRANSLATE_LANGS_PREF: {
677 this.populateLanguageList(data);
678 break;
685 * Removes Observers
687 removeObservers() {
688 Services.obs.removeObserver(this, "perm-changed");
689 Services.obs.removeObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED);
693 * Create a div HTML element representing a language.
694 * @param {Array} langChildren
695 * @returns {Element} div HTML element
697 createLangElement(langChildren, langId) {
698 const languageElement = document.createElement("div");
699 languageElement.classList.add("translations-settings-language");
700 // Keyboard navigation support
701 languageElement.setAttribute("role", "option");
702 languageElement.id = langId;
703 languageElement.addEventListener("keydown", this);
705 for (const child of langChildren) {
706 languageElement.appendChild(child);
708 return languageElement;
712 * Creates a moz-button element as icon
713 * @param {string} classNames classes added to the moz-button element
714 * @param {string} buttonFluentID Fluent ID for the aria-label
715 * @param {string} accessibleName "name" variable value of the aria-label
716 * @returns {Element} HTML element of type Moz-Button
718 createIconButton(classNames, buttonFluentID, accessibleName) {
719 const mozButton = document.createElement("moz-button");
721 for (const className of classNames) {
722 mozButton.classList.add(className);
724 mozButton.setAttribute("type", "ghost icon");
725 // Note: aria-labelledby cannot be used as the id is not available for the shadow DOM element
726 document.l10n.setAttributes(mozButton, buttonFluentID, {
727 name: accessibleName,
729 mozButton.addEventListener("click", this);
730 // Keyboard navigation support. Do not select the buttons on the list using tab.
731 // The buttons in the language lists are navigated using arrow buttons
732 mozButton.setAttribute("tabindex", "-1");
733 return mozButton;
737 * Adds a language selected by the user to the list of
738 * Always/Never translate settings list in the HTML.
739 * @param {string} langTag - The BCP-47 language tag for the language
740 * @param {Element} languageList - HTML element for the list of the languages.
741 * @param {string} translatePrefix - "never" / "always" prefix depending on the settings section
743 addTranslateLanguage(langTag, languageList, translatePrefix) {
744 // While adding the first language, add the Header and language List div
745 const languageDisplayNames =
746 TranslationsParent.createLanguageDisplayNames();
748 let languageDisplayName;
749 try {
750 languageDisplayName = languageDisplayNames.of(langTag);
751 } catch (error) {
752 console.warn(
753 `Failed to retrieve language display name for '${langTag}'.`
755 return;
758 const languageLabel = this.createLangLabel(
759 languageDisplayName,
760 langTag,
761 "translations-settings-language-" + translatePrefix + "-" + langTag
764 const mozButton = this.createIconButton(
766 "translations-settings-remove-icon",
767 "translations-settings-language-" + translatePrefix + "-button",
769 "translations-settings-remove-language-button-2",
770 languageDisplayName
773 const languageElement = this.createLangElement(
774 [mozButton, languageLabel],
775 "translations-settings-language-" +
776 translatePrefix +
777 "-" +
778 langTag +
779 "-id"
781 // Add the language after the Language Header
782 languageList.insertBefore(languageElement, languageList.firstElementChild);
783 // The first element is selected by default when keyboard navigation enters this list
784 languageList.setAttribute("aria-activedescendant", languageElement.id);
785 if (languageList.childElementCount) {
786 languageList.parentNode.hidden = false;
791 * Creates a label HTML element representing
792 * a language
793 * @param {string} textContent
794 * @param {string} value
795 * @param {string} id
796 * @returns {Element} HTML element of type label
798 createLangLabel(textContent, value, id) {
799 const languageLabel = document.createElement("label");
800 languageLabel.textContent = textContent;
801 languageLabel.setAttribute("value", value);
802 languageLabel.id = id;
803 return languageLabel;
807 * Removes a language currently in the always/never translate language list
808 * from the DOM. Invoked in response to changes in the relevant preferences.
809 * @param {string} langTag The BCP-47 language tag for the language
810 * @param {Element} languageList - HTML element for the list of the languages.
812 removeTranslateLanguage(langTag, languageList) {
813 const langElem = languageList.querySelector(`label[value=${langTag}]`);
814 if (langElem) {
815 langElem.parentNode.remove();
817 if (!languageList.childElementCount) {
818 languageList.parentNode.hidden = true;
823 * Event Handler to remove a language selected by the user from the list of
824 * Always translate settings list in Preferences.
825 * @param {Event} event
827 handleRemoveAlwaysTranslateLanguage(event) {
828 TranslationsParent.removeLangTagFromPref(
829 event.target.parentNode.querySelector("label").getAttribute("value"),
830 ALWAYS_TRANSLATE_LANGS_PREF
835 * Event Handler to remove a language selected by the user from the list of
836 * Never translate settings list in Preferences.
837 * @param {Event} event
839 handleRemoveNeverTranslateLanguage(event) {
840 TranslationsParent.removeLangTagFromPref(
841 event.target.parentNode.querySelector("label").getAttribute("value"),
842 NEVER_TRANSLATE_LANGS_PREF
847 * Removes the site chosen by the user in the HTML
848 * from the Never Translate Site Permission
849 * @param {Event} event
851 handleRemoveNeverTranslateSite(event) {
852 TranslationsParent.setNeverTranslateSiteByOrigin(
853 false,
854 event.target.parentNode.querySelector("label").getAttribute("value")
858 * Record the download phase downloaded/loading/removed for
859 * given language in the local data.
860 * @param {string} langTag
861 * @param {string} downloadPhase
863 updateDownloadPhase(langTag, downloadPhase) {
864 if (!this.downloadPhases.has(langTag)) {
865 console.error(
866 `Expected downloadPhases entry for ${langTag}, but found none.`
868 } else {
869 this.downloadPhases.get(langTag).downloadPhase = downloadPhase;
873 * Updates the button icons and its download states for the download language elements
874 * in the HTML by getting the download status of all languages from the browser records.
876 async reloadDownloadPhases() {
877 let downloadCount = 0;
878 const { downloadLanguageList } = this.elements;
880 const allLangElem = downloadLanguageList.firstElementChild;
881 const allLangButton = allLangElem.querySelector("moz-button");
883 const updatePromises = [];
884 for (const langElem of downloadLanguageList.querySelectorAll(
885 ".translations-settings-language:not(:first-child)"
886 )) {
887 const langLabel = langElem.querySelector("label");
888 const langTag = langLabel.getAttribute("value");
889 const langButton = langElem.querySelector("moz-button");
891 updatePromises.push(
892 TranslationsParent.hasAllFilesForLanguage(langTag).then(
893 hasAllFilesForLanguage => {
894 if (hasAllFilesForLanguage) {
895 downloadCount += 1;
896 this.changeButtonState({
897 langButton,
898 langTag,
899 langState: "downloaded",
901 } else {
902 this.changeButtonState({
903 langButton,
904 langTag,
905 langState: "removed",
908 langButton.removeAttribute("disabled");
913 await Promise.allSettled(updatePromises);
915 const allDownloaded =
916 downloadCount === this.supportedLanguageTagsNames.length;
917 if (allDownloaded) {
918 this.changeButtonState({
919 langButton: allLangButton,
920 langTag: "all",
921 langState: "downloaded",
923 } else {
924 this.changeButtonState({
925 langButton: allLangButton,
926 langTag: "all",
927 langState: "removed",
932 showErrorMessage(parentNode, fluentId, language) {
933 const errorElement = document.createElement("moz-message-bar");
934 errorElement.setAttribute("type", "error");
935 document.l10n.setAttributes(errorElement, fluentId, {
936 name: language,
938 errorElement.classList.add("translations-settings-language-error");
939 parentNode.appendChild(errorElement);
942 removeError(errorNode) {
943 errorNode?.remove();
947 * Event Handler to download a language model selected by the user through HTML
948 * @param {Event} event
950 async handleDownloadLanguage(event) {
951 let eventButton = event.target;
952 const langTag = eventButton.parentNode
953 .querySelector("label")
954 .getAttribute("value");
956 this.changeButtonState({
957 langButton: eventButton,
958 langTag,
959 langState: "loading",
962 try {
963 await TranslationsParent.downloadLanguageFiles(langTag);
964 } catch (error) {
965 console.error(error);
967 const languageDisplayNames =
968 TranslationsParent.createLanguageDisplayNames();
970 this.showErrorMessage(
971 eventButton.parentNode,
972 "translations-settings-language-download-error",
973 languageDisplayNames.of(langTag)
975 const hasAllFilesForLanguage =
976 await TranslationsParent.hasAllFilesForLanguage(langTag);
978 if (!hasAllFilesForLanguage) {
979 this.changeButtonState({
980 langButton: eventButton,
981 langTag,
982 langState: "removed",
984 return;
987 this.changeButtonState({
988 langButton: eventButton,
989 langTag,
990 langState: "downloaded",
993 // If all languages are downloaded, change "All Languages" to downloaded
994 const haveRemovedItem = [...this.downloadPhases].some(
995 ([k, v]) => v.downloadPhase != "downloaded" && k != "all"
997 if (
998 !haveRemovedItem &&
999 this.downloadPhases.get("all").downloadPhase !== "downloaded"
1001 this.changeButtonState({
1002 langButton:
1003 this.elements.downloadLanguageList.firstElementChild.querySelector(
1004 "moz-button"
1006 langTag: "all",
1007 langState: "downloaded",
1013 * Event Handler to remove a language model selected by the user through HTML
1014 * @param {Event} event
1016 async handleRemoveDownloadLanguage(event) {
1017 let eventButton = event.target;
1018 const langTag = eventButton.parentNode
1019 .querySelector("label")
1020 .getAttribute("value");
1022 this.changeButtonState({
1023 langButton: eventButton,
1024 langTag,
1025 langState: "loading",
1028 try {
1029 await TranslationsParent.deleteLanguageFiles(langTag);
1030 } catch (error) {
1031 // The download phases are invalidated with the error and must be reloaded.
1032 console.error(error);
1034 const languageDisplayNames =
1035 TranslationsParent.createLanguageDisplayNames();
1037 this.showErrorMessage(
1038 eventButton.parentNode,
1039 "translations-settings-language-remove-error",
1040 languageDisplayNames.of(langTag)
1042 const hasAllFilesForLanguage =
1043 await TranslationsParent.hasAllFilesForLanguage(langTag);
1044 if (hasAllFilesForLanguage) {
1045 this.changeButtonState({
1046 langButton: eventButton,
1047 langTag,
1048 langState: "downloaded",
1050 return;
1054 this.changeButtonState({
1055 langButton: eventButton,
1056 langTag,
1057 langState: "removed",
1060 // If >=1 languages are removed change "All Languages" state to removed
1061 if (this.downloadPhases.get("all").downloadPhase === "downloaded") {
1062 this.changeButtonState({
1063 langButton:
1064 this.elements.downloadLanguageList.firstElementChild.querySelector(
1065 "moz-button"
1067 langTag: "all",
1068 langState: "removed",
1074 * Event Handler to download all language models
1075 * @param {Event} event
1077 async handleDownloadAllLanguages(event) {
1078 // Disable all buttons and show loading icon
1079 this.disableDownloadButtons();
1080 let eventButton = event.target;
1081 this.changeButtonState({
1082 langButton: eventButton,
1083 langTag: "all",
1084 langState: "loading",
1087 try {
1088 await TranslationsParent.downloadAllFiles();
1089 } catch (error) {
1090 console.error(error);
1091 await this.reloadDownloadPhases();
1092 this.showErrorMessage(
1093 eventButton.parentNode,
1094 "translations-settings-language-download-error",
1095 "all"
1097 return;
1099 this.changeButtonState({
1100 langButton: eventButton,
1101 langTag: "all",
1102 langState: "downloaded",
1104 this.updateAllLanguageDownloadButtons("downloaded");
1108 * Event Handler to remove all language models
1109 * @param {Event} event
1111 async handleRemoveAllDownloadLanguages(event) {
1112 let eventButton = event.target;
1113 this.disableDownloadButtons();
1114 this.changeButtonState({
1115 langButton: eventButton,
1116 langTag: "all",
1117 langState: "loading",
1120 try {
1121 await TranslationsParent.deleteAllLanguageFiles();
1122 } catch (error) {
1123 console.error(error);
1124 await this.reloadDownloadPhases();
1125 this.showErrorMessage(
1126 eventButton.parentNode,
1127 "translations-settings-language-remove-error",
1128 "all"
1130 return;
1132 this.changeButtonState({
1133 langButton: eventButton,
1134 langTag: "all",
1135 langState: "removed",
1137 this.updateAllLanguageDownloadButtons("removed");
1141 * Disables the buttons to download/remove inidividual languages
1142 * when "all languages" are downloaded/removed.
1143 * This is done to ensure that no individual languages are downloaded/removed
1144 * when the download/remove operations for "all languages" is progress.
1146 disableDownloadButtons() {
1147 const { downloadLanguageList } = this.elements;
1149 // Disable all elements except the first one which is "All langauges"
1150 for (const langElem of downloadLanguageList.querySelectorAll(
1151 ".translations-settings-language:not(:first-child)"
1152 )) {
1153 const langButton = langElem.querySelector("moz-button");
1154 langButton.setAttribute("disabled", "true");
1159 * Changes the state of all individual language buttons as downloaded/removed
1160 * based on the download state of "All Language" status
1161 * changes the icon of individual language buttons:
1162 * from "download" icon to "remove" icon if "All Language" is downloaded.
1163 * from "remove" icon to "download" icon if "All Language" is removed.
1164 * @param {string} allLanguageDownloadStatus "All Language" status: downloaded/removed
1166 updateAllLanguageDownloadButtons(allLanguageDownloadStatus) {
1167 const { downloadLanguageList } = this.elements;
1169 // Change the state of all individual language buttons except the first one which is "All langauges"
1170 for (const langElem of downloadLanguageList.querySelectorAll(
1171 ".translations-settings-language:not(:first-child)"
1172 )) {
1173 let langButton = langElem.querySelector("moz-button");
1174 const langLabel = langElem.querySelector("label");
1175 const downloadPhase = this.downloadPhases.get(
1176 langLabel.getAttribute("value")
1177 ).downloadPhase;
1179 langButton.removeAttribute("disabled");
1181 if (
1182 downloadPhase !== "downloaded" &&
1183 allLanguageDownloadStatus === "downloaded"
1185 // In case of "All languages" downloaded
1186 this.changeButtonState({
1187 langButton,
1188 langTag: langLabel.getAttribute("value"),
1189 langState: "downloaded",
1191 } else if (
1192 downloadPhase === "downloaded" &&
1193 allLanguageDownloadStatus === "removed"
1195 // In case of "All languages" removed
1196 this.changeButtonState({
1197 langButton,
1198 langTag: langLabel.getAttribute("value"),
1199 langState: "removed",
1206 * Updates the state of a language download button.
1208 * This function changes the button's appearance and behavior based on the current language state
1209 * (e.g., "download", "loading", or "removed"). The button's icon and CSS class are updated to reflect
1210 * the state, and the appropriate event handler is set for downloading or removing the language.
1211 * The aria-label for accessibility is also updated using the Fluent string.
1213 * @param {object} options -
1214 * @param {Element} options.langButton - The HTML button element representing the language action (download/remove).
1215 * @param {string} options.langTag - The BCP-47 language tag for the language associated with the button.
1216 * @param {string} options.langState - The current state of the language, which can be "downloaded", "loading", or "removed".
1218 changeButtonState({ langButton, langTag, langState }) {
1219 // Remove any icon by removing it's respective CSS class
1220 langButton.classList.remove(
1221 "translations-settings-download-icon",
1222 "translations-settings-loading-icon",
1223 "translations-settings-remove-icon"
1225 // Set new icon based on the state of the language model
1226 switch (langState) {
1227 case "downloaded":
1228 // If language is downloaded show 'remove icon' as an option
1229 // for the user to remove the downloaded language model.
1230 langButton.classList.add("translations-settings-remove-icon");
1231 // The respective aria-label for accessibility is updated with correct Fluent string.
1232 if (langTag === "all") {
1233 document.l10n.setAttributes(
1234 langButton,
1235 "translations-settings-remove-all-button"
1237 } else {
1238 document.l10n.setAttributes(
1239 langButton,
1240 "translations-settings-remove-button",
1242 name: document.l10n.getAttributes(langButton).args.name,
1246 break;
1247 case "removed":
1248 // If language is removed show 'download icon' as an option
1249 // for the user to download the language model.
1250 langButton.classList.add("translations-settings-download-icon");
1251 // The respective aria-label for accessibility is updated with correct Fluent string.
1252 if (langTag === "all") {
1253 document.l10n.setAttributes(
1254 langButton,
1255 "translations-settings-download-all-button"
1257 } else {
1258 document.l10n.setAttributes(
1259 langButton,
1260 "translations-settings-download-button",
1262 name: document.l10n.getAttributes(langButton).args.name,
1266 break;
1267 case "loading":
1268 // While processing the download or remove language model
1269 // show 'loading icon' to the user
1270 langButton.classList.add("translations-settings-loading-icon");
1271 // The respective aria-label for accessibility is updated with correct Fluent string.
1272 if (langTag === "all") {
1273 document.l10n.setAttributes(
1274 langButton,
1275 "translations-settings-loading-all-button"
1277 } else {
1278 document.l10n.setAttributes(
1279 langButton,
1280 "translations-settings-loading-button",
1282 name: document.l10n.getAttributes(langButton).args.name,
1286 break;
1288 this.updateDownloadPhase(langTag, langState);