/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /** * The permission type to give to Services.perms for Translations. */ const TRANSLATIONS_PERMISSION = "translations"; /** * The list of BCP-47 language tags that will trigger auto-translate. */ const ALWAYS_TRANSLATE_LANGS_PREF = "browser.translations.alwaysTranslateLanguages"; /** * The list of BCP-47 language tags that will prevent showing Translations UI. */ const NEVER_TRANSLATE_LANGS_PREF = "browser.translations.neverTranslateLanguages"; function Tree(aId, aData) { this._data = aData; this._tree = document.getElementById(aId); this._tree.view = this; } Tree.prototype = { get tree() { return this._tree; }, get isEmpty() { return !this._data.length; }, get hasSelection() { return this.selection.count > 0; }, getSelectedItems() { let result = []; let rc = this.selection.getRangeCount(); for (let i = 0; i < rc; ++i) { let min = {}, max = {}; this.selection.getRangeAt(i, min, max); for (let j = min.value; j <= max.value; ++j) { result.push(this._data[j]); } } return result; }, // nsITreeView implementation get rowCount() { return this._data.length; }, getCellText(aRow, aColumn) { return this._data[aRow]; }, isSeparator(aIndex) { return false; }, isSorted() { return false; }, isContainer(aIndex) { return false; }, setTree(aTree) {}, getImageSrc(aRow, aColumn) {}, getCellValue(aRow, aColumn) {}, cycleHeader(column) {}, getRowProperties(row) { return ""; }, getColumnProperties(column) { return ""; }, getCellProperties(row, column) { return ""; }, QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), }; function Lang(aCode, label) { this.langCode = aCode; this._label = label; } Lang.prototype = { toString() { return this._label; }, }; var gTranslationsSettings = { onLoad() { if (this._neverTranslateSiteTree) { // Re-using an open dialog, clear the old observers. this.removeObservers(); } // Load site permissions into an array. this._neverTranslateSites = []; for (const perm of Services.perms.getAllByTypes([ TRANSLATIONS_PERMISSION, ])) { if (perm.capability === Services.perms.DENY_ACTION) { this._neverTranslateSites.push(perm.principal.origin); } } let stripProtocol = s => s?.replace(/^\w+:/, "") || ""; this._neverTranslateSites.sort((a, b) => { return stripProtocol(a).localeCompare(stripProtocol(b)); }); // Load language tags into arrays. this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages(); this._neverTranslateLangs = this.getNeverTranslateLanguages(); // Add observers for relevant prefs and permissions. Services.obs.addObserver(this, "perm-changed"); Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this); Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this); // Build trees from the arrays. this._alwaysTranslateLangsTree = new Tree( "alwaysTranslateLanguagesTree", this._alwaysTranslateLangs ); this._neverTranslateLangsTree = new Tree( "neverTranslateLanguagesTree", this._neverTranslateLangs ); this._neverTranslateSiteTree = new Tree( "neverTranslateSitesTree", this._neverTranslateSites ); // Ensure the UI for each group is in the correct state. this.onSelectAlwaysTranslateLanguage(); this.onSelectNeverTranslateLanguage(); this.onSelectNeverTranslateSite(); }, /** * Retrieves the value of a char-pref splits its value into an * array delimited by commas. * * This is used for the translations preferences which are comma- * separated lists of BCP-47 language tags. * * @param {string} pref * @returns {Array} */ getLangsFromPref(pref) { let rawLangs = Services.prefs.getCharPref(pref); if (!rawLangs) { return []; } let langArr = rawLangs.split(","); let displayNames = Services.intl.getLanguageDisplayNames( undefined, langArr ); let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i])); langs.sort(); return langs; }, /** * Retrieves the always-translate language tags as an array. * @returns {Array} */ getAlwaysTranslateLanguages() { return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF); }, /** * Retrieves the never-translate language tags as an array. * @returns {Array} */ getNeverTranslateLanguages() { return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF); }, /** * Handles updating the UI components on pref or permission changes. */ observe(aSubject, aTopic, aData) { if (aTopic === "perm-changed") { if (aData === "cleared") { // Permissions have been cleared if (!this._neverTranslateSites.length) { // There were no sites with permissions set, nothing to do. return; } // Update the tree based on the amount of permissions removed. let removed = this._neverTranslateSites.splice( 0, this._neverTranslateSites.length ); this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length); } else { let perm = aSubject.QueryInterface(Ci.nsIPermission); if (perm.type != TRANSLATIONS_PERMISSION) { // The updated permission was not for Translations, nothing to do. return; } if (aData === "added") { if (perm.capability != Services.perms.DENY_ACTION) { // We are only showing data for sites we should never translate. // If the permission is not DENY_ACTION, we don't care about it here. return; } this._neverTranslateSites.push(perm.principal.origin); this._neverTranslateSites.sort(); let tree = this._neverTranslateSiteTree.tree; tree.rowCountChanged(0, 1); tree.invalidate(); } else if (aData == "deleted") { let index = this._neverTranslateSites.indexOf(perm.principal.origin); if (index == -1) { // The deleted permission was not in the tree, nothing to do. return; } this._neverTranslateSites.splice(index, 1); this._neverTranslateSiteTree.tree.rowCountChanged(index, -1); } } // Ensure the UI updates to the changes. this.onSelectNeverTranslateSite(); } else if (aTopic === "nsPref:changed") { switch (aData) { case ALWAYS_TRANSLATE_LANGS_PREF: { this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages(); let alwaysTranslateLangsChange = this._alwaysTranslateLangs.length - this._alwaysTranslateLangsTree.rowCount; this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs; let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree; if (alwaysTranslateLangsChange) { alwaysTranslateLangsTree.rowCountChanged( 0, alwaysTranslateLangsChange ); } alwaysTranslateLangsTree.invalidate(); // Ensure the UI updates to the changes. this.onSelectAlwaysTranslateLanguage(); break; } case NEVER_TRANSLATE_LANGS_PREF: { this._neverTranslateLangs = this.getNeverTranslateLanguages(); let neverTranslateLangsChange = this._neverTranslateLangs.length - this._neverTranslateLangsTree.rowCount; this._neverTranslateLangsTree._data = this._neverTranslateLangs; let neverTranslateLangsTree = this._neverTranslateLangsTree.tree; if (neverTranslateLangsChange) { neverTranslateLangsTree.rowCountChanged( 0, neverTranslateLangsChange ); } neverTranslateLangsTree.invalidate(); // Ensure the UI updates to the changes. this.onSelectNeverTranslateLanguage(); break; } } } }, /** * Ensures that buttons states are enabled/disabled accordingly based on the * content of the trees. * * The remove button should be enabled only if an item is selected. * The removeAll button should be enabled any time the tree has content. * * @param {Tree} aTree * @param {string} aIdPart */ _handleButtonDisabling(aTree, aIdPart) { let empty = aTree.isEmpty; document.getElementById("removeAll" + aIdPart + "s").disabled = empty; document.getElementById("remove" + aIdPart).disabled = empty || !aTree.hasSelection; }, /** * Updates the UI state for the always-translate languages section. */ onSelectAlwaysTranslateLanguage() { this._handleButtonDisabling( this._alwaysTranslateLangsTree, "AlwaysTranslateLanguage" ); }, /** * Updates the UI state for the never-translate languages section. */ onSelectNeverTranslateLanguage() { this._handleButtonDisabling( this._neverTranslateLangsTree, "NeverTranslateLanguage" ); }, /** * Updates the UI state for the never-translate sites section. */ onSelectNeverTranslateSite() { this._handleButtonDisabling( this._neverTranslateSiteTree, "NeverTranslateSite" ); }, /** * Updates the value of a language pref to match when a language is removed * through the UI. * * @param {string} pref * @param {Tree} tree */ _onRemoveLanguage(pref, tree) { let langs = Services.prefs.getCharPref(pref); if (!langs) { return; } let removed = tree.getSelectedItems().map(l => l.langCode); langs = langs.split(",").filter(l => !removed.includes(l)); Services.prefs.setCharPref(pref, langs.join(",")); }, /** * Updates the never-translate language pref when a never-translate language * is removed via the UI. */ onRemoveAlwaysTranslateLanguage() { this._onRemoveLanguage( ALWAYS_TRANSLATE_LANGS_PREF, this._alwaysTranslateLangsTree ); }, /** * Updates the always-translate language pref when a always-translate language * is removed via the UI. */ onRemoveNeverTranslateLanguage() { this._onRemoveLanguage( NEVER_TRANSLATE_LANGS_PREF, this._neverTranslateLangsTree ); }, /** * Updates the permissions for a never-translate site when it is removed via the UI. */ onRemoveNeverTranslateSite() { let removedNeverTranslateSites = this._neverTranslateSiteTree.getSelectedItems(); for (let origin of removedNeverTranslateSites) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION); } }, /** * Clears the always-translate languages pref when the list is cleared in the UI. */ onRemoveAllAlwaysTranslateLanguages() { Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); }, /** * Clears the never-translate languages pref when the list is cleared in the UI. */ onRemoveAllNeverTranslateLanguages() { Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); }, /** * Clears the never-translate sites pref when the list is cleared in the UI. */ onRemoveAllNeverTranslateSites() { if (this._neverTranslateSiteTree.isEmpty) { return; } let removedNeverTranslateSites = this._neverTranslateSites.splice( 0, this._neverTranslateSites.length ); this._neverTranslateSiteTree.tree.rowCountChanged( 0, -removedNeverTranslateSites.length ); for (let origin of removedNeverTranslateSites) { let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION); } this.onSelectNeverTranslateSite(); }, /** * Handles removing a selected always-translate language via the keyboard. */ onAlwaysTranslateLanguageKeyPress(aEvent) { if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { this.onRemoveAlwaysTranslateLanguage(); } }, /** * Handles removing a selected never-translate language via the keyboard. */ onNeverTranslateLanguageKeyPress(aEvent) { if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { this.onRemoveNeverTranslateLanguage(); } }, /** * Handles removing a selected never-translate site via the keyboard. */ onNeverTranslateSiteKeyPress(aEvent) { if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { this.onRemoveNeverTranslateSite(); } }, /** * Removes any active preference and permissions observers. */ removeObservers() { Services.obs.removeObserver(this, "perm-changed"); Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this); Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this); }, };