1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
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 "js/ForOfIterator.h" // JS::ForOfIterator
8 #include "js/JSON.h" // JS_ParseJSON
9 #include "nsContentUtils.h"
10 #include "nsIScriptError.h"
11 #include "DOMLocalization.h"
12 #include "mozilla/intl/LocaleService.h"
13 #include "mozilla/dom/AutoEntryScript.h"
14 #include "mozilla/dom/Element.h"
15 #include "mozilla/dom/L10nOverlays.h"
17 using namespace mozilla
;
18 using namespace mozilla::dom
;
19 using namespace mozilla::intl
;
21 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMLocalization
)
22 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMLocalization
, Localization
)
23 tmp
->DisconnectMutations();
24 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMutations
)
25 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoots
)
26 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
27 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMLocalization
, Localization
)
29 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMutations
)
30 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoots
)
31 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
33 NS_IMPL_ADDREF_INHERITED(DOMLocalization
, Localization
)
34 NS_IMPL_RELEASE_INHERITED(DOMLocalization
, Localization
)
35 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMLocalization
)
36 NS_INTERFACE_MAP_END_INHERITING(Localization
)
39 already_AddRefed
<DOMLocalization
> DOMLocalization::Create(
40 nsIGlobalObject
* aGlobal
, const bool aSync
,
41 const BundleGenerator
& aBundleGenerator
) {
42 RefPtr
<DOMLocalization
> domLoc
=
43 new DOMLocalization(aGlobal
, aSync
, aBundleGenerator
);
47 return domLoc
.forget();
50 DOMLocalization::DOMLocalization(nsIGlobalObject
* aGlobal
, const bool aSync
,
51 const BundleGenerator
& aBundleGenerator
)
52 : Localization(aGlobal
, aSync
, aBundleGenerator
) {
53 mMutations
= new L10nMutations(this);
56 already_AddRefed
<DOMLocalization
> DOMLocalization::Constructor(
57 const GlobalObject
& aGlobal
, const Sequence
<nsString
>& aResourceIds
,
58 const bool aSync
, const BundleGenerator
& aBundleGenerator
,
60 nsCOMPtr
<nsIGlobalObject
> global
= do_QueryInterface(aGlobal
.GetAsSupports());
62 aRv
.Throw(NS_ERROR_FAILURE
);
66 RefPtr
<DOMLocalization
> domLoc
=
67 DOMLocalization::Create(global
, aSync
, aBundleGenerator
);
69 if (aResourceIds
.Length()) {
70 domLoc
->AddResourceIds(aResourceIds
);
73 domLoc
->Activate(true);
75 return domLoc
.forget();
78 JSObject
* DOMLocalization::WrapObject(JSContext
* aCx
,
79 JS::Handle
<JSObject
*> aGivenProto
) {
80 return DOMLocalization_Binding::Wrap(aCx
, this, aGivenProto
);
83 void DOMLocalization::Destroy() { DisconnectMutations(); }
85 DOMLocalization::~DOMLocalization() { Destroy(); }
91 void DOMLocalization::ConnectRoot(nsINode
& aNode
, ErrorResult
& aRv
) {
92 nsCOMPtr
<nsIGlobalObject
> global
= aNode
.GetOwnerGlobal();
96 MOZ_ASSERT(global
== mGlobal
,
97 "Cannot add a root that overlaps with existing root.");
100 for (nsINode
* root
: mRoots
) {
102 root
!= &aNode
&& !root
->Contains(&aNode
) && !aNode
.Contains(root
),
103 "Cannot add a root that overlaps with existing root.");
107 mRoots
.Insert(&aNode
);
109 aNode
.AddMutationObserverUnlessExists(mMutations
);
112 void DOMLocalization::DisconnectRoot(nsINode
& aNode
, ErrorResult
& aRv
) {
113 if (mRoots
.Contains(&aNode
)) {
114 aNode
.RemoveMutationObserver(mMutations
);
115 mRoots
.Remove(&aNode
);
119 void DOMLocalization::PauseObserving(ErrorResult
& aRv
) {
120 mMutations
->PauseObserving();
123 void DOMLocalization::ResumeObserving(ErrorResult
& aRv
) {
124 mMutations
->ResumeObserving();
127 void DOMLocalization::SetAttributes(
128 JSContext
* aCx
, Element
& aElement
, const nsAString
& aId
,
129 const Optional
<JS::Handle
<JSObject
*>>& aArgs
, ErrorResult
& aRv
) {
130 if (!aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::datal10nid
, aId
,
132 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nid
, aId
, true);
135 if (aArgs
.WasPassed() && aArgs
.Value()) {
137 JS::Rooted
<JS::Value
> val(aCx
, JS::ObjectValue(*aArgs
.Value()));
138 if (!nsContentUtils::StringifyJSON(aCx
, &val
, data
)) {
139 aRv
.NoteJSContextException(aCx
);
142 if (!aElement
.AttrValueIs(kNameSpaceID_None
, nsGkAtoms::datal10nargs
, data
,
144 aElement
.SetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nargs
, data
, true);
147 aElement
.UnsetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nargs
, true);
151 void DOMLocalization::GetAttributes(Element
& aElement
, L10nIdArgs
& aResult
,
154 nsAutoString l10nArgs
;
156 if (aElement
.GetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nid
, l10nId
)) {
157 CopyUTF16toUTF8(l10nId
, aResult
.mId
);
160 if (aElement
.GetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nargs
, l10nArgs
)) {
161 ConvertStringToL10nArgs(l10nArgs
, aResult
.mArgs
.SetValue(), aRv
);
165 already_AddRefed
<Promise
> DOMLocalization::TranslateFragment(nsINode
& aNode
,
167 Sequence
<OwningNonNull
<Element
>> elements
;
169 GetTranslatables(aNode
, elements
, aRv
);
171 return TranslateElements(elements
, aRv
);
175 * A Promise Handler used to apply the result of
176 * a call to Localization::FormatMessages onto the list
177 * of translatable elements.
179 class ElementTranslationHandler
: public PromiseNativeHandler
{
181 explicit ElementTranslationHandler(DOMLocalization
* aDOMLocalization
,
182 nsXULPrototypeDocument
* aProto
)
183 : mDOMLocalization(aDOMLocalization
), mProto(aProto
){};
185 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
186 NS_DECL_CYCLE_COLLECTION_CLASS(ElementTranslationHandler
)
188 nsTArray
<nsCOMPtr
<Element
>>& Elements() { return mElements
; }
190 void SetReturnValuePromise(Promise
* aReturnValuePromise
) {
191 mReturnValuePromise
= aReturnValuePromise
;
194 virtual void ResolvedCallback(JSContext
* aCx
,
195 JS::Handle
<JS::Value
> aValue
) override
{
198 nsTArray
<Nullable
<L10nMessage
>> l10nData
;
199 if (aValue
.isObject()) {
200 JS::ForOfIterator
iter(aCx
);
201 if (!iter
.init(aValue
, JS::ForOfIterator::AllowNonIterable
)) {
202 mReturnValuePromise
->MaybeRejectWithUndefined();
205 if (!iter
.valueIsIterable()) {
206 mReturnValuePromise
->MaybeRejectWithUndefined();
210 JS::Rooted
<JS::Value
> temp(aCx
);
213 if (!iter
.next(&temp
, &done
)) {
214 mReturnValuePromise
->MaybeRejectWithUndefined();
222 Nullable
<L10nMessage
>* slotPtr
=
223 l10nData
.AppendElement(mozilla::fallible
);
225 mReturnValuePromise
->MaybeRejectWithUndefined();
229 if (!temp
.isNull()) {
230 if (!slotPtr
->SetValue().Init(aCx
, temp
)) {
231 mReturnValuePromise
->MaybeRejectWithUndefined();
239 mDOMLocalization
->ApplyTranslations(mElements
, l10nData
, mProto
, rv
);
240 if (NS_WARN_IF(rv
.Failed()) || !allTranslated
) {
241 mReturnValuePromise
->MaybeRejectWithUndefined();
245 mReturnValuePromise
->MaybeResolveWithUndefined();
248 virtual void RejectedCallback(JSContext
* aCx
,
249 JS::Handle
<JS::Value
> aValue
) override
{
250 mReturnValuePromise
->MaybeRejectWithClone(aCx
, aValue
);
254 ~ElementTranslationHandler() = default;
256 nsTArray
<nsCOMPtr
<Element
>> mElements
;
257 RefPtr
<DOMLocalization
> mDOMLocalization
;
258 RefPtr
<Promise
> mReturnValuePromise
;
259 RefPtr
<nsXULPrototypeDocument
> mProto
;
262 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ElementTranslationHandler
)
263 NS_INTERFACE_MAP_ENTRY(nsISupports
)
266 NS_IMPL_CYCLE_COLLECTION_CLASS(ElementTranslationHandler
)
268 NS_IMPL_CYCLE_COLLECTING_ADDREF(ElementTranslationHandler
)
269 NS_IMPL_CYCLE_COLLECTING_RELEASE(ElementTranslationHandler
)
271 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ElementTranslationHandler
)
272 NS_IMPL_CYCLE_COLLECTION_UNLINK(mElements
)
273 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMLocalization
)
274 NS_IMPL_CYCLE_COLLECTION_UNLINK(mReturnValuePromise
)
275 NS_IMPL_CYCLE_COLLECTION_UNLINK(mProto
)
276 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
278 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ElementTranslationHandler
)
279 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElements
)
280 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMLocalization
)
281 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReturnValuePromise
)
282 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProto
)
283 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
285 already_AddRefed
<Promise
> DOMLocalization::TranslateElements(
286 const Sequence
<OwningNonNull
<Element
>>& aElements
, ErrorResult
& aRv
) {
287 return TranslateElements(aElements
, nullptr, aRv
);
290 already_AddRefed
<Promise
> DOMLocalization::TranslateElements(
291 const Sequence
<OwningNonNull
<Element
>>& aElements
,
292 nsXULPrototypeDocument
* aProto
, ErrorResult
& aRv
) {
293 JS::RootingContext
* rcx
= RootingCx();
294 Sequence
<OwningUTF8StringOrL10nIdArgs
> l10nKeys
;
295 SequenceRooter
<OwningUTF8StringOrL10nIdArgs
> rooter(rcx
, &l10nKeys
);
296 RefPtr
<ElementTranslationHandler
> nativeHandler
=
297 new ElementTranslationHandler(this, aProto
);
298 nsTArray
<nsCOMPtr
<Element
>>& domElements
= nativeHandler
->Elements();
299 domElements
.SetCapacity(aElements
.Length());
305 AutoEntryScript
aes(mGlobal
, "DOMLocalization TranslateElements");
306 JSContext
* cx
= aes
.cx();
308 for (auto& domElement
: aElements
) {
309 if (!domElement
->HasAttr(kNameSpaceID_None
, nsGkAtoms::datal10nid
)) {
313 OwningUTF8StringOrL10nIdArgs
* key
= l10nKeys
.AppendElement(fallible
);
315 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
319 GetAttributes(*domElement
, key
->SetAsL10nIdArgs(), aRv
);
320 if (NS_WARN_IF(aRv
.Failed())) {
324 if (!domElements
.AppendElement(domElement
, fallible
)) {
325 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
330 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
331 if (NS_WARN_IF(aRv
.Failed())) {
336 nsTArray
<Nullable
<L10nMessage
>> l10nMessages
;
338 FormatMessagesSync(cx
, l10nKeys
, l10nMessages
, aRv
);
341 ApplyTranslations(domElements
, l10nMessages
, aProto
, aRv
);
342 if (NS_WARN_IF(aRv
.Failed()) || !allTranslated
) {
343 promise
->MaybeRejectWithUndefined();
344 return MaybeWrapPromise(promise
);
347 promise
->MaybeResolveWithUndefined();
349 RefPtr
<Promise
> callbackResult
= FormatMessages(cx
, l10nKeys
, aRv
);
350 if (NS_WARN_IF(aRv
.Failed())) {
353 nativeHandler
->SetReturnValuePromise(promise
);
354 callbackResult
->AppendNativeHandler(nativeHandler
);
357 return MaybeWrapPromise(promise
);
361 * Promise handler used to set localization data on
362 * roots of elements that got successfully translated.
364 class L10nRootTranslationHandler final
: public PromiseNativeHandler
{
366 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
367 NS_DECL_CYCLE_COLLECTION_CLASS(L10nRootTranslationHandler
)
369 explicit L10nRootTranslationHandler(Element
* aRoot
) : mRoot(aRoot
) {}
371 void ResolvedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
372 DOMLocalization::SetRootInfo(mRoot
);
375 void RejectedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
379 ~L10nRootTranslationHandler() = default;
381 RefPtr
<Element
> mRoot
;
384 NS_IMPL_CYCLE_COLLECTION(L10nRootTranslationHandler
, mRoot
)
386 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nRootTranslationHandler
)
387 NS_INTERFACE_MAP_ENTRY(nsISupports
)
390 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nRootTranslationHandler
)
391 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nRootTranslationHandler
)
393 already_AddRefed
<Promise
> DOMLocalization::TranslateRoots(ErrorResult
& aRv
) {
394 nsTArray
<RefPtr
<Promise
>> promises
;
396 for (nsINode
* root
: mRoots
) {
397 RefPtr
<Promise
> promise
= TranslateFragment(*root
, aRv
);
399 // If the root is an element, we'll add a native handler
400 // to set root info (language, direction etc.) on it
401 // once the localization finishes.
402 if (root
->IsElement()) {
403 RefPtr
<L10nRootTranslationHandler
> nativeHandler
=
404 new L10nRootTranslationHandler(root
->AsElement());
405 promise
->AppendNativeHandler(nativeHandler
);
408 promises
.AppendElement(promise
);
410 AutoEntryScript
aes(mGlobal
, "DOMLocalization TranslateRoots");
411 return Promise::All(aes
.cx(), promises
, aRv
);
419 void DOMLocalization::GetTranslatables(
420 nsINode
& aNode
, Sequence
<OwningNonNull
<Element
>>& aElements
,
423 aNode
.IsContent() ? aNode
.AsContent() : aNode
.GetFirstChild();
424 for (; node
; node
= node
->GetNextNode(&aNode
)) {
425 if (!node
->IsElement()) {
429 Element
* domElement
= node
->AsElement();
431 if (!domElement
->HasAttr(kNameSpaceID_None
, nsGkAtoms::datal10nid
)) {
435 if (!aElements
.AppendElement(*domElement
, fallible
)) {
436 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
443 void DOMLocalization::SetRootInfo(Element
* aElement
) {
444 nsAutoCString primaryLocale
;
445 LocaleService::GetInstance()->GetAppLocaleAsBCP47(primaryLocale
);
446 aElement
->SetAttr(kNameSpaceID_None
, nsGkAtoms::lang
,
447 NS_ConvertUTF8toUTF16(primaryLocale
), true);
450 if (LocaleService::GetInstance()->IsAppLocaleRTL()) {
451 nsGkAtoms::rtl
->ToString(dir
);
453 nsGkAtoms::ltr
->ToString(dir
);
456 uint32_t nameSpace
= aElement
->GetNameSpaceID();
458 nameSpace
== kNameSpaceID_XUL
? nsGkAtoms::localedir
: nsGkAtoms::dir
;
460 aElement
->SetAttr(kNameSpaceID_None
, dirAtom
, dir
, true);
463 bool DOMLocalization::ApplyTranslations(
464 nsTArray
<nsCOMPtr
<Element
>>& aElements
,
465 nsTArray
<Nullable
<L10nMessage
>>& aTranslations
,
466 nsXULPrototypeDocument
* aProto
, ErrorResult
& aRv
) {
467 if (aElements
.Length() != aTranslations
.Length()) {
468 aRv
.Throw(NS_ERROR_FAILURE
);
473 if (NS_WARN_IF(aRv
.Failed())) {
474 aRv
.Throw(NS_ERROR_FAILURE
);
478 bool hasMissingTranslation
= false;
480 nsTArray
<L10nOverlaysError
> errors
;
481 for (size_t i
= 0; i
< aTranslations
.Length(); ++i
) {
482 Element
* elem
= aElements
[i
];
483 if (aTranslations
[i
].IsNull()) {
484 hasMissingTranslation
= true;
487 // If we have a proto, we expect all elements are connected up.
488 // If they're not, they may have been removed by earlier translations.
489 // We will have added an error in L10nOverlays in this case.
490 // This is an error in fluent use, but shouldn't be crashing. There's
491 // also no point translating the element - skip it:
492 if (aProto
&& !elem
->IsInComposedDoc()) {
495 L10nOverlays::TranslateElement(*elem
, aTranslations
[i
].Value(), errors
,
497 if (NS_WARN_IF(aRv
.Failed())) {
498 hasMissingTranslation
= true;
502 // We only need to rebuild deep if the translation has a value.
503 // Otherwise we'll only rebuild the attributes.
504 aProto
->RebuildL10nPrototype(elem
,
505 !aTranslations
[i
].Value().mValue
.IsVoid());
509 ReportL10nOverlaysErrors(errors
);
511 ResumeObserving(aRv
);
512 if (NS_WARN_IF(aRv
.Failed())) {
513 aRv
.Throw(NS_ERROR_FAILURE
);
517 return !hasMissingTranslation
;
522 void DOMLocalization::OnChange() {
523 Localization::OnChange();
524 if (mLocalization
&& !mResourceIds
.IsEmpty()) {
526 RefPtr
<Promise
> promise
= TranslateRoots(rv
);
530 void DOMLocalization::DisconnectMutations() {
532 mMutations
->Disconnect();
537 void DOMLocalization::DisconnectRoots() {
538 for (nsINode
* node
: mRoots
) {
539 node
->RemoveMutationObserver(mMutations
);
544 void DOMLocalization::ReportL10nOverlaysErrors(
545 nsTArray
<L10nOverlaysError
>& aErrors
) {
548 for (auto& error
: aErrors
) {
549 if (error
.mCode
.WasPassed()) {
550 msg
= u
"[fluent-dom] "_ns
;
551 switch (error
.mCode
.Value()) {
552 case L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE
:
553 msg
+= u
"An element of forbidden type \""_ns
+
554 error
.mTranslatedElementName
.Value() +
556 u
"\" was found in the translation. Only safe text-level "
557 "elements and elements with data-l10n-name are allowed.");
559 case L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING
:
560 msg
+= u
"An element named \""_ns
+ error
.mL10nName
.Value() +
561 u
"\" wasn't found in the source."_ns
;
563 case L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH
:
564 msg
+= u
"An element named \""_ns
+ error
.mL10nName
.Value() +
566 u
"\" was found in the translation but its type ") +
567 error
.mTranslatedElementName
.Value() +
569 u
" didn't match the element found in the source ") +
570 error
.mSourceElementName
.Value() + u
"."_ns
;
572 case L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED
:
573 msg
+= u
"The element using message \""_ns
+ error
.mL10nName
.Value() +
575 u
"\" was removed from the DOM when translating its \"") +
576 error
.mTranslatedElementName
.Value() + u
"\" parent."_ns
;
578 case L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM
:
579 msg
+= nsLiteralString(
580 u
"While translating an element with fluent ID \"") +
581 error
.mL10nName
.Value() + u
"\" a child element of type \""_ns
+
582 error
.mTranslatedElementName
.Value() +
584 u
"\" was removed. Either the fluent message "
585 "does not contain markup, or it does not contain markup "
588 case L10nOverlays_Binding::ERROR_UNKNOWN
:
590 msg
+= nsLiteralString(
591 u
"Unknown error happened while translating an element.");
594 nsPIDOMWindowInner
* innerWindow
= GetParentObject()->AsInnerWindow();
595 Document
* doc
= innerWindow
? innerWindow
->GetExtantDoc() : nullptr;
597 nsContentUtils::ReportToConsoleNonLocalized(
598 msg
, nsIScriptError::warningFlag
, "DOM"_ns
, doc
);
600 NS_WARNING("Failed to report l10n DOM Overlay errors to console.");
606 void DOMLocalization::ConvertStringToL10nArgs(const nsString
& aInput
,
607 intl::L10nArgs
& aRetVal
,
609 // This method uses a temporary dictionary to automate
610 // converting a JSON string into an IDL Record via a dictionary.
612 // Once we get Record::Init(const nsAString& aJSON), we'll switch to
614 L10nArgsHelperDict helperDict
;
615 if (!helperDict
.Init(u
"{\"args\": "_ns
+ aInput
+ u
"}"_ns
)) {
616 aRv
.Throw(NS_ERROR_UNEXPECTED
);
619 for (auto& entry
: helperDict
.mArgs
.Entries()) {
620 L10nArgs::EntryType
* newEntry
= aRetVal
.Entries().AppendElement(fallible
);
622 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
625 newEntry
->mKey
= entry
.mKey
;
626 newEntry
->mValue
= entry
.mValue
;