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"
14 using namespace mozilla
;
15 using namespace mozilla::intl
;
16 using namespace mozilla::dom
;
18 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n
)
19 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n
, DOMLocalization
)
20 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument
)
21 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady
)
22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink
)
23 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
24 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
25 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n
, DOMLocalization
)
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument
)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady
)
28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink
)
29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
31 NS_IMPL_ADDREF_INHERITED(DocumentL10n
, DOMLocalization
)
32 NS_IMPL_RELEASE_INHERITED(DocumentL10n
, DOMLocalization
)
34 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n
)
35 NS_INTERFACE_MAP_END_INHERITING(DOMLocalization
)
38 RefPtr
<DocumentL10n
> DocumentL10n::Create(Document
* aDocument
, bool aSync
) {
39 RefPtr
<DocumentL10n
> l10n
= new DocumentL10n(aDocument
, aSync
);
41 IgnoredErrorResult rv
;
42 l10n
->mReady
= Promise::Create(l10n
->mGlobal
, rv
);
43 if (NS_WARN_IF(rv
.Failed())) {
50 DocumentL10n::DocumentL10n(Document
* aDocument
, bool aSync
)
51 : DOMLocalization(aDocument
->GetScopeObject(), aSync
),
53 mState(DocumentL10nState::Constructed
) {
54 mContentSink
= do_QueryInterface(aDocument
->GetCurrentContentSink());
57 JSObject
* DocumentL10n::WrapObject(JSContext
* aCx
,
58 JS::Handle
<JSObject
*> aGivenProto
) {
59 return DocumentL10n_Binding::Wrap(aCx
, this, aGivenProto
);
62 class L10nReadyHandler final
: public PromiseNativeHandler
{
64 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
65 NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler
)
67 explicit L10nReadyHandler(Promise
* aPromise
, DocumentL10n
* aDocumentL10n
)
68 : mPromise(aPromise
), mDocumentL10n(aDocumentL10n
) {}
70 void ResolvedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
,
71 ErrorResult
& aRv
) override
{
72 mDocumentL10n
->InitialTranslationCompleted(true);
73 mPromise
->MaybeResolveWithUndefined();
76 void RejectedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
,
77 ErrorResult
& aRv
) override
{
78 mDocumentL10n
->InitialTranslationCompleted(false);
80 nsTArray
<nsCString
> errors
{
81 "[dom/l10n] Could not complete initial document translation."_ns
,
83 IgnoredErrorResult rv
;
84 MaybeReportErrorsToGecko(errors
, rv
, mDocumentL10n
->GetParentObject());
87 * We resolve the mReady here even if we encountered failures, because
88 * there is nothing actionable for the user pending on `mReady` to do here
89 * and we don't want to incentivized consumers of this API to plan the
90 * same pending operation for resolve and reject scenario.
92 * Additionally, without it, the stderr received "uncaught promise
93 * rejection" warning, which is noisy and not-actionable.
95 * So instead, we just resolve and report errors.
97 * However, in automated tests we do reject so that errors with missing
98 * messages and resources can be caught.
100 if (xpc::IsInAutomation()) {
101 mPromise
->MaybeRejectWithClone(aCx
, aValue
);
103 mPromise
->MaybeResolveWithUndefined();
108 ~L10nReadyHandler() = default;
110 RefPtr
<Promise
> mPromise
;
111 RefPtr
<DocumentL10n
> mDocumentL10n
;
114 NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler
, mPromise
, mDocumentL10n
)
116 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler
)
117 NS_INTERFACE_MAP_ENTRY(nsISupports
)
120 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler
)
121 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler
)
123 void DocumentL10n::TriggerInitialTranslation() {
124 MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
125 if (mState
>= DocumentL10nState::InitialTranslationTriggered
) {
129 // If we don't have `mReady` it means that we are in shutdown mode.
130 // See bug 1687118 for details.
131 InitialTranslationCompleted(false);
135 AutoAllowLegacyScriptExecution exemption
;
137 nsTArray
<RefPtr
<Promise
>> promises
;
140 promises
.AppendElement(TranslateDocument(rv
));
141 if (NS_WARN_IF(rv
.Failed())) {
142 rv
.SuppressException();
143 InitialTranslationCompleted(false);
144 mReady
->MaybeRejectWithUndefined();
147 promises
.AppendElement(TranslateRoots(rv
));
148 Element
* documentElement
= mDocument
->GetDocumentElement();
149 if (!documentElement
) {
150 InitialTranslationCompleted(false);
151 mReady
->MaybeRejectWithUndefined();
155 DOMLocalization::ConnectRoot(*documentElement
);
157 AutoEntryScript
aes(mGlobal
, "DocumentL10n InitialTranslation");
158 RefPtr
<Promise
> promise
= Promise::All(aes
.cx(), promises
, rv
);
160 if (promise
->State() == Promise::PromiseState::Resolved
) {
161 // If the promise is already resolved, we can fast-track
162 // to initial translation completed.
163 InitialTranslationCompleted(true);
164 mReady
->MaybeResolveWithUndefined();
166 RefPtr
<PromiseNativeHandler
> l10nReadyHandler
=
167 new L10nReadyHandler(mReady
, this);
168 promise
->AppendNativeHandler(l10nReadyHandler
);
170 mState
= DocumentL10nState::InitialTranslationTriggered
;
174 already_AddRefed
<Promise
> DocumentL10n::TranslateDocument(ErrorResult
& aRv
) {
175 MOZ_ASSERT(mState
== DocumentL10nState::Constructed
,
176 "This method should be called only from Constructed state.");
177 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
178 if (NS_WARN_IF(aRv
.Failed())) {
182 Element
* elem
= mDocument
->GetDocumentElement();
184 promise
->MaybeRejectWithUndefined();
185 return promise
.forget();
188 // 1. Collect all localizable elements.
189 Sequence
<OwningNonNull
<Element
>> elements
;
190 GetTranslatables(*elem
, elements
, aRv
);
191 if (NS_WARN_IF(aRv
.Failed())) {
192 promise
->MaybeRejectWithUndefined();
193 return promise
.forget();
196 RefPtr
<nsXULPrototypeDocument
> proto
= mDocument
->GetPrototype();
198 // 2. Check if the document has a prototype that may cache
199 // translated elements.
201 // 2.1. Handle the case when we have proto.
203 // 2.1.1. Move elements that are not in the proto to a separate
205 Sequence
<OwningNonNull
<Element
>> nonProtoElements
;
207 uint32_t i
= elements
.Length();
209 Element
* elem
= elements
.ElementAt(i
- 1);
210 MOZ_RELEASE_ASSERT(elem
->HasAttr(nsGkAtoms::datal10nid
));
211 if (!elem
->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) {
212 if (NS_WARN_IF(!nonProtoElements
.AppendElement(*elem
, fallible
))) {
213 promise
->MaybeRejectWithUndefined();
214 return promise
.forget();
216 elements
.RemoveElement(elem
);
221 // We populate the sequence in reverse order. Let's bring it
222 // back to top->bottom one.
223 nonProtoElements
.Reverse();
225 AutoAllowLegacyScriptExecution exemption
;
227 nsTArray
<RefPtr
<Promise
>> promises
;
229 // 2.1.2. If we're not loading from cache, push the elements that
230 // are in the prototype to be translated and cached.
231 if (!proto
->WasL10nCached() && !elements
.IsEmpty()) {
232 RefPtr
<Promise
> translatePromise
=
233 TranslateElements(elements
, proto
, aRv
);
234 if (NS_WARN_IF(!translatePromise
|| aRv
.Failed())) {
235 promise
->MaybeRejectWithUndefined();
236 return promise
.forget();
238 promises
.AppendElement(translatePromise
);
241 // 2.1.3. If there are elements that are not in the prototype,
242 // localize them without attempting to cache and
243 // independently of if we're loading from cache.
244 if (!nonProtoElements
.IsEmpty()) {
245 RefPtr
<Promise
> nonProtoTranslatePromise
=
246 TranslateElements(nonProtoElements
, nullptr, aRv
);
247 if (NS_WARN_IF(!nonProtoTranslatePromise
|| aRv
.Failed())) {
248 promise
->MaybeRejectWithUndefined();
249 return promise
.forget();
251 promises
.AppendElement(nonProtoTranslatePromise
);
254 // 2.1.4. Collect promises with Promise::All (maybe empty).
255 AutoEntryScript
aes(mGlobal
, "DocumentL10n InitialTranslationCompleted");
256 promise
= Promise::All(aes
.cx(), promises
, aRv
);
257 if (NS_WARN_IF(aRv
.Failed())) {
261 // 2.2. Handle the case when we don't have proto.
263 // 2.2.1. Otherwise, translate all available elements,
264 // without attempting to cache them.
265 promise
= TranslateElements(elements
, nullptr, aRv
);
266 if (NS_WARN_IF(aRv
.Failed())) {
271 return promise
.forget();
274 void DocumentL10n::InitialTranslationCompleted(bool aL10nCached
) {
275 if (mState
>= DocumentL10nState::Ready
) {
279 Element
* documentElement
= mDocument
->GetDocumentElement();
280 if (documentElement
) {
281 SetRootInfo(documentElement
);
284 mState
= DocumentL10nState::Ready
;
286 RefPtr
<Document
> doc
= mDocument
;
287 doc
->InitialTranslationCompleted(aL10nCached
);
289 // In XUL scenario contentSink is nullptr.
291 nsCOMPtr
<nsIContentSink
> sink
= mContentSink
.forget();
292 sink
->InitialTranslationCompleted();
295 // From now on, the state of Localization is unconditionally
300 void DocumentL10n::ConnectRoot(nsINode
& aNode
, bool aTranslate
,
303 if (mState
>= DocumentL10nState::InitialTranslationTriggered
) {
304 RefPtr
<Promise
> promise
= TranslateFragment(aNode
, aRv
);
307 DOMLocalization::ConnectRoot(aNode
);
310 Promise
* DocumentL10n::Ready() { return mReady
; }
312 void DocumentL10n::OnCreatePresShell() { mMutations
->OnCreatePresShell(); }