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;
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())) {
53 DocumentL10n::DocumentL10n(Document
* aDocument
, bool aSync
)
54 : DOMLocalization(aDocument
->GetScopeObject(), aSync
),
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
{
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());
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();
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
)
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
) {
125 // If we don't have `mReady` it means that we are in shutdown mode.
126 // See bug 1687118 for details.
127 InitialTranslationCompleted(false);
131 mInitialTranslationStart
= mozilla::TimeStamp::Now();
133 AutoAllowLegacyScriptExecution exemption
;
135 nsTArray
<RefPtr
<Promise
>> promises
;
138 promises
.AppendElement(TranslateDocument(rv
));
139 if (NS_WARN_IF(rv
.Failed())) {
140 InitialTranslationCompleted(false);
141 mReady
->MaybeRejectWithUndefined();
144 promises
.AppendElement(TranslateRoots(rv
));
145 Element
* documentElement
= mDocument
->GetDocumentElement();
146 if (!documentElement
) {
147 InitialTranslationCompleted(false);
148 mReady
->MaybeRejectWithUndefined();
152 DOMLocalization::ConnectRoot(*documentElement
, rv
);
153 if (NS_WARN_IF(rv
.Failed())) {
154 InitialTranslationCompleted(false);
155 mReady
->MaybeRejectWithUndefined();
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();
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();
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.
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
204 Sequence
<OwningNonNull
<Element
>> nonProtoElements
;
206 uint32_t i
= elements
.Length();
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
);
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
);
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
;
277 rv
= mDocument
->GetDocumentURI(documentURI
);
284 if (documentURI
.Find("chrome://browser/content/browser.xhtml") == 0) {
285 if (mIsFirstBrowserWindow
) {
286 key
= "browser_first_window";
287 mIsFirstBrowserWindow
= false;
289 key
= "browser_new_window";
291 } else if (documentURI
.Find("about:home") == 0) {
293 } else if (documentURI
.Find("about:newtab") == 0) {
294 key
= "about:newtab";
295 } else if (documentURI
.Find("about:preferences") == 0) {
296 key
= "about:preferences";
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
) {
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.
325 mContentSink
->InitialTranslationCompleted();
326 mContentSink
= nullptr;
329 // From now on, the state of Localization is unconditionally
334 void DocumentL10n::ConnectRoot(nsINode
& aNode
, bool 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(); }