tor-browser

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

commit dd670349f93d8b8195404b2109be3c260dd02510
parent 59b687cc5f157dee8ce52f4732839361984cc575
Author: Erik Nordin <enordin@mozilla.com>
Date:   Wed, 15 Oct 2025 14:46:09 +0000

Bug 1975487 - Apply script direciton to translated elements r=translations-reviewers,gregtatum

This patch applies the target-language script direction
to elements with translatable content within within the
TranslationsDocument. This preserves the core layout of
the page, while still ensuring that translated text
displays correctly.

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

Diffstat:
Mtoolkit/components/translations/content/translations-document.sys.mjs | 17+++++++++++++++++
Mtoolkit/components/translations/tests/browser/browser_translations_translation_document.js | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtoolkit/components/translations/tests/browser/shared-head.js | 32++++++++++++++++++++++----------
3 files changed, 140 insertions(+), 11 deletions(-)

diff --git a/toolkit/components/translations/content/translations-document.sys.mjs b/toolkit/components/translations/content/translations-document.sys.mjs @@ -897,6 +897,13 @@ export class TranslationsDocument { #scheduler; /** + * The script direction of the source language. + * + * @type {("ltr"|"rtl")} + */ + #sourceScriptDirection; + + /** * The script direction of the target language. * * @type {("ltr"|"rtl")} @@ -1093,6 +1100,8 @@ export class TranslationsDocument { this.#documentLanguage = documentLanguage; this.#translationsCache = translationsCache; this.#actorReportFirstVisibleChange = reportVisibleChange; + this.#sourceScriptDirection = + Services.intl.getScriptDirection(documentLanguage); this.#targetScriptDirection = Services.intl.getScriptDirection(targetLanguage); this.#translationsMode = isFindBarOpen ? "content-eager" : "lazy"; @@ -3249,9 +3258,17 @@ export class TranslationsDocument { "text/html" ); + if (this.#sourceScriptDirection !== this.#targetScriptDirection) { + element.setAttribute("dir", this.#targetScriptDirection); + } + updateElement(translationsDocument, element); this.#processedContentNodes.add(targetNode); } else { + if (this.#sourceScriptDirection !== this.#targetScriptDirection) { + const parentElement = asElement(targetNode.parentNode); + parentElement?.setAttribute("dir", this.#targetScriptDirection); + } textNodeCount++; targetNode.textContent = translatedContent; this.#processedContentNodes.add(targetNode); diff --git a/toolkit/components/translations/tests/browser/browser_translations_translation_document.js b/toolkit/components/translations/tests/browser/browser_translations_translation_document.js @@ -1183,7 +1183,7 @@ add_task(async function test_html_lang_attribute() { translate(); - await waitForCondition(() => document.documentElement.lang === "EN"); + await waitForCondition(() => document.documentElement.lang === "es"); cleanup(); }); @@ -1946,3 +1946,103 @@ add_task(async function test_node_specific_attribute_mutation() { cleanup(); }); + +add_task( + async function test_direction_ltr_to_rtl_sets_dir_on_content_not_attributes() { + const { translate, htmlMatches, cleanup } = await createTranslationsDoc( + /* html */ ` + <div id="content" title="A translated title"> + This block of content should get RTL direction. + </div> + <input id="onlyPlaceholder" type="text" placeholder="A translated placeholder"> + <div id="onlyTitle" title="Only a translated title"></div> + <div> + Div text. + <span>Span within a div.</span> + </div> + <span> + Span text. + <div>Div within a span.</div> + </span> + `, + { sourceLanguage: "en", targetLanguage: "ar" } + ); + + translate(); + + await htmlMatches( + 'LTR to RTL: content elements get dir="rtl", but attribute-only elements do not.', + /* html */ ` + <div id="content" title="A TRANSLATED TITLE" dir="rtl"> + THIS BLOCK OF CONTENT SHOULD GET RTL DIRECTION. + </div> + <input id="onlyPlaceholder" type="text" placeholder="A TRANSLATED PLACEHOLDER"> + <div id="onlyTitle" title="ONLY A TRANSLATED TITLE"></div> + <div dir="rtl"> + DIV TEXT. + <span> + SPAN WITHIN A DIV. + </span> + </div> + <span dir="rtl"> + SPAN TEXT. + <div dir="rtl"> + DIV WITHIN A SPAN. + </div> + </span> + ` + ); + + cleanup(); + } +); + +add_task( + async function test_direction_rtl_to_ltr_sets_dir_on_content_not_attributes() { + const { translate, htmlMatches, cleanup } = await createTranslationsDoc( + /* html */ ` + <div id="content" title="A translated title"> + This block of content should get LTR direction. + </div> + <input id="onlyPlaceholder" type="text" placeholder="A translated placeholder"> + <div id="onlyTitle" title="Only a translated title"></div> + <div> + Div text. + <span>Span within a div.</span> + </div> + <span> + Span text. + <div>Div within a span.</div> + </span> + `, + { sourceLanguage: "ar", targetLanguage: "en" } + ); + + translate(); + + await htmlMatches( + 'RTL to LTR: content elements get dir="ltr", but attribute-only elements do not.', + /* html */ ` + <div id="content" title="A TRANSLATED TITLE" dir="ltr"> + THIS BLOCK OF CONTENT SHOULD GET LTR DIRECTION. + </div> + <input id="onlyPlaceholder" type="text" placeholder="A TRANSLATED PLACEHOLDER"> + <div id="onlyTitle" title="ONLY A TRANSLATED TITLE"></div> + <div dir="ltr"> + DIV TEXT. + <span> + SPAN WITHIN A DIV. + </span> + </div> + <span dir="ltr"> + SPAN TEXT. + <div dir="ltr"> + DIV WITHIN A SPAN. + </div> + </span> + ` + ); + + cleanup(); + } +); diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js @@ -678,13 +678,25 @@ const { TranslationsDocument, LRUCache } = ChromeUtils.importESModule( ); /** - * @param {string} html - * @param {{ - * mockedTranslatorPort?: (message: string) => Promise<string>, - * mockedReportVisibleChange?: () => void - * }} [options] + * Creates a translated document from the provided HTML string. + * + * @param {string} html - The HTML source to translate. + * @param {object} [options] - Optional configuration. + * @param {string} [options.sourceLanguage="en"] - Source language code (default: "en"). + * @param {string} [options.targetLanguage="en"] - Target language code (default: "en"). + * @param {(message: string) => Promise<string>} [options.mockedTranslatorPort] - Optional mock translation function. + * @param {() => void} [options.mockedReportVisibleChange] - Optional callback for visibility reporting. + * @returns {Promise<void>} Resolves when the document translation is complete. */ -async function createTranslationsDoc(html, options) { +async function createTranslationsDoc( + html, + { + sourceLanguage = "en", + targetLanguage = "es", + mockedTranslatorPort, + mockedReportVisibleChange, + } = {} +) { await SpecialPowers.pushPrefEnv({ set: [ ["browser.translations.enable", true], @@ -707,14 +719,14 @@ async function createTranslationsDoc(html, options) { info("Creating the TranslationsDocument."); translationsDoc = new TranslationsDocument( document, - "en", - "EN", + sourceLanguage, + targetLanguage, 0, // This is a fake innerWindowID - options?.mockedTranslatorPort ?? createMockedTranslatorPort(), + mockedTranslatorPort ?? createMockedTranslatorPort(), () => { throw new Error("Cannot request a new port"); }, - options?.mockedReportVisibleChange ?? (() => {}), + mockedReportVisibleChange ?? (() => {}), new LRUCache(), false );