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 "mozilla/dom/AutoEntryScript.h"
10 #include "mozilla/dom/Document.h"
11 #include "mozilla/dom/DocumentL10nBinding.h"
12 #include "mozilla/Telemetry.h"
14 using namespace mozilla::dom
;
16 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n
)
17 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n
, DOMLocalization
)
18 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument
)
19 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady
)
20 NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink
)
21 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
22 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
23 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n
, DOMLocalization
)
24 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument
)
25 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady
)
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink
)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
29 NS_IMPL_ADDREF_INHERITED(DocumentL10n
, DOMLocalization
)
30 NS_IMPL_RELEASE_INHERITED(DocumentL10n
, DOMLocalization
)
32 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n
)
33 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization
)
35 bool DocumentL10n::mIsFirstBrowserWindow
= true;
38 RefPtr
<DocumentL10n
> DocumentL10n::Create(Document
* aDocument
,
40 RefPtr
<DocumentL10n
> l10n
= new DocumentL10n(aDocument
, aSync
);
48 DocumentL10n::DocumentL10n(Document
* aDocument
, const bool aSync
)
49 : DOMLocalization(aDocument
->GetScopeObject(), aSync
, {}),
51 mState(DocumentL10nState::Constructed
) {
52 mContentSink
= do_QueryInterface(aDocument
->GetCurrentContentSink());
55 bool DocumentL10n::Init() {
56 DOMLocalization::Init();
58 mReady
= Promise::Create(mGlobal
, rv
);
59 if (NS_WARN_IF(rv
.Failed())) {
65 JSObject
* DocumentL10n::WrapObject(JSContext
* aCx
,
66 JS::Handle
<JSObject
*> aGivenProto
) {
67 return DocumentL10n_Binding::Wrap(aCx
, this, aGivenProto
);
70 class L10nReadyHandler final
: public PromiseNativeHandler
{
72 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
73 NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler
)
75 explicit L10nReadyHandler(Promise
* aPromise
, DocumentL10n
* aDocumentL10n
)
76 : mPromise(aPromise
), mDocumentL10n(aDocumentL10n
) {}
78 void ResolvedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
79 mDocumentL10n
->InitialTranslationCompleted(true);
80 mPromise
->MaybeResolveWithUndefined();
83 void RejectedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
84 mDocumentL10n
->InitialTranslationCompleted(false);
85 mPromise
->MaybeRejectWithUndefined();
89 ~L10nReadyHandler() = default;
91 RefPtr
<Promise
> mPromise
;
92 RefPtr
<DocumentL10n
> mDocumentL10n
;
95 NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler
, mPromise
, mDocumentL10n
)
97 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler
)
98 NS_INTERFACE_MAP_ENTRY(nsISupports
)
101 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler
)
102 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler
)
104 void DocumentL10n::TriggerInitialTranslation() {
105 if (mState
>= DocumentL10nState::InitialTranslationTriggered
) {
108 mInitialTranslationStart
= mozilla::TimeStamp::NowUnfuzzed();
110 AutoAllowLegacyScriptExecution exemption
;
112 nsTArray
<RefPtr
<Promise
>> promises
;
115 promises
.AppendElement(TranslateDocument(rv
));
116 if (NS_WARN_IF(rv
.Failed())) {
117 InitialTranslationCompleted(false);
118 mReady
->MaybeRejectWithUndefined();
121 promises
.AppendElement(TranslateRoots(rv
));
122 Element
* documentElement
= mDocument
->GetDocumentElement();
123 if (!documentElement
) {
124 InitialTranslationCompleted(false);
125 mReady
->MaybeRejectWithUndefined();
129 DOMLocalization::ConnectRoot(*documentElement
, rv
);
130 if (NS_WARN_IF(rv
.Failed())) {
131 InitialTranslationCompleted(false);
132 mReady
->MaybeRejectWithUndefined();
136 AutoEntryScript
aes(mGlobal
, "DocumentL10n InitialTranslation");
137 RefPtr
<Promise
> promise
= Promise::All(aes
.cx(), promises
, rv
);
139 if (promise
->State() == Promise::PromiseState::Resolved
) {
140 // If the promise is already resolved, we can fast-track
141 // to initial translation completed.
142 InitialTranslationCompleted(true);
143 mReady
->MaybeResolveWithUndefined();
145 RefPtr
<PromiseNativeHandler
> l10nReadyHandler
=
146 new L10nReadyHandler(mReady
, this);
147 promise
->AppendNativeHandler(l10nReadyHandler
);
149 mState
= DocumentL10nState::InitialTranslationTriggered
;
153 already_AddRefed
<Promise
> DocumentL10n::TranslateDocument(ErrorResult
& aRv
) {
154 MOZ_ASSERT(mState
== DocumentL10nState::Constructed
,
155 "This method should be called only from Constructed state.");
156 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
158 Element
* elem
= mDocument
->GetDocumentElement();
160 promise
->MaybeRejectWithUndefined();
161 return promise
.forget();
164 // 1. Collect all localizable elements.
165 Sequence
<OwningNonNull
<Element
>> elements
;
166 GetTranslatables(*elem
, elements
, aRv
);
167 if (NS_WARN_IF(aRv
.Failed())) {
168 promise
->MaybeRejectWithUndefined();
169 return promise
.forget();
172 RefPtr
<nsXULPrototypeDocument
> proto
= mDocument
->GetPrototype();
174 // 2. Check if the document has a prototype that may cache
175 // translated elements.
177 // 2.1. Handle the case when we have proto.
179 // 2.1.1. Move elements that are not in the proto to a separate
181 Sequence
<OwningNonNull
<Element
>> nonProtoElements
;
183 uint32_t i
= elements
.Length();
185 Element
* elem
= elements
.ElementAt(i
- 1);
186 MOZ_RELEASE_ASSERT(elem
->HasAttr(nsGkAtoms::datal10nid
));
187 if (!elem
->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) {
188 if (NS_WARN_IF(!nonProtoElements
.AppendElement(*elem
, fallible
))) {
189 promise
->MaybeRejectWithUndefined();
190 return promise
.forget();
192 elements
.RemoveElement(elem
);
197 // We populate the sequence in reverse order. Let's bring it
198 // back to top->bottom one.
199 nonProtoElements
.Reverse();
201 AutoAllowLegacyScriptExecution exemption
;
203 nsTArray
<RefPtr
<Promise
>> promises
;
205 // 2.1.2. If we're not loading from cache, push the elements that
206 // are in the prototype to be translated and cached.
207 if (!proto
->WasL10nCached() && !elements
.IsEmpty()) {
208 RefPtr
<Promise
> translatePromise
=
209 TranslateElements(elements
, proto
, aRv
);
210 if (NS_WARN_IF(!translatePromise
|| aRv
.Failed())) {
211 promise
->MaybeRejectWithUndefined();
212 return promise
.forget();
214 promises
.AppendElement(translatePromise
);
217 // 2.1.3. If there are elements that are not in the prototype,
218 // localize them without attempting to cache and
219 // independently of if we're loading from cache.
220 if (!nonProtoElements
.IsEmpty()) {
221 RefPtr
<Promise
> nonProtoTranslatePromise
=
222 TranslateElements(nonProtoElements
, nullptr, aRv
);
223 if (NS_WARN_IF(!nonProtoTranslatePromise
|| aRv
.Failed())) {
224 promise
->MaybeRejectWithUndefined();
225 return promise
.forget();
227 promises
.AppendElement(nonProtoTranslatePromise
);
230 // 2.1.4. Collect promises with Promise::All (maybe empty).
231 AutoEntryScript
aes(mGlobal
, "DocumentL10n InitialTranslationCompleted");
232 promise
= Promise::All(aes
.cx(), promises
, aRv
);
234 // 2.2. Handle the case when we don't have proto.
236 // 2.2.1. Otherwise, translate all available elements,
237 // without attempting to cache them.
238 promise
= TranslateElements(elements
, nullptr, aRv
);
241 if (NS_WARN_IF(!promise
|| aRv
.Failed())) {
242 promise
->MaybeRejectWithUndefined();
243 return promise
.forget();
246 return promise
.forget();
249 void DocumentL10n::MaybeRecordTelemetry() {
250 mozilla::TimeStamp initialTranslationEnd
= mozilla::TimeStamp::NowUnfuzzed();
252 nsAutoString documentURI
;
254 rv
= mDocument
->GetDocumentURI(documentURI
);
261 if (documentURI
.Find("chrome://browser/content/browser.xhtml") == 0) {
262 if (mIsFirstBrowserWindow
) {
263 key
= "browser_first_window";
264 mIsFirstBrowserWindow
= false;
266 key
= "browser_new_window";
268 } else if (documentURI
.Find("about:home") == 0) {
270 } else if (documentURI
.Find("about:newtab") == 0) {
271 key
= "about:newtab";
272 } else if (documentURI
.Find("about:preferences") == 0) {
273 key
= "about:preferences";
278 mozilla::TimeDuration
totalTime(initialTranslationEnd
-
279 mInitialTranslationStart
);
280 Accumulate(Telemetry::L10N_DOCUMENT_INITIAL_TRANSLATION_TIME_US
, key
,
281 totalTime
.ToMicroseconds());
284 void DocumentL10n::InitialTranslationCompleted(bool aL10nCached
) {
285 if (mState
>= DocumentL10nState::Ready
) {
289 Element
* documentElement
= mDocument
->GetDocumentElement();
290 if (documentElement
) {
291 SetRootInfo(documentElement
);
294 mState
= DocumentL10nState::Ready
;
296 MaybeRecordTelemetry();
298 mDocument
->InitialTranslationCompleted(aL10nCached
);
300 // In XUL scenario contentSink is nullptr.
302 mContentSink
->InitialTranslationCompleted();
305 // From now on, the state of Localization is unconditionally
310 void DocumentL10n::ConnectRoot(nsINode
& aNode
, bool aTranslate
,
313 if (mState
>= DocumentL10nState::InitialTranslationTriggered
) {
314 RefPtr
<Promise
> promise
= TranslateFragment(aNode
, aRv
);
317 DOMLocalization::ConnectRoot(aNode
, aRv
);
320 Promise
* DocumentL10n::Ready() { return mReady
; }
322 void DocumentL10n::OnCreatePresShell() { mMutations
->OnCreatePresShell(); }