tor-browser

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

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

Bug 2002127 - Part 11: Implement Never Translate Languages Functionality r=hjones

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

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

Diffstat:
Mbrowser/components/preferences/translations.d.ts | 3+++
Mbrowser/components/preferences/translations.js | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 252 insertions(+), 11 deletions(-)

diff --git a/browser/components/preferences/translations.d.ts b/browser/components/preferences/translations.d.ts @@ -20,4 +20,7 @@ export interface TranslationsSettingsElements { alwaysTranslateLanguagesGroup: HTMLElement; alwaysTranslateLanguagesSelect: HTMLSelectElement; alwaysTranslateLanguagesNoneRow: HTMLElement; + neverTranslateLanguagesGroup: HTMLElement; + neverTranslateLanguagesSelect: HTMLSelectElement; + neverTranslateLanguagesNoneRow: HTMLElement; } diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js @@ -30,6 +30,11 @@ const ALWAYS_LANGUAGE_ITEM_CLASS = "translations-always-language-item"; /** @type {string} */ const ALWAYS_LANGUAGE_REMOVE_BUTTON_CLASS = "translations-always-remove-button"; +/** @type {string} */ +const NEVER_LANGUAGE_ITEM_CLASS = "translations-never-language-item"; +/** @type {string} */ +const NEVER_LANGUAGE_REMOVE_BUTTON_CLASS = "translations-never-remove-button"; + const TranslationsSettings = { /** * True once initialization has completed. @@ -74,6 +79,13 @@ const TranslationsSettings = { alwaysLanguageTags: new Set(), /** + * Current never-translate language tags. + * + * @type {Set<string>} + */ + neverLanguageTags: new Set(), + + /** * Cached DOM elements used by the module. * * @type {TranslationsSettingsElements|null} @@ -97,15 +109,29 @@ const TranslationsSettings = { await this.onAlwaysLanguageChosen( /** @type {HTMLSelectElement} */ (event.target).value ); + } else if ( + event.target === this.elements?.neverTranslateLanguagesSelect + ) { + await this.onNeverLanguageChosen( + /** @type {HTMLSelectElement} */ (event.target).value + ); } break; case "click": { const target = /** @type {HTMLElement} */ (event.target); - const removeButton = /** @type {HTMLElement|null} */ ( + const alwaysRemoveButton = /** @type {HTMLElement|null} */ ( target.closest?.(`.${ALWAYS_LANGUAGE_REMOVE_BUTTON_CLASS}`) ); - if (removeButton?.dataset.langTag) { - this.removeAlwaysLanguage(removeButton.dataset.langTag); + if (alwaysRemoveButton?.dataset.langTag) { + this.removeAlwaysLanguage(alwaysRemoveButton.dataset.langTag); + break; + } + + const neverRemoveButton = /** @type {HTMLElement|null} */ ( + target.closest?.(`.${NEVER_LANGUAGE_REMOVE_BUTTON_CLASS}`) + ); + if (neverRemoveButton?.dataset.langTag) { + this.removeNeverLanguage(neverRemoveButton.dataset.langTag); } break; } @@ -123,11 +149,12 @@ const TranslationsSettings = { * @param {string} data */ observe(subject, topic, data) { - if ( - topic === TOPIC_TRANSLATIONS_PREF_CHANGED && - data === ALWAYS_TRANSLATE_LANGS_PREF - ) { - this.refreshAlwaysLanguages().catch(console.error); + if (topic === TOPIC_TRANSLATIONS_PREF_CHANGED) { + if (data === ALWAYS_TRANSLATE_LANGS_PREF) { + this.refreshAlwaysLanguages().catch(console.error); + } else if (data === NEVER_TRANSLATE_LANGS_PREF) { + this.refreshNeverLanguages().catch(console.error); + } } }, @@ -145,11 +172,13 @@ const TranslationsSettings = { if (this.initPromise) { await this.initPromise; await this.refreshAlwaysLanguages(); + await this.refreshNeverLanguages(); return; } if (this.initialized) { await this.refreshAlwaysLanguages(); + await this.refreshNeverLanguages(); return; } @@ -200,7 +229,7 @@ const TranslationsSettings = { }, /** - * Initialize the "Always translate languages" section. + * Initialize the "Always translate languages" and "Never translate languages" sections. * * @returns {Promise<void>} */ @@ -210,7 +239,10 @@ const TranslationsSettings = { if ( !this.elements?.alwaysTranslateLanguagesGroup || !this.elements?.alwaysTranslateLanguagesSelect || - !this.elements?.alwaysTranslateLanguagesNoneRow + !this.elements?.alwaysTranslateLanguagesNoneRow || + !this.elements?.neverTranslateLanguagesGroup || + !this.elements?.neverTranslateLanguagesSelect || + !this.elements?.neverTranslateLanguagesNoneRow ) { return; } @@ -223,21 +255,30 @@ const TranslationsSettings = { } catch (error) { console.error("Failed to initialize translations settings UI", error); this.elements.alwaysTranslateLanguagesSelect.disabled = true; + this.elements.neverTranslateLanguagesSelect.disabled = true; return; } this.elements.alwaysTranslateLanguagesSelect.disabled = false; + this.elements.neverTranslateLanguagesSelect.disabled = false; await this.buildAlwaysSelectOptions(); + await this.buildNeverSelectOptions(); this.elements.alwaysTranslateLanguagesSelect.addEventListener( "change", this ); this.elements.alwaysTranslateLanguagesGroup.addEventListener("click", this); + this.elements.neverTranslateLanguagesSelect.addEventListener( + "change", + this + ); + this.elements.neverTranslateLanguagesGroup.addEventListener("click", this); Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); window.addEventListener("unload", this); await this.refreshAlwaysLanguages(); + await this.refreshNeverLanguages(); this.initialized = true; }, @@ -259,12 +300,24 @@ const TranslationsSettings = { alwaysTranslateLanguagesNoneRow: /** @type {HTMLElement} */ ( document.getElementById("translationsAlwaysTranslateLanguagesNoneRow") ), + neverTranslateLanguagesGroup: /** @type {HTMLElement} */ ( + document.getElementById("translationsNeverTranslateLanguagesGroup") + ), + neverTranslateLanguagesSelect: /** @type {HTMLSelectElement} */ ( + document.getElementById("translationsNeverTranslateLanguagesSelect") + ), + neverTranslateLanguagesNoneRow: /** @type {HTMLElement} */ ( + document.getElementById("translationsNeverTranslateLanguagesNoneRow") + ), }; if ( !elements.alwaysTranslateLanguagesGroup || !elements.alwaysTranslateLanguagesSelect || - !elements.alwaysTranslateLanguagesNoneRow + !elements.alwaysTranslateLanguagesNoneRow || + !elements.neverTranslateLanguagesGroup || + !elements.neverTranslateLanguagesSelect || + !elements.neverTranslateLanguagesNoneRow ) { return; } @@ -505,6 +558,183 @@ const TranslationsSettings = { }, /** + * Handle a selection in the "Never translate languages" dropdown. + * + * @param {string} langTag + */ + async onNeverLanguageChosen(langTag) { + if (!langTag) { + return; + } + + TranslationsParent.addLangTagToPref(langTag, NEVER_TRANSLATE_LANGS_PREF); + TranslationsParent.removeLangTagFromPref( + langTag, + ALWAYS_TRANSLATE_LANGS_PREF + ); + await this.resetNeverSelect(); + }, + + /** + * Remove the given language from the never translate list. + * + * @param {string} langTag + */ + removeNeverLanguage(langTag) { + TranslationsParent.removeLangTagFromPref( + langTag, + NEVER_TRANSLATE_LANGS_PREF + ); + }, + + /** + * Reset the dropdown back to the placeholder value and underlying setting state. + */ + async resetNeverSelect() { + await this.resetSelect( + this.elements?.neverTranslateLanguagesSelect, + "translationsNeverTranslateLanguagesSelect" + ); + }, + + /** + * Refresh the rendered list of never-translate languages to match prefs. + */ + async refreshNeverLanguages() { + if (!this.elements?.neverTranslateLanguagesGroup) { + return; + } + + const langTags = Array.from( + TranslationsParent.getNeverTranslateLanguages?.() ?? [] + ); + this.neverLanguageTags = new Set(langTags); + + this.renderNeverLanguages(langTags); + await this.updateNeverSelectOptionState(); + }, + + /** + * Render the current set of never-translate languages into the list UI. + * + * @param {string[]} langTags + */ + renderNeverLanguages(langTags) { + const { neverTranslateLanguagesGroup, neverTranslateLanguagesNoneRow } = + this.elements; + + for (const item of neverTranslateLanguagesGroup.querySelectorAll( + `.${NEVER_LANGUAGE_ITEM_CLASS}` + )) { + item.remove(); + } + + if (neverTranslateLanguagesNoneRow) { + const hasLanguages = Boolean(langTags.length); + neverTranslateLanguagesNoneRow.hidden = hasLanguages; + + if (hasLanguages && neverTranslateLanguagesNoneRow.isConnected) { + neverTranslateLanguagesNoneRow.remove(); + } else if (!hasLanguages && !neverTranslateLanguagesNoneRow.isConnected) { + neverTranslateLanguagesGroup.appendChild( + neverTranslateLanguagesNoneRow + ); + } + } + + const sortedLangTags = [...langTags].sort((langTagA, langTagB) => { + const labelA = this.formatLanguageLabel(langTagA) ?? langTagA; + const labelB = this.formatLanguageLabel(langTagB) ?? langTagB; + return labelA.localeCompare(labelB); + }); + + for (const langTag of sortedLangTags) { + const label = this.formatLanguageLabel(langTag); + if (!label) { + continue; + } + + 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_LANGUAGE_REMOVE_BUTTON_CLASS); + removeButton.dataset.langTag = langTag; + removeButton.setAttribute("aria-label", label); + + const item = document.createElement("moz-box-item"); + item.classList.add(NEVER_LANGUAGE_ITEM_CLASS); + item.setAttribute("label", label); + item.dataset.langTag = langTag; + item.appendChild(removeButton); + if ( + neverTranslateLanguagesNoneRow && + neverTranslateLanguagesNoneRow.parentElement === + neverTranslateLanguagesGroup + ) { + neverTranslateLanguagesGroup.insertBefore( + item, + neverTranslateLanguagesNoneRow + ); + } else { + neverTranslateLanguagesGroup.appendChild(item); + } + } + }, + + /** + * Populate the select options for the supported source languages. + */ + async buildNeverSelectOptions() { + const select = this.elements?.neverTranslateLanguagesSelect; + if (!select || !this.supportedLanguages?.sourceLanguages?.length) { + return; + } + + const placeholder = select.querySelector('moz-option[value=""]'); + for (const option of select.querySelectorAll("moz-option")) { + if (option !== placeholder) { + option.remove(); + } + } + + const sourceLanguages = [...this.supportedLanguages.sourceLanguages].sort( + (a, b) => a.displayName.localeCompare(b.displayName) + ); + for (const { langTag, displayName } of sourceLanguages) { + const option = document.createElement("moz-option"); + option.setAttribute("value", langTag); + option.setAttribute("label", displayName); + select.appendChild(option); + } + + await this.resetNeverSelect(); + }, + + /** + * Disable already-added languages in the select so they cannot be re-added. + */ + async updateNeverSelectOptionState() { + const select = this.elements?.neverTranslateLanguagesSelect; + if (!select) { + return; + } + + for (const option of select.querySelectorAll("moz-option")) { + const value = option.getAttribute("value"); + if (!value) { + continue; + } + option.disabled = this.neverLanguageTags.has(value); + } + + await this.resetNeverSelect(); + }, + + /** * Remove observers and listeners added during init. */ teardown() { @@ -523,6 +753,14 @@ const TranslationsSettings = { "click", this ); + this.elements?.neverTranslateLanguagesSelect?.removeEventListener( + "change", + this + ); + this.elements?.neverTranslateLanguagesGroup?.removeEventListener( + "click", + this + ); }, };