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:
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
);