tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 742ed6d1c2d627341e05e630f6ce7bbfe1be8cbd
parent 4ab31c469663068de16e1aaed0b6870feb6ebbff
Author: Erik Nordin <enordin@mozilla.com>
Date:   Thu, 18 Dec 2025 15:42:06 +0000

Bug 2002127 - Part 12: Implement Never Translate Sites Functionality r=hjones

This commit adds implements the "Never Translate Sites" functionality
in the Translations subpage within the about:settings UI, compatible with
the settings redesign initiative.

Differential Revision: https://phabricator.services.mozilla.com/D274229

Diffstat:
Mbrowser/components/preferences/translations.d.ts | 3+++
Mbrowser/components/preferences/translations.js | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 182 insertions(+), 1 deletion(-)

diff --git a/browser/components/preferences/translations.d.ts b/browser/components/preferences/translations.d.ts @@ -23,4 +23,7 @@ export interface TranslationsSettingsElements { neverTranslateLanguagesGroup: HTMLElement; neverTranslateLanguagesSelect: HTMLSelectElement; neverTranslateLanguagesNoneRow: HTMLElement; + neverTranslateSitesGroup: HTMLElement; + neverTranslateSitesRow: HTMLElement; + neverTranslateSitesNoneRow: HTMLElement; } diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js @@ -24,6 +24,8 @@ const NEVER_TRANSLATE_LANGS_PREF = "browser.translations.neverTranslateLanguages"; /** @type {string} */ const TOPIC_TRANSLATIONS_PREF_CHANGED = "translations:pref-changed"; +/** @type {string} */ +const TRANSLATIONS_PERMISSION = "translations"; /** @type {string} */ const ALWAYS_LANGUAGE_ITEM_CLASS = "translations-always-language-item"; @@ -34,6 +36,10 @@ const ALWAYS_LANGUAGE_REMOVE_BUTTON_CLASS = "translations-always-remove-button"; const NEVER_LANGUAGE_ITEM_CLASS = "translations-never-language-item"; /** @type {string} */ const NEVER_LANGUAGE_REMOVE_BUTTON_CLASS = "translations-never-remove-button"; +/** @type {string} */ +const NEVER_SITE_ITEM_CLASS = "translations-never-site-item"; +/** @type {string} */ +const NEVER_SITE_REMOVE_BUTTON_CLASS = "translations-never-site-remove-button"; const TranslationsSettings = { /** @@ -86,6 +92,13 @@ const TranslationsSettings = { neverLanguageTags: new Set(), /** + * Current never-translate site origins. + * + * @type {Set<string>} + */ + neverSiteOrigins: new Set(), + + /** * Cached DOM elements used by the module. * * @type {TranslationsSettingsElements|null} @@ -132,6 +145,14 @@ const TranslationsSettings = { ); if (neverRemoveButton?.dataset.langTag) { this.removeNeverLanguage(neverRemoveButton.dataset.langTag); + break; + } + + const neverSiteRemoveButton = /** @type {HTMLElement|null} */ ( + target.closest?.(`.${NEVER_SITE_REMOVE_BUTTON_CLASS}`) + ); + if (neverSiteRemoveButton?.dataset.origin) { + this.removeNeverSite(neverSiteRemoveButton.dataset.origin); } break; } @@ -155,6 +176,8 @@ const TranslationsSettings = { } else if (data === NEVER_TRANSLATE_LANGS_PREF) { this.refreshNeverLanguages().catch(console.error); } + } else if (topic === "perm-changed") { + this.handlePermissionChange(subject, data); } }, @@ -173,12 +196,14 @@ const TranslationsSettings = { await this.initPromise; await this.refreshAlwaysLanguages(); await this.refreshNeverLanguages(); + this.refreshNeverSites(); return; } if (this.initialized) { await this.refreshAlwaysLanguages(); await this.refreshNeverLanguages(); + this.refreshNeverSites(); return; } @@ -242,7 +267,8 @@ const TranslationsSettings = { !this.elements?.alwaysTranslateLanguagesNoneRow || !this.elements?.neverTranslateLanguagesGroup || !this.elements?.neverTranslateLanguagesSelect || - !this.elements?.neverTranslateLanguagesNoneRow + !this.elements?.neverTranslateLanguagesNoneRow || + !this.elements?.neverTranslateSitesGroup ) { return; } @@ -274,11 +300,14 @@ const TranslationsSettings = { this ); this.elements.neverTranslateLanguagesGroup.addEventListener("click", this); + this.elements.neverTranslateSitesGroup.addEventListener("click", this); Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); + Services.obs.addObserver(this, "perm-changed"); window.addEventListener("unload", this); await this.refreshAlwaysLanguages(); await this.refreshNeverLanguages(); + this.refreshNeverSites(); this.initialized = true; }, @@ -309,6 +338,15 @@ const TranslationsSettings = { neverTranslateLanguagesNoneRow: /** @type {HTMLElement} */ ( document.getElementById("translationsNeverTranslateLanguagesNoneRow") ), + neverTranslateSitesGroup: /** @type {HTMLElement} */ ( + document.getElementById("translationsNeverTranslateSitesGroup") + ), + neverTranslateSitesRow: /** @type {HTMLElement} */ ( + document.getElementById("translationsNeverTranslateSitesRow") + ), + neverTranslateSitesNoneRow: /** @type {HTMLElement} */ ( + document.getElementById("translationsNeverTranslateSitesNoneRow") + ), }; if ( @@ -735,11 +773,150 @@ const TranslationsSettings = { }, /** + * Refresh the rendered list of never-translate sites. + */ + refreshNeverSites() { + if (!this.elements?.neverTranslateSitesGroup) { + return; + } + + /** @type {string[]} */ + let siteOrigins = []; + try { + siteOrigins = TranslationsParent.listNeverTranslateSites() ?? []; + } catch (error) { + console.error("Failed to list never translate sites", error); + } + + this.neverSiteOrigins = new Set(siteOrigins); + this.renderNeverSites(siteOrigins); + }, + + /** + * Render the never-translate sites list. + * + * @param {string[]} siteOrigins + */ + renderNeverSites(siteOrigins) { + const { neverTranslateSitesGroup, neverTranslateSitesNoneRow } = + this.elements ?? {}; + if (!neverTranslateSitesGroup) { + return; + } + + for (const item of neverTranslateSitesGroup.querySelectorAll( + `.${NEVER_SITE_ITEM_CLASS}` + )) { + item.remove(); + } + + if (neverTranslateSitesNoneRow) { + const hasSites = Boolean(siteOrigins.length); + neverTranslateSitesNoneRow.hidden = hasSites; + + if (hasSites && neverTranslateSitesNoneRow.isConnected) { + neverTranslateSitesNoneRow.remove(); + } else if (!hasSites && !neverTranslateSitesNoneRow.isConnected) { + neverTranslateSitesGroup.appendChild(neverTranslateSitesNoneRow); + } + } + + const sortedOrigins = [...siteOrigins].sort((originA, originB) => { + return this.getSiteSortKey(originA).localeCompare( + this.getSiteSortKey(originB) + ); + }); + + for (const origin of sortedOrigins) { + const removeButton = document.createElement("moz-button"); + removeButton.setAttribute("slot", "actions-start"); + removeButton.setAttribute("type", "icon ghost"); + removeButton.setAttribute( + "iconsrc", + "chrome://global/skin/icons/delete.svg" + ); + removeButton.classList.add(NEVER_SITE_REMOVE_BUTTON_CLASS); + removeButton.dataset.origin = origin; + removeButton.setAttribute("aria-label", origin); + + const item = document.createElement("moz-box-item"); + item.classList.add(NEVER_SITE_ITEM_CLASS); + item.setAttribute("label", origin); + item.dataset.origin = origin; + item.appendChild(removeButton); + if ( + neverTranslateSitesNoneRow && + neverTranslateSitesNoneRow.parentElement === neverTranslateSitesGroup + ) { + neverTranslateSitesGroup.insertBefore(item, neverTranslateSitesNoneRow); + } else { + neverTranslateSitesGroup.appendChild(item); + } + } + }, + + /** + * Remove a site from the never-translate list. + * + * @param {string} origin + */ + removeNeverSite(origin) { + if (!origin || !this.neverSiteOrigins.has(origin)) { + return; + } + + try { + TranslationsParent.setNeverTranslateSiteByOrigin(false, origin); + } catch (error) { + console.error("Failed to remove never translate site", error); + return; + } + + this.refreshNeverSites(); + }, + + /** + * Create a sort key that ignores protocol differences. + * + * @param {string} origin + * @returns {string} + */ + getSiteSortKey(origin) { + try { + return Services.io.newURI(origin).asciiHostPort; + } catch { + return origin; + } + }, + + /** + * Handle updates to translations permissions. + * + * @param {nsISupports} subject + * @param {string} data + */ + handlePermissionChange(subject, data) { + if (data === "cleared") { + this.neverSiteOrigins = new Set(); + this.renderNeverSites([]); + return; + } + + const perm = subject?.QueryInterface?.(Ci.nsIPermission); + if (perm?.type !== TRANSLATIONS_PERMISSION) { + return; + } + + this.refreshNeverSites(); + }, + + /** * Remove observers and listeners added during init. */ teardown() { try { Services.obs.removeObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); + Services.obs.removeObserver(this, "perm-changed"); } catch (e) { // Ignore if we were never added. } @@ -761,6 +938,7 @@ const TranslationsSettings = { "click", this ); + this.elements?.neverTranslateSitesGroup?.removeEventListener("click", this); }, };