mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 07:42:04 +00:00
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:
parent
4084d5c48b
commit
ff33f89421
@ -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);
|
||||
};
|
||||
|
@ -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(); }
|
||||
|
@ -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; };
|
||||
};
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user