commit c06a945be3fc35232223b32c48484517a0a24752
parent 715d3b0dece00ac6c89ec879f13be049cb4b1713
Author: Erik Nordin <enordin@mozilla.com>
Date: Wed, 3 Dec 2025 21:37:09 +0000
Bug 2003545 - Support Full-Page Translations for standalone SVGs r=translations-reviewers,gregtatum
This commit expands the context in which we add root elements to
the TranslationsDocument to support cases where the document may
not have a body. This happens in the case of a standalone SVG page.
Differential Revision: https://phabricator.services.mozilla.com/D274776
Diffstat:
3 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/toolkit/components/translations/content/translations-document.sys.mjs b/toolkit/components/translations/content/translations-document.sys.mjs
@@ -1590,6 +1590,10 @@ export class TranslationsDocument {
this.#addRootElement(document.body);
this.#addRootElement(document.head);
this.#addRootElement(document.querySelector("title"));
+ if (!document.body && document.documentElement) {
+ // Handle documents such as standalone SVGs that lack a body.
+ this.#addRootElement(document.documentElement);
+ }
ChromeUtils.addProfilerMarker(
"TranslationsDocument Initialize",
@@ -1624,7 +1628,13 @@ export class TranslationsDocument {
}
};
- if (document.body) {
+ if (
+ // There exists a document body, so we are clear to continue.
+ document.body ||
+ // The page has finished loading, but there is no document body.
+ // There may still be roots to add, such as in the case of a standalone SVG.
+ document.readyState !== "loading"
+ ) {
addRootElements();
} else {
// The TranslationsDocument was invoked before the DOM was ready, wait for
@@ -2404,7 +2414,7 @@ export class TranslationsDocument {
return;
}
- const element = asHTMLElement(node);
+ const element = asElement(node);
if (!element) {
return;
}
diff --git a/toolkit/components/translations/tests/browser/browser_translations_translation_document.js b/toolkit/components/translations/tests/browser/browser_translations_translation_document.js
@@ -1004,6 +1004,37 @@ add_task(async function test_svgs_more() {
await cleanup();
});
+add_task(async function test_standalone_svg_document() {
+ const svgSource = /* html */ `
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <title>Test title</title>
+ <text x="10" y="20">Test text inside of standalone SVG.</text>
+ </svg>
+ `;
+
+ const { translate, htmlMatches, cleanup, document } =
+ await createTranslationsDoc(svgSource, { parserType: "image/svg+xml" });
+
+ translate();
+
+ await htmlMatches(
+ "Standalone SVG documents are translated.",
+ /* html */ `
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <title>
+ TEST TITLE
+ </title>
+ <text x="10" y="20">
+ TEST TEXT INSIDE OF STANDALONE SVG.
+ </text>
+ </svg>
+ `,
+ document
+ );
+
+ await cleanup();
+});
+
add_task(async function test_tables() {
const { translate, htmlMatches, cleanup } =
await createTranslationsDoc(/* html */ `
diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js
@@ -684,6 +684,7 @@ const { TranslationsDocument, LRUCache } = ChromeUtils.importESModule(
* @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 {DOMParserSupportedType} [options.parserType="text/html"] - Parser type for the source content.
* @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.
@@ -693,6 +694,7 @@ async function createTranslationsDoc(
{
sourceLanguage = "en",
targetLanguage = "es",
+ parserType = "text/html",
mockedTranslatorPort,
mockedReportVisibleChange,
} = {}
@@ -706,12 +708,14 @@ async function createTranslationsDoc(
});
const parser = new DOMParser();
- const document = parser.parseFromString(html, "text/html");
+ const document = parser.parseFromString(html, parserType);
// For some reason, the document <body> here from the DOMParser is "display: flex" by
// default. Ensure that it is "display: block" instead, otherwise the children of the
// <body> will not be "display: inline".
- document.body.style.display = "block";
+ if (document.body) {
+ document.body.style.display = "block";
+ }
let translationsDoc = null;
@@ -823,6 +827,12 @@ async function createTranslationsDoc(
let didSimulateIntersectionObservation = false;
+ const getHTMLSource = () => {
+ return (
+ sourceDoc.body?.innerHTML ?? sourceDoc.documentElement?.outerHTML ?? ""
+ );
+ };
+
try {
await waitForCondition(async () => {
await waitForCondition(
@@ -860,7 +870,7 @@ async function createTranslationsDoc(
() => !translationsDoc.hasPendingCallbackOnEventLoop()
);
- const actualHtml = naivelyPrettify(sourceDoc.body.innerHTML);
+ const actualHtml = naivelyPrettify(getHTMLSource());
const htmlMatches = expected.test(actualHtml);
if (!htmlMatches && !didSimulateIntersectionObservation) {
@@ -893,7 +903,7 @@ async function createTranslationsDoc(
console.error(error);
// Provide a nice error message.
- const actual = naivelyPrettify(sourceDoc.body.innerHTML);
+ const actual = naivelyPrettify(getHTMLSource());
ok(
false,
`${message}\n\nExpected HTML:\n\n${