Bug 1627809 - Make DocumentL10n translate all roots and add ConnectRoot with initial translation. r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D70981
This commit is contained in:
Zibi Braniecki 2020-04-30 17:56:54 +00:00
parent 4084d5c48b
commit ff33f89421
6 changed files with 286 additions and 40 deletions

View File

@ -24,4 +24,24 @@ interface DocumentL10n : DOMLocalization {
* fetching is complete and the initial translation of the DOM is finished.
*/
readonly attribute Promise<any> ready;
/**
* An overload for the DOMLocalization::connectRoot which takes an optional second
* argument to allow the user to express an intent of translating the root
* as soon as the localization becomes available.
*
* If the root is being connected while the document is still being parsed,
* then irrelevant of the value of the second argument, it will be translated
* as part of the initial translation step right after the parsing completes.
*
* If the root is being connected after the document is parsed, then the
* second argument controls whether the root is also going to get translated,
* or just connected.
*
* This is a temporary workaround to avoid having to wait for the `DocumentL10n`
* to become active. It should be unnecessary once we remove JSM and make
* the `TranslateFragment` be available immediately when `DocumentL10n` becomes
* available.
*/
[Throws] void connectRoot(Node aElement, optional boolean aTranslate = false);
};

View File

@ -120,29 +120,67 @@ void DocumentL10n::TriggerInitialTranslation() {
return;
}
mState = DocumentL10nState::InitialTranslationTriggered;
nsTArray<RefPtr<Promise>> promises;
Element* elem = mDocument->GetDocumentElement();
if (!elem) {
ErrorResult rv;
promises.AppendElement(TranslateDocument(rv));
if (NS_WARN_IF(rv.Failed())) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
}
promises.AppendElement(TranslateRoots(rv));
Element* documentElement = mDocument->GetDocumentElement();
if (!documentElement) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
}
ErrorResult rv;
// 1. Collect all localizable elements.
Sequence<OwningNonNull<Element>> elements;
GetTranslatables(*elem, elements, rv);
DOMLocalization::ConnectRoot(*documentElement, rv);
if (NS_WARN_IF(rv.Failed())) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
}
RefPtr<nsXULPrototypeDocument> proto = mDocument->GetPrototype();
AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation");
RefPtr<Promise> promise = Promise::All(aes.cx(), promises, rv);
RefPtr<Promise> promise;
if (promise->State() == Promise::PromiseState::Resolved) {
// If the promise is already resolved, we can fast-track
// to initial translation completed.
InitialTranslationCompleted();
mReady->MaybeResolveWithUndefined();
} else {
RefPtr<PromiseNativeHandler> l10nReadyHandler =
new L10nReadyHandler(mReady, this);
promise->AppendNativeHandler(l10nReadyHandler);
mState = DocumentL10nState::InitialTranslationTriggered;
}
}
already_AddRefed<Promise> DocumentL10n::TranslateDocument(ErrorResult& aRv) {
MOZ_ASSERT(mState == DocumentL10nState::Activated,
"This method should be called only from Activated state.");
RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
Element* elem = mDocument->GetDocumentElement();
if (!elem) {
promise->MaybeRejectWithUndefined();
return promise.forget();
}
// 1. Collect all localizable elements.
Sequence<OwningNonNull<Element>> elements;
GetTranslatables(*elem, elements, aRv);
if (NS_WARN_IF(aRv.Failed())) {
promise->MaybeRejectWithUndefined();
return promise.forget();
}
RefPtr<nsXULPrototypeDocument> proto = mDocument->GetPrototype();
// 2. Check if the document has a prototype that may cache
// translated elements.
@ -181,11 +219,11 @@ void DocumentL10n::TriggerInitialTranslation() {
// 2.1.2. If we're not loading from cache, push the elements that
// are in the prototype to be translated and cached.
if (!proto->WasL10nCached() && !elements.IsEmpty()) {
RefPtr<Promise> translatePromise = TranslateElements(elements, proto, rv);
if (NS_WARN_IF(!translatePromise || rv.Failed())) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
RefPtr<Promise> translatePromise =
TranslateElements(elements, proto, aRv);
if (NS_WARN_IF(!translatePromise || aRv.Failed())) {
promise->MaybeRejectWithUndefined();
return promise.forget();
}
promises.AppendElement(translatePromise);
}
@ -195,46 +233,31 @@ void DocumentL10n::TriggerInitialTranslation() {
// independently of if we're loading from cache.
if (!nonProtoElements.IsEmpty()) {
RefPtr<Promise> nonProtoTranslatePromise =
TranslateElements(nonProtoElements, nullptr, rv);
if (NS_WARN_IF(!nonProtoTranslatePromise || rv.Failed())) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
TranslateElements(nonProtoElements, nullptr, aRv);
if (NS_WARN_IF(!nonProtoTranslatePromise || aRv.Failed())) {
promise->MaybeRejectWithUndefined();
return promise.forget();
}
promises.AppendElement(nonProtoTranslatePromise);
}
// 2.1.4. Collect promises with Promise::All (maybe empty).
AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslationCompleted");
promise = Promise::All(aes.cx(), promises, rv);
promise = Promise::All(aes.cx(), promises, aRv);
} else {
// 2.2. Handle the case when we don't have proto.
// 2.2.1. Otherwise, translate all available elements,
// without attempting to cache them.
promise = TranslateElements(elements, nullptr, rv);
promise = TranslateElements(elements, nullptr, aRv);
}
if (NS_WARN_IF(!promise || rv.Failed())) {
InitialTranslationCompleted();
mReady->MaybeRejectWithUndefined();
return;
if (NS_WARN_IF(!promise || aRv.Failed())) {
promise->MaybeRejectWithUndefined();
return promise.forget();
}
// 3. Connect the root to L10nMutations observer.
ConnectRoot(*elem, rv);
// 4. Check if the promise is already resolved.
if (promise->State() == Promise::PromiseState::Resolved) {
// 4.1. If it is, resolved immediatelly.
InitialTranslationCompleted();
mReady->MaybeResolveWithUndefined();
} else {
// 4.2. If not, schedule the L10nReadyHandler.
RefPtr<PromiseNativeHandler> l10nReadyHandler =
new L10nReadyHandler(mReady, this);
promise->AppendNativeHandler(l10nReadyHandler);
}
return promise.forget();
}
void DocumentL10n::InitialTranslationCompleted() {
@ -265,6 +288,16 @@ void DocumentL10n::InitialTranslationCompleted() {
}
}
void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate,
ErrorResult& aRv) {
if (aTranslate) {
if (mState >= DocumentL10nState::InitialTranslationTriggered) {
RefPtr<Promise> promise = TranslateFragment(aNode, aRv);
}
}
DOMLocalization::ConnectRoot(aNode, aRv);
}
Promise* DocumentL10n::Ready() { return mReady; }
void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); }

