diff options
Diffstat (limited to '')
-rw-r--r-- | dom/l10n/DocumentL10n.cpp | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/dom/l10n/DocumentL10n.cpp b/dom/l10n/DocumentL10n.cpp new file mode 100644 index 0000000000..828a0583e3 --- /dev/null +++ b/dom/l10n/DocumentL10n.cpp @@ -0,0 +1,347 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DocumentL10n.h" +#include "nsIContentSink.h" +#include "nsContentUtils.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentL10nBinding.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::intl; +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n, DOMLocalization) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n, DOMLocalization) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(DocumentL10n, DOMLocalization) +NS_IMPL_RELEASE_INHERITED(DocumentL10n, DOMLocalization) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n) +NS_INTERFACE_MAP_END_INHERITING(DOMLocalization) + +bool DocumentL10n::mIsFirstBrowserWindow = true; + +/* static */ +RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync) { + RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync); + + IgnoredErrorResult rv; + l10n->mReady = Promise::Create(l10n->mGlobal, rv); + if (NS_WARN_IF(rv.Failed())) { + return nullptr; + } + + return l10n.forget(); +} + +DocumentL10n::DocumentL10n(Document* aDocument, bool aSync) + : DOMLocalization(aDocument->GetScopeObject(), aSync), + mDocument(aDocument), + mState(DocumentL10nState::Constructed) { + mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink()); +} + +JSObject* DocumentL10n::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto); +} + +class L10nReadyHandler final : public PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler) + + explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n) + : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + mDocumentL10n->InitialTranslationCompleted(true); + mPromise->MaybeResolveWithUndefined(); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + mDocumentL10n->InitialTranslationCompleted(false); + + nsTArray<nsCString> errors{ + "[dom/l10n] Could not complete initial document translation."_ns, + }; + IgnoredErrorResult rv; + MaybeReportErrorsToGecko(errors, rv, mDocumentL10n->GetParentObject()); + + /** + * We resolve the mReady here even if we encountered failures, because + * there is nothing actionable for the user pending on `mReady` to do here + * and we don't want to incentivized consumers of this API to plan the + * same pending operation for resolve and reject scenario. + * + * Additionally, without it, the stderr received "uncaught promise + * rejection" warning, which is noisy and not-actionable. + * + * So instead, we just resolve and report errors. + */ + mPromise->MaybeResolveWithUndefined(); + } + + private: + ~L10nReadyHandler() = default; + + RefPtr<Promise> mPromise; + RefPtr<DocumentL10n> mDocumentL10n; +}; + +NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise, mDocumentL10n) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler) + +void DocumentL10n::TriggerInitialTranslation() { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + if (mState >= DocumentL10nState::InitialTranslationTriggered) { + return; + } + if (!mReady) { + // If we don't have `mReady` it means that we are in shutdown mode. + // See bug 1687118 for details. + InitialTranslationCompleted(false); + return; + } + + mInitialTranslationStart = mozilla::TimeStamp::Now(); + + AutoAllowLegacyScriptExecution exemption; + + nsTArray<RefPtr<Promise>> promises; + + ErrorResult rv; + promises.AppendElement(TranslateDocument(rv)); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + InitialTranslationCompleted(false); + mReady->MaybeRejectWithUndefined(); + return; + } + promises.AppendElement(TranslateRoots(rv)); + Element* documentElement = mDocument->GetDocumentElement(); + if (!documentElement) { + InitialTranslationCompleted(false); + mReady->MaybeRejectWithUndefined(); + return; + } + + DOMLocalization::ConnectRoot(*documentElement); + + AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation"); + RefPtr<Promise> promise = Promise::All(aes.cx(), promises, rv); + + if (promise->State() == Promise::PromiseState::Resolved) { + // If the promise is already resolved, we can fast-track + // to initial translation completed. + InitialTranslationCompleted(true); + 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::Constructed, + "This method should be called only from Constructed state."); + RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + 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. + if (proto) { + // 2.1. Handle the case when we have proto. + + // 2.1.1. Move elements that are not in the proto to a separate + // array. + Sequence<OwningNonNull<Element>> nonProtoElements; + + uint32_t i = elements.Length(); + while (i > 0) { + Element* elem = elements.ElementAt(i - 1); + MOZ_RELEASE_ASSERT(elem->HasAttr(nsGkAtoms::datal10nid)); + if (!elem->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) { + if (NS_WARN_IF(!nonProtoElements.AppendElement(*elem, fallible))) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + elements.RemoveElement(elem); + } + i--; + } + + // We populate the sequence in reverse order. Let's bring it + // back to top->bottom one. + nonProtoElements.Reverse(); + + AutoAllowLegacyScriptExecution exemption; + + nsTArray<RefPtr<Promise>> promises; + + // 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, aRv); + if (NS_WARN_IF(!translatePromise || aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + promises.AppendElement(translatePromise); + } + + // 2.1.3. If there are elements that are not in the prototype, + // localize them without attempting to cache and + // independently of if we're loading from cache. + if (!nonProtoElements.IsEmpty()) { + RefPtr<Promise> nonProtoTranslatePromise = + 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, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } 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, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return promise.forget(); +} + +void DocumentL10n::MaybeRecordTelemetry() { + mozilla::TimeStamp initialTranslationEnd = mozilla::TimeStamp::Now(); + + nsAutoString documentURI; + ErrorResult rv; + rv = mDocument->GetDocumentURI(documentURI); + if (rv.Failed()) { + return; + } + + nsCString key; + + if (documentURI.Find(u"chrome://browser/content/browser.xhtml") == 0) { + if (mIsFirstBrowserWindow) { + key = "browser_first_window"; + mIsFirstBrowserWindow = false; + } else { + key = "browser_new_window"; + } + } else if (documentURI.Find(u"about:home") == 0) { + key = "about:home"; + } else if (documentURI.Find(u"about:newtab") == 0) { + key = "about:newtab"; + } else if (documentURI.Find(u"about:preferences") == 0) { + key = "about:preferences"; + } else { + return; + } + + mozilla::TimeDuration totalTime(initialTranslationEnd - + mInitialTranslationStart); + Accumulate(Telemetry::L10N_DOCUMENT_INITIAL_TRANSLATION_TIME_US, key, + totalTime.ToMicroseconds()); +} + +void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) { + if (mState >= DocumentL10nState::Ready) { + return; + } + + Element* documentElement = mDocument->GetDocumentElement(); + if (documentElement) { + SetRootInfo(documentElement); + } + + mState = DocumentL10nState::Ready; + + MaybeRecordTelemetry(); + + RefPtr<Document> doc = mDocument; + doc->InitialTranslationCompleted(aL10nCached); + + // In XUL scenario contentSink is nullptr. + if (mContentSink) { + nsCOMPtr<nsIContentSink> sink = mContentSink.forget(); + sink->InitialTranslationCompleted(); + } + + // From now on, the state of Localization is unconditionally + // async. + SetAsync(); +} + +void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate, + ErrorResult& aRv) { + if (aTranslate) { + if (mState >= DocumentL10nState::InitialTranslationTriggered) { + RefPtr<Promise> promise = TranslateFragment(aNode, aRv); + } + } + DOMLocalization::ConnectRoot(aNode); +} + +Promise* DocumentL10n::Ready() { return mReady; } + +void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); } |