tor-browser

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

commit c3999a869557cb3a4c99357a3833ad39156a3bc4
parent 768c94aed51ae4232560922471269b641820768b
Author: Erik Nordin <enordin@mozilla.com>
Date:   Thu, 20 Nov 2025 13:11:51 +0000

Bug 2000959 - Add Translations QuickAction to Nightly r=daleharvey,search-reviewers,fluent-reviewers,bolsson

This commit adds a new QuickAction for Translations, which
leads to the `about:translations` page. The action is enabled
in Nightly only, for now.

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

Diffstat:
Mbrowser/app/profile/firefox.js | 7+++++++
Mbrowser/components/urlbar/QuickActionsLoaderDefault.sys.mjs | 42++++++++++++++++++++++++++++++++++++++++--
Mbrowser/components/urlbar/tests/browser/browser.toml | 2++
Abrowser/components/urlbar/tests/browser/browser_quickactions_translate.js | 275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales/en-US/browser/browser.ftl | 4++++
5 files changed, 328 insertions(+), 2 deletions(-)

diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -2376,6 +2376,13 @@ pref("browser.translations.newSettingsUI.enable", false); // engine https://browser.mt/. pref("browser.translations.select.enable", true); +// Enable the Translations QuickAction in the URL bar. +#ifdef NIGHTLY_BUILD + pref("browser.translations.quickAction.enabled", true); +#else + pref("browser.translations.quickAction.enabled", false); +#endif + // Telemetry settings. // Determines if Telemetry pings can be archived locally. pref("toolkit.telemetry.archive.enabled", true); diff --git a/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs b/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs @@ -7,14 +7,20 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + ActionsProviderQuickActions: + "moz-src:///browser/components/urlbar/ActionsProviderQuickActions.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs", ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs", ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", - ActionsProviderQuickActions: - "moz-src:///browser/components/urlbar/ActionsProviderQuickActions.sys.mjs", + TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", + UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", }); +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.UrlbarUtils.getLogger({ prefix: "QuickActions" }) +); + import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; if (AppConstants.MOZ_UPDATER) { @@ -264,6 +270,38 @@ const DEFAULT_ACTIONS = { label: "quickactions-themes", onPick: openAddonsUrl("addons://list/theme"), }, + translate: { + l10nCommands: ["quickactions-cmd-translate"], + icon: "chrome://browser/skin/translations.svg", + label: "quickactions-translate", + isVisible: () => { + return Services.prefs.getBoolPref( + "browser.translations.quickAction.enabled", + false + ); + }, + onPick: async () => { + let url = "about:translations"; + let targetLanguage; + + try { + targetLanguage = + await lazy.TranslationsParent.getTopPreferredSupportedToLang(); + } catch (error) { + lazy.logger.error(error); + } + + if (targetLanguage) { + const urlObj = new URL(url); + const params = new URLSearchParams(); + params.set("trg", targetLanguage); + urlObj.hash = params.toString(); + url = urlObj.href; + } + + return openUrl(url); + }, + }, update: { l10nCommands: ["quickactions-cmd-update"], label: "quickactions-update", diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml @@ -422,6 +422,8 @@ support-files = [ ["browser_quickactions_tab_refocus.js"] +["browser_quickactions_translate.js"] + ["browser_raceWithTabs.js"] ["browser_recentsearches.js"] diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_translate.js b/browser/components/urlbar/tests/browser/browser_quickactions_translate.js @@ -0,0 +1,275 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for the translate quick action. + */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/translations/tests/browser/shared-head.js", + this +); + +const assertAction = async name => { + await BrowserTestUtils.waitForCondition(() => + window.document.querySelector(`.urlbarView-action-btn[data-action=${name}]`) + ); + Assert.ok(true, `We found action "${name}"`); +}; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.translations.quickAction.enabled", true], + ["browser.urlbar.quickactions.timesShownOnboardingLabel", 0], + ], + }); + + const { removeMocks } = await createAndMockRemoteSettings({ + languagePairs: [ + { fromLang: "en", toLang: "fr" }, + { fromLang: "fr", toLang: "en" }, + ], + }); + + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + await removeMocks(); + }); +}); + +add_task(async function test_translate_disabled() { + info("Disable the translate quick action and ensure it is hidden"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.translations.quickAction.enabled", false]], + }); + + info("Search for the translate quick action keyword"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + let hasTranslateAction = Boolean( + window.document.querySelector( + `.urlbarView-action-btn[data-action=translate]` + ) + ); + Assert.ok(!hasTranslateAction, "Translate action is not shown when disabled"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_translate_keyword() { + info("Search with the primary translate keyword"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + await assertAction("translate"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function test_partial_match() { + info("Search with a partial translate prefix"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "transl", + }); + + await assertAction("translate"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function test_translate_opens_about_translations() { + info("Open a new tab for the translate action result"); + const translateTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Search for the translate quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + await assertAction("translate"); + + EventUtils.synthesizeKey("KEY_Tab", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + info("Wait for about:translations to load and verify"); + await BrowserTestUtils.browserLoaded(translateTab.linkedBrowser, false, url => + url.startsWith("about:translations") + ); + + Assert.ok( + translateTab.linkedBrowser.currentURI.spec.startsWith("about:translations"), + "about:translations page is loaded" + ); + + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + } + BrowserTestUtils.removeTab(translateTab); +}); + +add_task(async function test_translate_includes_target_language() { + info("Open a new tab for the translate action result"); + const translateTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Search for the translate quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + await assertAction("translate"); + + EventUtils.synthesizeKey("KEY_Tab", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + info("Wait for about:translations to load and check query params"); + await BrowserTestUtils.browserLoaded(translateTab.linkedBrowser, false, url => + url.startsWith("about:translations") + ); + + const url = new URL(translateTab.linkedBrowser.currentURI.spec); + const hashParams = new URLSearchParams(url.hash.substring(1)); + const targetLang = hashParams.get("trg"); + + Assert.equal(targetLang, "en", "Target language parameter is set to 'en'"); + + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + } + BrowserTestUtils.removeTab(translateTab); +}); + +add_task( + async function test_translate_missing_language_does_not_append_target() { + info("Open a new tab for the translate action result"); + const translateTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Temporarily override preferred language lookup to fail once"); + let oneTimeOverrideCalled = false; + const originalFn = TranslationsParent.getTopPreferredSupportedToLang; + TranslationsParent.getTopPreferredSupportedToLang = async () => { + oneTimeOverrideCalled = true; + TranslationsParent.getTopPreferredSupportedToLang = originalFn; + throw new Error("Simulated failure retrieving the preferred language"); + }; + + info("Search for the translate quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + await assertAction("translate"); + + EventUtils.synthesizeKey("KEY_Tab", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + info( + "Wait for about:translations and confirm no target language is appended" + ); + await BrowserTestUtils.browserLoaded( + translateTab.linkedBrowser, + false, + url => url.startsWith("about:translations") + ); + + const url = new URL(translateTab.linkedBrowser.currentURI.spec); + Assert.equal(url.hash, "", "No target language parameter is appended"); + Assert.ok(oneTimeOverrideCalled, "The overridden language lookup ran once"); + Assert.equal( + originalFn, + TranslationsParent.getTopPreferredSupportedToLang, + "The preferred-language getter has been restored" + ); + + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + } + BrowserTestUtils.removeTab(translateTab); + } +); + +add_task(async function test_translate_switches_to_existing_tab() { + info("Open about:translations in the first tab"); + const translateTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:translations" + ); + + info("Open a content page in another foreground tab"); + const otherTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + + Assert.equal( + gBrowser.selectedTab, + otherTab, + "Other tab is currently selected" + ); + + info("Trigger the translate quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "translate", + }); + + await assertAction("translate"); + + EventUtils.synthesizeKey("KEY_Tab", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + info("Wait for the existing tab to be selected"); + await BrowserTestUtils.waitForCondition( + () => gBrowser.selectedTab === translateTab, + "Should switch to existing about:translations tab" + ); + + Assert.equal( + gBrowser.selectedTab, + translateTab, + "Switched to existing about:translations tab" + ); + + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + } + BrowserTestUtils.removeTab(otherTab); + BrowserTestUtils.removeTab(translateTab); +}); diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl @@ -324,6 +324,10 @@ quickactions-cmd-restart = restart quickactions-screenshot3 = Take a screenshot quickactions-cmd-screenshot2 = screenshot, take a screenshot +# Opens about:translations +quickactions-translate = Translate +quickactions-cmd-translate = translate + # Opens about:preferences quickactions-settings2 = Manage settings # "manage" should match the corresponding command, which is “Manage settings” in English.