View File

@ -69,12 +69,15 @@ class DocumentL10n final : public DOMLocalization {
Promise* Ready();
void TriggerInitialTranslation();
already_AddRefed<Promise> TranslateDocument(ErrorResult& aRv);
void InitialTranslationCompleted();
Document* GetDocument() { return mDocument; };
void OnCreatePresShell();
void ConnectRoot(nsINode& aNode, bool aTranslate, ErrorResult& aRv);
DocumentL10nState GetState() { return mState; };
};

View File

@ -40,3 +40,5 @@
[document_l10n/test_docl10n_ready_rejected.html]
[document_l10n/test_docl10n_removeResourceIds.html]
[document_l10n/test_docl10n_lazy.html]
[document_l10n/test_connectRoot_webcomponent.html]
[document_l10n/test_connectRoot_webcomponent_lazy.html]

View File

@ -0,0 +1,90 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test Web Component connecting into Document's l10n</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<link rel="localization" href="browser/preferences/preferences.ftl"></link>
<script type="application/javascript">
SimpleTest.waitForExplicitFinish();
// In this test we are introducing two widgets. The only difference between them
// is that the first one is using `connectRoot` with the `aTranslate` argument set to `true`,
// and the other one to `false`.
//
// In this test, we will inject both of them into the DOM for parsing.
// For a test that verifies the behavior when they're injected lazily, see
// `test_connectRoot_webcomponent_lazy.html` test.
//
// Since both widgets get injected into DOM during parsing, we expect both of them
// to get translated before `document.l10n.ready` is resolved.
let passedTests = 0;
class FluentWidget extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: "open"});
const t = document.querySelector("#fluent-widget-template");
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
async connectedCallback() {
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
document.l10n.connectRoot(this.shadowRoot, true);
let label = this.shadowRoot.getElementById("label");
await document.l10n.ready;
is(label.textContent, "Learn more", "localization content applied to element");
passedTests++;
if (passedTests == 2) {
SimpleTest.finish();
}
}
}
class FluentWidget2 extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: "open"});
const t = document.querySelector("#fluent-widget-template");
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
async connectedCallback() {
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
document.l10n.connectRoot(this.shadowRoot, false);
let label = this.shadowRoot.getElementById("label");
await document.l10n.ready;
is(label.textContent, "Learn more", "localization content applied to element");
passedTests++;
if (passedTests == 2) {
SimpleTest.finish();
}
}
}
customElements.define("fluent-widget", FluentWidget);
customElements.define("fluent-widget2", FluentWidget2);
</script>
</head>
<body>
<template id="fluent-widget-template">
<div>
<button id="label" data-l10n-id="do-not-track-learn-more"></button>
</div>
</template>
<fluent-widget></fluent-widget>
<fluent-widget2></fluent-widget2>
<script>
// This trick makes sure that we connect the widgets before parsing is completed.
document.write("");
</script>
</body>
</html>

