Bug 1769628 [wpt PR 34081] - Update wpt metadata, a=testonly
[gecko.git] / dom / l10n / DocumentL10n.cpp
blobbb8629ae335066685b517025000a726d22c5e4f4
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "DocumentL10n.h"
8 #include "nsIContentSink.h"
9 #include "nsContentUtils.h"
10 #include "mozilla/dom/AutoEntryScript.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/dom/DocumentL10nBinding.h"
13 #include "mozilla/Telemetry.h"
15 using namespace mozilla;
16 using namespace mozilla::intl;
17 using namespace mozilla::dom;
19 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n)
20 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n, DOMLocalization)
21 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady)
23 NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink)
24 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
25 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n, DOMLocalization)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady)
29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink)
30 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
32 NS_IMPL_ADDREF_INHERITED(DocumentL10n, DOMLocalization)
33 NS_IMPL_RELEASE_INHERITED(DocumentL10n, DOMLocalization)
35 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
36 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization)
38 bool DocumentL10n::mIsFirstBrowserWindow = true;
40 /* static */
41 RefPtr<DocumentL10n> DocumentL10n::Create(Document* aDocument, bool aSync) {
42 RefPtr<DocumentL10n> l10n = new DocumentL10n(aDocument, aSync);
44 IgnoredErrorResult rv;
45 l10n->mReady = Promise::Create(l10n->mGlobal, rv);
46 if (NS_WARN_IF(rv.Failed())) {
47 return nullptr;
50 return l10n.forget();
53 DocumentL10n::DocumentL10n(Document* aDocument, bool aSync)
54 : DOMLocalization(aDocument->GetScopeObject(), aSync),
55 mDocument(aDocument),
56 mState(DocumentL10nState::Constructed) {
57 mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink());
60 JSObject* DocumentL10n::WrapObject(JSContext* aCx,
61 JS::Handle<JSObject*> aGivenProto) {
62 return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto);
65 class L10nReadyHandler final : public PromiseNativeHandler {
66 public:
67 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
68 NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler)
70 explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n)
71 : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {}
73 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
74 ErrorResult& aRv) override {
75 mDocumentL10n->InitialTranslationCompleted(true);
76 mPromise->MaybeResolveWithUndefined();
79 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
80 ErrorResult& aRv) override {
81 mDocumentL10n->InitialTranslationCompleted(false);
83 nsTArray<nsCString> errors{
84 "[dom/l10n] Could not complete initial document translation."_ns,
86 IgnoredErrorResult rv;
87 MaybeReportErrorsToGecko(errors, rv, mDocumentL10n->GetParentObject());
89 /**
90 * We resolve the mReady here even if we encountered failures, because
91 * there is nothing actionable for the user pending on `mReady` to do here
92 * and we don't want to incentivized consumers of this API to plan the
93 * same pending operation for resolve and reject scenario.
95 * Additionally, without it, the stderr received "uncaught promise
96 * rejection" warning, which is noisy and not-actionable.
98 * So instead, we just resolve and report errors.
100 mPromise->MaybeResolveWithUndefined();
103 private:
104 ~L10nReadyHandler() = default;
106 RefPtr<Promise> mPromise;
107 RefPtr<DocumentL10n> mDocumentL10n;
110 NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise, mDocumentL10n)
112 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler)
113 NS_INTERFACE_MAP_ENTRY(nsISupports)
114 NS_INTERFACE_MAP_END
116 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler)
117 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler)
119 void DocumentL10n::TriggerInitialTranslation() {
120 MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
121 if (mState >= DocumentL10nState::InitialTranslationTriggered) {
122 return;
124 if (!mReady) {
125 // If we don't have `mReady` it means that we are in shutdown mode.
126 // See bug 1687118 for details.
127 InitialTranslationCompleted(false);
128 return;
131 mInitialTranslationStart = mozilla::TimeStamp::Now();
133 AutoAllowLegacyScriptExecution exemption;
135 nsTArray<RefPtr<Promise>> promises;
137 ErrorResult rv;
138 promises.AppendElement(TranslateDocument(rv));
139 if (NS_WARN_IF(rv.Failed())) {
140 InitialTranslationCompleted(false);
141 mReady->MaybeRejectWithUndefined();
142 return;
144 promises.AppendElement(TranslateRoots(rv));
145 Element* documentElement = mDocument->GetDocumentElement();
146 if (!documentElement) {
147 InitialTranslationCompleted(false);
148 mReady->MaybeRejectWithUndefined();
149 return;
152 DOMLocalization::ConnectRoot(*documentElement, rv);
153 if (NS_WARN_IF(rv.Failed())) {
154 InitialTranslationCompleted(false);
155 mReady->MaybeRejectWithUndefined();
156 return;
159 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation");
160 RefPtr<Promise> promise = Promise::All(aes.cx(), promises, rv);
162 if (promise->State() == Promise::PromiseState::Resolved) {
163 // If the promise is already resolved, we can fast-track
164 // to initial translation completed.
165 InitialTranslationCompleted(true);
166 mReady->MaybeResolveWithUndefined();
167 } else {
168 RefPtr<PromiseNativeHandler> l10nReadyHandler =
169 new L10nReadyHandler(mReady, this);
170 promise->AppendNativeHandler(l10nReadyHandler);
172 mState = DocumentL10nState::InitialTranslationTriggered;
176 already_AddRefed<Promise> DocumentL10n::TranslateDocument(ErrorResult& aRv) {
177 MOZ_ASSERT(mState == DocumentL10nState::Constructed,
178 "This method should be called only from Constructed state.");
179 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
181 Element* elem = mDocument->GetDocumentElement();
182 if (!elem) {
183 promise->MaybeRejectWithUndefined();
184 return promise.forget();
187 // 1. Collect all localizable elements.
188 Sequence<OwningNonNull<Element>> elements;
189 GetTranslatables(*elem, elements, aRv);
190 if (NS_WARN_IF(aRv.Failed())) {
191 promise->MaybeRejectWithUndefined();
192 return promise.forget();
195 RefPtr<nsXULPrototypeDocument> proto = mDocument->GetPrototype();
197 // 2. Check if the document has a prototype that may cache
198 // translated elements.
199 if (proto) {
200 // 2.1. Handle the case when we have proto.
202 // 2.1.1. Move elements that are not in the proto to a separate
203 // array.
204 Sequence<OwningNonNull<Element>> nonProtoElements;
206 uint32_t i = elements.Length();
207 while (i > 0) {
208 Element* elem = elements.ElementAt(i - 1);
209 MOZ_RELEASE_ASSERT(elem->HasAttr(nsGkAtoms::datal10nid));
210 if (!elem->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) {
211 if (NS_WARN_IF(!nonProtoElements.AppendElement(*elem, fallible))) {
212 promise->MaybeRejectWithUndefined();
213 return promise.forget();
215 elements.RemoveElement(elem);
217 i--;
220 // We populate the sequence in reverse order. Let's bring it
221 // back to top->bottom one.
222 nonProtoElements.Reverse();
224 AutoAllowLegacyScriptExecution exemption;
226 nsTArray<RefPtr<Promise>> promises;
228 // 2.1.2. If we're not loading from cache, push the elements that
229 // are in the prototype to be translated and cached.
230 if (!proto->WasL10nCached() && !elements.IsEmpty()) {
231 RefPtr<Promise> translatePromise =
232 TranslateElements(elements, proto, aRv);
233 if (NS_WARN_IF(!translatePromise || aRv.Failed())) {
234 promise->MaybeRejectWithUndefined();
235 return promise.forget();
237 promises.AppendElement(translatePromise);
240 // 2.1.3. If there are elements that are not in the prototype,
241 // localize them without attempting to cache and
242 // independently of if we're loading from cache.
243 if (!nonProtoElements.IsEmpty()) {
244 RefPtr<Promise> nonProtoTranslatePromise =
245 TranslateElements(nonProtoElements, nullptr, aRv);
246 if (NS_WARN_IF(!nonProtoTranslatePromise || aRv.Failed())) {
247 promise->MaybeRejectWithUndefined();
248 return promise.forget();
250 promises.AppendElement(nonProtoTranslatePromise);
253 // 2.1.4. Collect promises with Promise::All (maybe empty).
254 AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslationCompleted");
255 promise = Promise::All(aes.cx(), promises, aRv);
256 } else {
257 // 2.2. Handle the case when we don't have proto.
259 // 2.2.1. Otherwise, translate all available elements,
260 // without attempting to cache them.
261 promise = TranslateElements(elements, nullptr, aRv);
264 if (NS_WARN_IF(!promise || aRv.Failed())) {
265 promise->MaybeRejectWithUndefined();
266 return promise.forget();
269 return promise.forget();
272 void DocumentL10n::MaybeRecordTelemetry() {
273 mozilla::TimeStamp initialTranslationEnd = mozilla::TimeStamp::Now();
275 nsAutoString documentURI;
276 ErrorResult rv;
277 rv = mDocument->GetDocumentURI(documentURI);
278 if (rv.Failed()) {
279 return;
282 nsCString key;
284 if (documentURI.Find("chrome://browser/content/browser.xhtml") == 0) {
285 if (mIsFirstBrowserWindow) {
286 key = "browser_first_window";
287 mIsFirstBrowserWindow = false;
288 } else {
289 key = "browser_new_window";
291 } else if (documentURI.Find("about:home") == 0) {
292 key = "about:home";
293 } else if (documentURI.Find("about:newtab") == 0) {
294 key = "about:newtab";
295 } else if (documentURI.Find("about:preferences") == 0) {
296 key = "about:preferences";
297 } else {
298 return;
301 mozilla::TimeDuration totalTime(initialTranslationEnd -
302 mInitialTranslationStart);
303 Accumulate(Telemetry::L10N_DOCUMENT_INITIAL_TRANSLATION_TIME_US, key,
304 totalTime.ToMicroseconds());
307 void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) {
308 if (mState >= DocumentL10nState::Ready) {
309 return;
312 Element* documentElement = mDocument->GetDocumentElement();
313 if (documentElement) {
314 SetRootInfo(documentElement);
317 mState = DocumentL10nState::Ready;
319 MaybeRecordTelemetry();
321 mDocument->InitialTranslationCompleted(aL10nCached);
323 // In XUL scenario contentSink is nullptr.
324 if (mContentSink) {
325 mContentSink->InitialTranslationCompleted();
326 mContentSink = nullptr;
329 // From now on, the state of Localization is unconditionally
330 // async.
331 SetAsync();
334 void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate,
335 ErrorResult& aRv) {
336 if (aTranslate) {
337 if (mState >= DocumentL10nState::InitialTranslationTriggered) {
338 RefPtr<Promise> promise = TranslateFragment(aNode, aRv);
341 DOMLocalization::ConnectRoot(aNode, aRv);
344 Promise* DocumentL10n::Ready() { return mReady; }
346 void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); }