commit 5ab9b6db2133ea9e0680853a547d7e1bc4854e4e
parent 3ed520b4183281fd1e282e762ae6bb97779d3989
Author: Erik Nordin <enordin@mozilla.com>
Date: Tue, 21 Oct 2025 21:38:46 +0000
Bug 1995403 - Apply translated script direction to <table> r=translations-reviewers,gregtatum
This patch adds some special-case logic that ensures `<table>`
elements and relevant childen are flipped to the target script
direction when translating between LTR/RTL pages.
Differential Revision: https://phabricator.services.mozilla.com/D269294
Diffstat:
4 files changed, 900 insertions(+), 553 deletions(-)
diff --git a/toolkit/components/translations/content/translations-document.sys.mjs b/toolkit/components/translations/content/translations-document.sys.mjs
@@ -3213,39 +3213,45 @@ export class TranslationsDocument {
}
/**
- * Updates the script direction of a given element,
- * only if the source and target script directions differ.
+ * Ensure the element and certain structured ancestors use the target
+ * script direction when it differs from the source script direction.
*
- * If the element is contained within a list item, then this
- * also updates the script direction of the list item as well
- * as the containing list.
+ * No-op if the source and target directions match.
*
- * This is a special-case scenario that really improves the layout
- * of lists on pages when translating to the reverse script direciton.
- *
- * @param {Element?} element
+ * @param {Element | null} element
*/
#maybeUpdateScriptDirection(element) {
- if (
- !element ||
- this.#sourceScriptDirection === this.#targetScriptDirection
- ) {
+ const targetScriptDirection = this.#targetScriptDirection;
+
+ if (!element || this.#sourceScriptDirection === targetScriptDirection) {
return;
}
- const targetScriptDirection = this.#targetScriptDirection;
+ /** @param {Element?} [el] */
+ const ensureDirection = el => {
+ el?.setAttribute("dir", targetScriptDirection);
+ };
- element.setAttribute("dir", targetScriptDirection);
+ ensureDirection(element);
const listItemAncestor = element.closest("li");
- if (!listItemAncestor) {
- return;
+ if (listItemAncestor) {
+ ensureDirection(listItemAncestor);
+ ensureDirection(listItemAncestor.closest("ul, ol"));
}
- listItemAncestor.setAttribute("dir", targetScriptDirection);
- listItemAncestor
- .closest("ul, ol")
- ?.setAttribute("dir", targetScriptDirection);
+ const tableCell = element.closest("th, td, caption");
+ if (tableCell) {
+ ensureDirection(tableCell);
+
+ const row = tableCell.closest("tr");
+ ensureDirection(row);
+
+ const body = row?.closest("tbody");
+ ensureDirection(body);
+
+ ensureDirection(body?.closest("table"));
+ }
}
/**
diff --git a/toolkit/components/translations/tests/browser/browser.toml b/toolkit/components/translations/tests/browser/browser.toml
@@ -88,3 +88,5 @@ skip-if = ["os == 'linux'"] # Bug 1821461
["browser_translations_translation_document.js"]
["browser_translations_translation_document_mutations.js"]
+
+["browser_translations_translation_document_script_direction.js"]
diff --git a/toolkit/components/translations/tests/browser/browser_translations_translation_document.js b/toolkit/components/translations/tests/browser/browser_translations_translation_document.js
@@ -1946,535 +1946,3 @@ add_task(async function test_node_specific_attribute_mutation() {
cleanup();
});
-
-add_task(
- async function test_direction_ltr_to_rtl_basic_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 (basic): 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_ltr_to_rtl_lists_ul_basic() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ul>
- <li>List item.</li>
- </ul>
- <ul>
- <li>
- Span within list item.
- <span>Span inside list item.</span>
- </li>
- </ul>
- <ul>
- <li>
- Div within list item.
- <div>Div inside list item.</div>
- </li>
- </ul>
- `,
- { sourceLanguage: "en", targetLanguage: "ar" }
- );
-
- translate();
-
- await htmlMatches(
- "LTR to RTL (UL basic): <ul> and <li> with simple nested inline/block content.",
- /* html */ `
- <ul dir="rtl">
- <li dir="rtl">
- LIST ITEM.
- </li>
- </ul>
- <ul dir="rtl">
- <li dir="rtl">
- SPAN WITHIN LIST ITEM.
- <span>
- SPAN INSIDE LIST ITEM.
- </span>
- </li>
- </ul>
- <ul dir="rtl">
- <li dir="rtl">
- DIV WITHIN LIST ITEM.
- <div dir="rtl">
- DIV INSIDE LIST ITEM.
- </div>
- </li>
- </ul>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_ltr_to_rtl_lists_ul_nested_combos() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ul>
- <li>
- Span within div within list item.
- <div>
- <span>Span inside div inside list item.</span>
- </div>
- </li>
- </ul>
- <ul>
- <li>
- Div within span within list item.
- <span>
- <div>Div inside span inside list item.</div>
- </span>
- </li>
- </ul>
- `,
- { sourceLanguage: "en", targetLanguage: "ar" }
- );
-
- translate();
-
- await htmlMatches(
- "LTR to RTL (UL nested combos): nested inline/block permutations.",
- /* html */ `
- <ul dir="rtl">
- <li dir="rtl">
- SPAN WITHIN DIV WITHIN LIST ITEM.
- <div dir="rtl">
- <span>
- SPAN INSIDE DIV INSIDE LIST ITEM.
- </span>
- </div>
- </li>
- </ul>
- <ul dir="rtl">
- <li dir="rtl">
- DIV WITHIN SPAN WITHIN LIST ITEM.
- <span>
- <div dir="rtl">
- DIV INSIDE SPAN INSIDE LIST ITEM.
- </div>
- </span>
- </li>
- </ul>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_ltr_to_rtl_lists_ol_basic() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ol>
- <li>List item.</li>
- </ol>
- <ol>
- <li>
- Span within list item.
- <span>Span inside list item.</span>
- </li>
- </ol>
- <ol>
- <li>
- Div within list item.
- <div>Div inside list item.</div>
- </li>
- </ol>
- `,
- { sourceLanguage: "en", targetLanguage: "ar" }
- );
-
- translate();
-
- await htmlMatches(
- "LTR to RTL (OL basic): <ol> and <li> with simple nested inline/block content.",
- /* html */ `
- <ol dir="rtl">
- <li dir="rtl">
- LIST ITEM.
- </li>
- </ol>
- <ol dir="rtl">
- <li dir="rtl">
- SPAN WITHIN LIST ITEM.
- <span>
- SPAN INSIDE LIST ITEM.
- </span>
- </li>
- </ol>
- <ol dir="rtl">
- <li dir="rtl">
- DIV WITHIN LIST ITEM.
- <div dir="rtl">
- DIV INSIDE LIST ITEM.
- </div>
- </li>
- </ol>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_ltr_to_rtl_lists_ol_nested_combos() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ol>
- <li>
- Span within div within list item.
- <div>
- <span>Span inside div inside list item.</span>
- </div>
- </li>
- </ol>
- <ol>
- <li>
- Div within span within list item.
- <span>
- <div>Div inside span inside list item.</div>
- </span>
- </li>
- </ol>
- `,
- { sourceLanguage: "en", targetLanguage: "ar" }
- );
-
- translate();
-
- await htmlMatches(
- "LTR to RTL (OL nested combos): nested inline/block permutations.",
- /* html */ `
- <ol dir="rtl">
- <li dir="rtl">
- SPAN WITHIN DIV WITHIN LIST ITEM.
- <div dir="rtl">
- <span>
- SPAN INSIDE DIV INSIDE LIST ITEM.
- </span>
- </div>
- </li>
- </ol>
- <ol dir="rtl">
- <li dir="rtl">
- DIV WITHIN SPAN WITHIN LIST ITEM.
- <span>
- <div dir="rtl">
- DIV INSIDE SPAN INSIDE LIST ITEM.
- </div>
- </span>
- </li>
- </ol>
- `
- );
-
- cleanup();
-});
-
-add_task(
- async function test_direction_rtl_to_ltr_basic_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 (basic): 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();
- }
-);
-
-add_task(async function test_direction_rtl_to_ltr_lists_ul_basic() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ul>
- <li>List item.</li>
- </ul>
- <ul>
- <li>
- Span within list item.
- <span>Span inside list item.</span>
- </li>
- </ul>
- <ul>
- <li>
- Div within list item.
- <div>Div inside list item.</div>
- </li>
- </ul>
- `,
- { sourceLanguage: "ar", targetLanguage: "en" }
- );
-
- translate();
-
- await htmlMatches(
- "RTL to LTR (UL basic): <ul> and <li> with simple nested inline/block content.",
- /* html */ `
- <ul dir="ltr">
- <li dir="ltr">
- LIST ITEM.
- </li>
- </ul>
- <ul dir="ltr">
- <li dir="ltr">
- SPAN WITHIN LIST ITEM.
- <span>
- SPAN INSIDE LIST ITEM.
- </span>
- </li>
- </ul>
- <ul dir="ltr">
- <li dir="ltr">
- DIV WITHIN LIST ITEM.
- <div dir="ltr">
- DIV INSIDE LIST ITEM.
- </div>
- </li>
- </ul>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_rtl_to_ltr_lists_ul_nested_combos() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ul>
- <li>
- Span within div within list item.
- <div>
- <span>Span inside div inside list item.</span>
- </div>
- </li>
- </ul>
- <ul>
- <li>
- Div within span within list item.
- <span>
- <div>Div inside span inside list item.</div>
- </span>
- </li>
- </ul>
- `,
- { sourceLanguage: "ar", targetLanguage: "en" }
- );
-
- translate();
-
- await htmlMatches(
- "RTL to LTR (UL nested combos): nested inline/block permutations.",
- /* html */ `
- <ul dir="ltr">
- <li dir="ltr">
- SPAN WITHIN DIV WITHIN LIST ITEM.
- <div dir="ltr">
- <span>
- SPAN INSIDE DIV INSIDE LIST ITEM.
- </span>
- </div>
- </li>
- </ul>
- <ul dir="ltr">
- <li dir="ltr">
- DIV WITHIN SPAN WITHIN LIST ITEM.
- <span>
- <div dir="ltr">
- DIV INSIDE SPAN INSIDE LIST ITEM.
- </div>
- </span>
- </li>
- </ul>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_rtl_to_ltr_lists_ol_basic() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ol>
- <li>List item.</li>
- </ol>
- <ol>
- <li>
- Span within list item.
- <span>Span inside list item.</span>
- </li>
- </ol>
- <ol>
- <li>
- Div within list item.
- <div>Div inside list item.</div>
- </li>
- </ol>
- `,
- { sourceLanguage: "ar", targetLanguage: "en" }
- );
-
- translate();
-
- await htmlMatches(
- "RTL to LTR (OL basic): <ol> and <li> with simple nested inline/block content.",
- /* html */ `
- <ol dir="ltr">
- <li dir="ltr">
- LIST ITEM.
- </li>
- </ol>
- <ol dir="ltr">
- <li dir="ltr">
- SPAN WITHIN LIST ITEM.
- <span>
- SPAN INSIDE LIST ITEM.
- </span>
- </li>
- </ol>
- <ol dir="ltr">
- <li dir="ltr">
- DIV WITHIN LIST ITEM.
- <div dir="ltr">
- DIV INSIDE LIST ITEM.
- </div>
- </li>
- </ol>
- `
- );
-
- cleanup();
-});
-
-add_task(async function test_direction_rtl_to_ltr_lists_ol_nested_combos() {
- const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
- /* html */ `
- <ol>
- <li>
- Span within div within list item.
- <div>
- <span>Span inside div inside list item.</span>
- </div>
- </li>
- </ol>
- <ol>
- <li>
- Div within span within list item.
- <span>
- <div>Div inside span inside list item.</div>
- </span>
- </li>
- </ol>
- `,
- { sourceLanguage: "ar", targetLanguage: "en" }
- );
-
- translate();
-
- await htmlMatches(
- "RTL to LTR (OL nested combos): nested inline/block permutations.",
- /* html */ `
- <ol dir="ltr">
- <li dir="ltr">
- SPAN WITHIN DIV WITHIN LIST ITEM.
- <div dir="ltr">
- <span>
- SPAN INSIDE DIV INSIDE LIST ITEM.
- </span>
- </div>
- </li>
- </ol>
- <ol dir="ltr">
- <li dir="ltr">
- DIV WITHIN SPAN WITHIN LIST ITEM.
- <span>
- <div dir="ltr">
- DIV INSIDE SPAN INSIDE LIST ITEM.
- </div>
- </span>
- </li>
- </ol>
- `
- );
-
- cleanup();
-});
diff --git a/toolkit/components/translations/tests/browser/browser_translations_translation_document_script_direction.js b/toolkit/components/translations/tests/browser/browser_translations_translation_document_script_direction.js
@@ -0,0 +1,871 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Request 2x longer timeout for this test.
+ * There are lot of test cases in this file, but they are all of the same nature,
+ * and it makes the most sense to have them all in this single test file.
+ */
+requestLongerTimeout(2);
+
+add_task(
+ async function test_direction_ltr_to_rtl_basic_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 (basic): 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_ltr_to_rtl_lists_ul_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ul>
+ <li>List item.</li>
+ </ul>
+ <ul>
+ <li>
+ Span within list item.
+ <span>Span inside list item.</span>
+ </li>
+ </ul>
+ <ul>
+ <li>
+ Div within list item.
+ <div>Div inside list item.</div>
+ </li>
+ </ul>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (UL basic): <ul> and <li> with simple nested inline/block content.",
+ /* html */ `
+ <ul dir="rtl">
+ <li dir="rtl">
+ LIST ITEM.
+ </li>
+ </ul>
+ <ul dir="rtl">
+ <li dir="rtl">
+ SPAN WITHIN LIST ITEM.
+ <span>
+ SPAN INSIDE LIST ITEM.
+ </span>
+ </li>
+ </ul>
+ <ul dir="rtl">
+ <li dir="rtl">
+ DIV WITHIN LIST ITEM.
+ <div dir="rtl">
+ DIV INSIDE LIST ITEM.
+ </div>
+ </li>
+ </ul>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_ltr_to_rtl_lists_ul_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ul>
+ <li>
+ Span within div within list item.
+ <div>
+ <span>Span inside div inside list item.</span>
+ </div>
+ </li>
+ </ul>
+ <ul>
+ <li>
+ Div within span within list item.
+ <span>
+ <div>Div inside span inside list item.</div>
+ </span>
+ </li>
+ </ul>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (UL nested combos): nested inline/block permutations.",
+ /* html */ `
+ <ul dir="rtl">
+ <li dir="rtl">
+ SPAN WITHIN DIV WITHIN LIST ITEM.
+ <div dir="rtl">
+ <span>
+ SPAN INSIDE DIV INSIDE LIST ITEM.
+ </span>
+ </div>
+ </li>
+ </ul>
+ <ul dir="rtl">
+ <li dir="rtl">
+ DIV WITHIN SPAN WITHIN LIST ITEM.
+ <span>
+ <div dir="rtl">
+ DIV INSIDE SPAN INSIDE LIST ITEM.
+ </div>
+ </span>
+ </li>
+ </ul>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_ltr_to_rtl_lists_ol_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ol>
+ <li>List item.</li>
+ </ol>
+ <ol>
+ <li>
+ Span within list item.
+ <span>Span inside list item.</span>
+ </li>
+ </ol>
+ <ol>
+ <li>
+ Div within list item.
+ <div>Div inside list item.</div>
+ </li>
+ </ol>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (OL basic): <ol> and <li> with simple nested inline/block content.",
+ /* html */ `
+ <ol dir="rtl">
+ <li dir="rtl">
+ LIST ITEM.
+ </li>
+ </ol>
+ <ol dir="rtl">
+ <li dir="rtl">
+ SPAN WITHIN LIST ITEM.
+ <span>
+ SPAN INSIDE LIST ITEM.
+ </span>
+ </li>
+ </ol>
+ <ol dir="rtl">
+ <li dir="rtl">
+ DIV WITHIN LIST ITEM.
+ <div dir="rtl">
+ DIV INSIDE LIST ITEM.
+ </div>
+ </li>
+ </ol>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_ltr_to_rtl_lists_ol_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ol>
+ <li>
+ Span within div within list item.
+ <div>
+ <span>Span inside div inside list item.</span>
+ </div>
+ </li>
+ </ol>
+ <ol>
+ <li>
+ Div within span within list item.
+ <span>
+ <div>Div inside span inside list item.</div>
+ </span>
+ </li>
+ </ol>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (OL nested combos): nested inline/block permutations.",
+ /* html */ `
+ <ol dir="rtl">
+ <li dir="rtl">
+ SPAN WITHIN DIV WITHIN LIST ITEM.
+ <div dir="rtl">
+ <span>
+ SPAN INSIDE DIV INSIDE LIST ITEM.
+ </span>
+ </div>
+ </li>
+ </ol>
+ <ol dir="rtl">
+ <li dir="rtl">
+ DIV WITHIN SPAN WITHIN LIST ITEM.
+ <span>
+ <div dir="rtl">
+ DIV INSIDE SPAN INSIDE LIST ITEM.
+ </div>
+ </span>
+ </li>
+ </ol>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(
+ async function test_direction_rtl_to_ltr_basic_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 (basic): 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();
+ }
+);
+
+add_task(async function test_direction_rtl_to_ltr_lists_ul_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ul>
+ <li>List item.</li>
+ </ul>
+ <ul>
+ <li>
+ Span within list item.
+ <span>Span inside list item.</span>
+ </li>
+ </ul>
+ <ul>
+ <li>
+ Div within list item.
+ <div>Div inside list item.</div>
+ </li>
+ </ul>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (UL basic): <ul> and <li> with simple nested inline/block content.",
+ /* html */ `
+ <ul dir="ltr">
+ <li dir="ltr">
+ LIST ITEM.
+ </li>
+ </ul>
+ <ul dir="ltr">
+ <li dir="ltr">
+ SPAN WITHIN LIST ITEM.
+ <span>
+ SPAN INSIDE LIST ITEM.
+ </span>
+ </li>
+ </ul>
+ <ul dir="ltr">
+ <li dir="ltr">
+ DIV WITHIN LIST ITEM.
+ <div dir="ltr">
+ DIV INSIDE LIST ITEM.
+ </div>
+ </li>
+ </ul>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_rtl_to_ltr_lists_ul_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ul>
+ <li>
+ Span within div within list item.
+ <div>
+ <span>Span inside div inside list item.</span>
+ </div>
+ </li>
+ </ul>
+ <ul>
+ <li>
+ Div within span within list item.
+ <span>
+ <div>Div inside span inside list item.</div>
+ </span>
+ </li>
+ </ul>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (UL nested combos): nested inline/block permutations.",
+ /* html */ `
+ <ul dir="ltr">
+ <li dir="ltr">
+ SPAN WITHIN DIV WITHIN LIST ITEM.
+ <div dir="ltr">
+ <span>
+ SPAN INSIDE DIV INSIDE LIST ITEM.
+ </span>
+ </div>
+ </li>
+ </ul>
+ <ul dir="ltr">
+ <li dir="ltr">
+ DIV WITHIN SPAN WITHIN LIST ITEM.
+ <span>
+ <div dir="ltr">
+ DIV INSIDE SPAN INSIDE LIST ITEM.
+ </div>
+ </span>
+ </li>
+ </ul>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_rtl_to_ltr_lists_ol_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ol>
+ <li>List item.</li>
+ </ol>
+ <ol>
+ <li>
+ Span within list item.
+ <span>Span inside list item.</span>
+ </li>
+ </ol>
+ <ol>
+ <li>
+ Div within list item.
+ <div>Div inside list item.</div>
+ </li>
+ </ol>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (OL basic): <ol> and <li> with simple nested inline/block content.",
+ /* html */ `
+ <ol dir="ltr">
+ <li dir="ltr">
+ LIST ITEM.
+ </li>
+ </ol>
+ <ol dir="ltr">
+ <li dir="ltr">
+ SPAN WITHIN LIST ITEM.
+ <span>
+ SPAN INSIDE LIST ITEM.
+ </span>
+ </li>
+ </ol>
+ <ol dir="ltr">
+ <li dir="ltr">
+ DIV WITHIN LIST ITEM.
+ <div dir="ltr">
+ DIV INSIDE LIST ITEM.
+ </div>
+ </li>
+ </ol>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_rtl_to_ltr_lists_ol_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <ol>
+ <li>
+ Span within div within list item.
+ <div>
+ <span>Span inside div inside list item.</span>
+ </div>
+ </li>
+ </ol>
+ <ol>
+ <li>
+ Div within span within list item.
+ <span>
+ <div>Div inside span inside list item.</div>
+ </span>
+ </li>
+ </ol>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (OL nested combos): nested inline/block permutations.",
+ /* html */ `
+ <ol dir="ltr">
+ <li dir="ltr">
+ SPAN WITHIN DIV WITHIN LIST ITEM.
+ <div dir="ltr">
+ <span>
+ SPAN INSIDE DIV INSIDE LIST ITEM.
+ </span>
+ </div>
+ </li>
+ </ol>
+ <ol dir="ltr">
+ <li dir="ltr">
+ DIV WITHIN SPAN WITHIN LIST ITEM.
+ <span>
+ <div dir="ltr">
+ DIV INSIDE SPAN INSIDE LIST ITEM.
+ </div>
+ </span>
+ </li>
+ </ol>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_ltr_to_rtl_tables_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <table>
+ <tbody>
+ <tr>
+ <td>Cell text.</td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Span within cell.
+ <span>Span inside cell.</span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Div within cell.
+ <div>Div inside cell.</div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <th>Header cell.</th>
+ </tr>
+ </tbody>
+ </table>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (TABLE basic): <table> and <td>/<th> with simple nested inline/block content.",
+ /* html */ `
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <td dir="rtl">
+ CELL TEXT.
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <td dir="rtl">
+ SPAN WITHIN CELL.
+ <span>
+ SPAN INSIDE CELL.
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <td dir="rtl">
+ DIV WITHIN CELL.
+ <div dir="rtl">
+ DIV INSIDE CELL.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <th dir="rtl">
+ HEADER CELL.
+ </th>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_ltr_to_rtl_tables_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Span within div within cell.
+ <div>
+ <span>Span inside div inside cell.</span>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Div within span within cell.
+ <span>
+ <div>Div inside span inside cell.</div>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `,
+ { sourceLanguage: "en", targetLanguage: "ar" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "LTR to RTL (TABLE nested combos): nested inline/block permutations.",
+ /* html */ `
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <td dir="rtl">
+ SPAN WITHIN DIV WITHIN CELL.
+ <div dir="rtl">
+ <span>
+ SPAN INSIDE DIV INSIDE CELL.
+ </span>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="rtl">
+ <tbody dir="rtl">
+ <tr dir="rtl">
+ <td dir="rtl">
+ DIV WITHIN SPAN WITHIN CELL.
+ <span>
+ <div dir="rtl">
+ DIV INSIDE SPAN INSIDE CELL.
+ </div>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_rtl_to_ltr_tables_basic() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <table>
+ <tbody>
+ <tr>
+ <td>Cell text.</td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Span within cell.
+ <span>Span inside cell.</span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Div within cell.
+ <div>Div inside cell.</div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <th>Header cell.</th>
+ </tr>
+ </tbody>
+ </table>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (TABLE basic): <table> and <td>/<th> with simple nested inline/block content.",
+ /* html */ `
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <td dir="ltr">
+ CELL TEXT.
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <td dir="ltr">
+ SPAN WITHIN CELL.
+ <span>
+ SPAN INSIDE CELL.
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <td dir="ltr">
+ DIV WITHIN CELL.
+ <div dir="ltr">
+ DIV INSIDE CELL.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <th dir="ltr">
+ HEADER CELL.
+ </th>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+
+ cleanup();
+});
+
+add_task(async function test_direction_rtl_to_ltr_tables_nested_combos() {
+ const { translate, htmlMatches, cleanup } = await createTranslationsDoc(
+ /* html */ `
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Span within div within cell.
+ <div>
+ <span>Span inside div inside cell.</span>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ Div within span within cell.
+ <span>
+ <div>Div inside span inside cell.</div>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `,
+ { sourceLanguage: "ar", targetLanguage: "en" }
+ );
+
+ translate();
+
+ await htmlMatches(
+ "RTL to LTR (TABLE nested combos): nested inline/block permutations.",
+ /* html */ `
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <td dir="ltr">
+ SPAN WITHIN DIV WITHIN CELL.
+ <div dir="ltr">
+ <span>
+ SPAN INSIDE DIV INSIDE CELL.
+ </span>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table dir="ltr">
+ <tbody dir="ltr">
+ <tr dir="ltr">
+ <td dir="ltr">
+ DIV WITHIN SPAN WITHIN CELL.
+ <span>
+ <div dir="ltr">
+ DIV INSIDE SPAN INSIDE CELL.
+ </div>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+
+ cleanup();
+});