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 */
8 * @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages
12 * The permission type to give to Services.perms for Translations.
14 const TRANSLATIONS_PERMISSION
= "translations";
17 * The list of BCP-47 language tags that will trigger auto-translate.
19 const ALWAYS_TRANSLATE_LANGS_PREF
=
20 "browser.translations.alwaysTranslateLanguages";
23 * The list of BCP-47 language tags that will prevent auto-translate.
25 const NEVER_TRANSLATE_LANGS_PREF
=
26 "browser.translations.neverTranslateLanguages";
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
= {
35 * List of languages set in the Always Translate Preferences
38 alwaysTranslateLanguages
: [],
41 * List of languages set in the Never Translate Preferences
44 neverTranslateLanguages
: [],
47 * List of languages set in the Never Translate Site Preferences
50 neverTranslateSites
: [],
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(),
60 * Object with details of languages supported by the browser.
62 * @type {SupportedLanguages}
64 supportedLanguages
: {},
67 * List of languages names supported along with their tags (BCP 47 locale identifiers).
68 * @type Array<{ langTag: string, displayName: string}>
70 supportedLanguageTagsNames
: [],
73 * Add Lazy getter for document 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(
110 // Keyboard navigation support.
111 this.elements
.alwaysTranslateMenuList
.addEventListener("keydown", this);
112 this.elements
.alwaysTranslateMenuPopup
.addEventListener(
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();
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", {
155 _defineLazyElements(document
, entries
) {
157 for (const [name
, elementId
] of Object
.entries(entries
)) {
158 ChromeUtils
.defineLazyGetter(this.elements
, name
, () => {
159 const element
= document
.getElementById(elementId
);
161 throw new Error(`Could not find "${name}" at "#${elementId}".`);
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(
203 allDownloadSize
+= downloadSize
;
204 const hasAllFilesForLanguage
=
205 await TranslationsParent
.hasAllFilesForLanguage(language
.langTag
);
206 const downloadPhase
= hasAllFilesForLanguage
? "downloaded" : "removed";
207 this.downloadPhases
.set(language
.langTag
, {
211 downloadCount
+= downloadPhase
=== "downloaded" ? 1 : 0;
213 const allDownloadPhase
=
214 downloadCount
=== this.supportedLanguageTagsNames
.length
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(
237 "translations-settings-download-size",
239 size
: size
+ " " + units
,
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",
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
,
269 "translations-settings-download-" + language
.langTag
273 this.downloadPhases
.get(language
.langTag
).downloadPhase
===
276 const mozButton
= isDownloaded
277 ? this.createIconButton(
279 "translations-settings-remove-icon",
280 "translations-settings-manage-downloaded-language-button",
282 "translations-settings-remove-button",
285 : this.createIconButton(
287 "translations-settings-download-icon",
288 "translations-settings-manage-downloaded-language-button",
290 "translations-settings-download-button",
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
,
306 langState
: "downloaded",
310 const allDownloadSize
= this.downloadPhases
.get("all").size
;
311 const languageSize
= createSizeElement(allDownloadSize
);
313 allLangElement
.appendChild(languageSize
);
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"
323 this.removeError(err
);
326 switch (event
.type
) {
328 // Keyboard navigation support.
329 this.handleKeys(event
);
332 // Handle Menulist selection through pointing device
334 eventNodeParent
.id
=== "translations-settings-always-translate-list"
336 this.handleAddAlwaysTranslateLanguage(
337 event
.target
.parentNode
.getAttribute("value")
340 eventNodeParent
.id
=== "translations-settings-never-translate-list"
342 this.handleAddNeverTranslateLanguage(
343 event
.target
.parentNode
.getAttribute("value")
348 if (eventNodeClassList
.contains("translations-settings-site-button")) {
349 this.handleRemoveNeverTranslateSite(event
);
351 eventNodeClassList
.contains(
352 "translations-settings-language-never-button"
355 this.handleRemoveNeverTranslateLanguage(event
);
357 eventNodeClassList
.contains(
358 "translations-settings-language-always-button"
361 this.handleRemoveAlwaysTranslateLanguage(event
);
363 eventNodeClassList
.contains(
364 "translations-settings-manage-downloaded-language-button"
368 eventNodeClassList
.contains("translations-settings-download-icon")
371 eventNodeParent
.querySelector("label").id
===
372 "translations-settings-download-all-languages"
374 this.handleDownloadAllLanguages(event
);
376 this.handleDownloadLanguage(event
);
379 eventNodeClassList
.contains("translations-settings-remove-icon")
382 eventNodeParent
.querySelector("label").id
===
383 "translations-settings-download-all-languages"
385 this.handleRemoveAllDownloadLanguages(event
);
387 this.handleRemoveDownloadLanguage(event
);
395 // Keyboard navigation support.
399 // Handle Menulist selection through keyboard
400 if (event
.target
.id
=== "translations-settings-always-translate-list") {
401 this.handleAddAlwaysTranslateLanguage(
402 event
.target
.getAttribute("value")
405 event
.target
.id
=== "translations-settings-never-translate-list"
407 this.handleAddNeverTranslateLanguage(
408 event
.target
.getAttribute("value")
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")
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();
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")
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();
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
,
529 languageList
: this.elements
.alwaysTranslateLanguageList
,
530 curLangTags
: Array
.from(this.alwaysTranslateLanguages
),
531 otherPref
: NEVER_TRANSLATE_LANGS_PREF
,
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
;
560 this.alwaysTranslateLanguages
= updatedLangTags
;
565 * Adds a site to Never translate site list
566 * @param {string} site
569 const { neverTranslateSiteList
} = this.elements
;
571 // Label and textContent of the added site element is the same
572 const languageLabel
= this.createLangLabel(
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",
587 // Create unique id using site name
588 const languageElement
= this.createLangElement(
589 [mozButton
, languageLabel
],
590 "translations-settings-" + site
+ "-id"
592 neverTranslateSiteList
.insertBefore(
594 neverTranslateSiteList
.firstElementChild
596 // The first element is selected by default when keyboard navigation enters this list
597 neverTranslateSiteList
.setAttribute(
598 "aria-activedescendant",
601 if (neverTranslateSiteList
.childElementCount
) {
602 neverTranslateSiteList
.parentNode
.hidden
= false;
607 * Removes a site from Never translate site list
608 * @param {string} 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
628 const siteList
= TranslationsParent
.listNeverTranslateSites();
629 for (const site
of siteList
) {
632 this.neverTranslateSites
= siteList
;
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
) {
649 if (!neverTranslateSiteList
.childElementCount
) {
650 neverTranslateSiteList
.parentNode
.hidden
= true;
653 const perm
= subject
.QueryInterface(Ci
.nsIPermission
);
654 if (perm
.type
!= TRANSLATIONS_PERMISSION
) {
655 // The updated permission was not for Translations, nothing to do.
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.
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
) {
675 case ALWAYS_TRANSLATE_LANGS_PREF
:
676 case NEVER_TRANSLATE_LANGS_PREF
: {
677 this.populateLanguageList(data
);
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");
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
;
750 languageDisplayName
= languageDisplayNames
.of(langTag
);
753 `Failed to retrieve language display name for '${langTag}'.`
758 const languageLabel
= this.createLangLabel(
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",
773 const languageElement
= this.createLangElement(
774 [mozButton
, languageLabel
],
775 "translations-settings-language-" +
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
793 * @param {string} textContent
794 * @param {string} value
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}]`);
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(
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
)) {
866 `Expected downloadPhases entry for ${langTag}, but found none.`
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)"
887 const langLabel
= langElem
.querySelector("label");
888 const langTag
= langLabel
.getAttribute("value");
889 const langButton
= langElem
.querySelector("moz-button");
892 TranslationsParent
.hasAllFilesForLanguage(langTag
).then(
893 hasAllFilesForLanguage
=> {
894 if (hasAllFilesForLanguage
) {
896 this.changeButtonState({
899 langState
: "downloaded",
902 this.changeButtonState({
905 langState
: "removed",
908 langButton
.removeAttribute("disabled");
913 await Promise
.allSettled(updatePromises
);
915 const allDownloaded
=
916 downloadCount
=== this.supportedLanguageTagsNames
.length
;
918 this.changeButtonState({
919 langButton
: allLangButton
,
921 langState
: "downloaded",
924 this.changeButtonState({
925 langButton
: allLangButton
,
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
, {
938 errorElement
.classList
.add("translations-settings-language-error");
939 parentNode
.appendChild(errorElement
);
942 removeError(errorNode
) {
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
,
959 langState
: "loading",
963 await TranslationsParent
.downloadLanguageFiles(langTag
);
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
,
982 langState
: "removed",
987 this.changeButtonState({
988 langButton
: eventButton
,
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"
999 this.downloadPhases
.get("all").downloadPhase
!== "downloaded"
1001 this.changeButtonState({
1003 this.elements
.downloadLanguageList
.firstElementChild
.querySelector(
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
,
1025 langState
: "loading",
1029 await TranslationsParent
.deleteLanguageFiles(langTag
);
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
,
1048 langState
: "downloaded",
1054 this.changeButtonState({
1055 langButton
: eventButton
,
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({
1064 this.elements
.downloadLanguageList
.firstElementChild
.querySelector(
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
,
1084 langState
: "loading",
1088 await TranslationsParent
.downloadAllFiles();
1090 console
.error(error
);
1091 await
this.reloadDownloadPhases();
1092 this.showErrorMessage(
1093 eventButton
.parentNode
,
1094 "translations-settings-language-download-error",
1099 this.changeButtonState({
1100 langButton
: eventButton
,
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
,
1117 langState
: "loading",
1121 await TranslationsParent
.deleteAllLanguageFiles();
1123 console
.error(error
);
1124 await
this.reloadDownloadPhases();
1125 this.showErrorMessage(
1126 eventButton
.parentNode
,
1127 "translations-settings-language-remove-error",
1132 this.changeButtonState({
1133 langButton
: eventButton
,
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)"
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)"
1173 let langButton
= langElem
.querySelector("moz-button");
1174 const langLabel
= langElem
.querySelector("label");
1175 const downloadPhase
= this.downloadPhases
.get(
1176 langLabel
.getAttribute("value")
1179 langButton
.removeAttribute("disabled");
1182 downloadPhase
!== "downloaded" &&
1183 allLanguageDownloadStatus
=== "downloaded"
1185 // In case of "All languages" downloaded
1186 this.changeButtonState({
1188 langTag
: langLabel
.getAttribute("value"),
1189 langState
: "downloaded",
1192 downloadPhase
=== "downloaded" &&
1193 allLanguageDownloadStatus
=== "removed"
1195 // In case of "All languages" removed
1196 this.changeButtonState({
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
) {
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(
1235 "translations-settings-remove-all-button"
1238 document
.l10n
.setAttributes(
1240 "translations-settings-remove-button",
1242 name
: document
.l10n
.getAttributes(langButton
).args
.name
,
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(
1255 "translations-settings-download-all-button"
1258 document
.l10n
.setAttributes(
1260 "translations-settings-download-button",
1262 name
: document
.l10n
.getAttributes(langButton
).args
.name
,
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(
1275 "translations-settings-loading-all-button"
1278 document
.l10n
.setAttributes(
1280 "translations-settings-loading-button",
1282 name
: document
.l10n
.getAttributes(langButton
).args
.name
,
1288 this.updateDownloadPhase(langTag
, langState
);