View File

@ -0,0 +1,98 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test Web Component connecting into Document's l10n</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
SimpleTest.waitForExplicitFinish();
// In this test we are introducing two widgets. The only difference between them
// is that the first one is using `connectRoot` with the `aTranslate` argument set to `true`,
// and the other one to `false`.
//
// In this test, we will inject both of them lazily, after initial parsing is completed.
// For a test that verifies the behavior when they're injected during parsing, see
// `test_connectRoot_webcomponent.html` test.
//
// The expected difference is that when both get lazily injected into the DOM, the first one
// will get translated, while the other will not.
// The latter behavior will be used by widgets that will want to translate the initial DOM on their
// own before connecting the root.
let firstWidgetTranslated = false;
class FluentWidget extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: "open"});
const t = document.querySelector("#fluent-widget-template");
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
connectedCallback() {
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
document.l10n.connectRoot(this.shadowRoot, true);
let label = this.shadowRoot.getElementById("label");
let verifyL10n = () => {
if (label.textContent.length > 0) {
window.removeEventListener("MozAfterPaint", verifyL10n);
is(label.textContent, "Learn more", "localization content applied to element");
firstWidgetTranslated = true;
}
};
window.addEventListener("MozAfterPaint", verifyL10n);
}
}
class FluentWidget2 extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: "open"});
const t = document.querySelector("#fluent-widget-template");
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
connectedCallback() {
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
document.l10n.connectRoot(this.shadowRoot, false);
let label = this.shadowRoot.getElementById("label");
let verifyL10n = () => {
if (firstWidgetTranslated) {
is(label.textContent.length, 0, "This widget should remain untranslated.");
window.removeEventListener("MozAfterPaint", verifyL10n);
SimpleTest.finish();
}
};
window.addEventListener("MozAfterPaint", verifyL10n);
}
}
customElements.define("fluent-widget", FluentWidget);
customElements.define("fluent-widget2", FluentWidget2);
window.addEventListener("load", () => {
window.requestIdleCallback(async () => {
let widget = document.createElement("fluent-widget");
document.body.appendChild(widget);
let widget2 = document.createElement("fluent-widget2");
document.body.appendChild(widget2);
});
}, { once: true });
</script>
</head>
<body>
<template id="fluent-widget-template">
<div>
<button id="label" data-l10n-id="do-not-track-learn-more"></button>
</div>
</template>
</body>
</html>