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:
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.