Bug 1546432 - Migrate DocumentL10n to use dom::l10n::Mutations. r=smaug
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 21 May 2019 19:46:32 +0000
changeset 475235 03ee100c9534af5152231d40b83a6154f9585242
parent 475234 b2413635c1484c3c1ea426020b074161732f8ac6
child 475236 8444c891fa21bf91717021fc2a6c09ca1fa8ef06
push id36058
push useraciure@mozilla.com
push dateFri, 24 May 2019 03:53:25 +0000
treeherdermozilla-central@c87317c41902 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1546432
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1546432 - Migrate DocumentL10n to use dom::l10n::Mutations. r=smaug Differential Revision: https://phabricator.services.mozilla.com/D28981
dom/base/Document.cpp
dom/bindings/Bindings.conf
intl/l10n/DocumentL10n.cpp
intl/l10n/DocumentL10n.h
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -7880,20 +7880,16 @@ void Document::Destroy() {
 
   // Make sure to report before IPC closed.
   if (!nsContentUtils::IsInPrivateBrowsing(this)) {
     mContentBlockingLog.ReportLog();
   }
 
   mIsGoingAway = true;
 
-  if (mDocumentL10n) {
-    mDocumentL10n->Destroy();
-  }
-
   ScriptLoader()->Destroy();
   SetScriptGlobalObject(nullptr);
   RemovedFromDocShell();
 
   bool oldVal = mInUnlinkOrDeletion;
   mInUnlinkOrDeletion = true;
 
 #ifdef DEBUG
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -219,20 +219,16 @@ DOMInterfaces = {
 'DeviceAcceleration': {
     'headerFile': 'mozilla/dom/DeviceMotionEvent.h',
 },
 
 'DeviceRotationRate': {
     'headerFile': 'mozilla/dom/DeviceMotionEvent.h',
 },
 
-'DocumentL10n': {
-    'implicitJSContext': ['translateFragment'],
-},
-
 'DominatorTree': {
     'nativeType': 'mozilla::devtools::DominatorTree'
 },
 
 'DOMException': {
     'binaryNames': {
         'message': 'messageMoz',
     },
--- a/intl/l10n/DocumentL10n.cpp
+++ b/intl/l10n/DocumentL10n.cpp
@@ -3,35 +3,39 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "js/ForOfIterator.h"  // JS::ForOfIterator
 #include "js/JSON.h"           // JS_ParseJSON
 #include "mozilla/dom/DocumentL10n.h"
 #include "mozilla/dom/DocumentL10nBinding.h"
-#include "mozilla/dom/Element.h"
 #include "mozilla/dom/L10nUtilsBinding.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/l10n/DOMOverlays.h"
+#include "mozilla/intl/LocaleService.h"
 #include "nsQueryObject.h"
+#include "nsIScriptError.h"
 #include "nsISupports.h"
 #include "nsImportModule.h"
 #include "nsContentUtils.h"
 
 #define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed"
 
 #define L10N_PSEUDO_PREF "intl.l10n.pseudo"
 
 #define INTL_UI_DIRECTION_PREF "intl.uidirection"
 
 static const char* kObservedPrefs[] = {L10N_PSEUDO_PREF, INTL_UI_DIRECTION_PREF,
                                        nullptr};
 
+using namespace mozilla::intl;
+
 namespace mozilla {
 namespace dom {
 
 NS_INTERFACE_MAP_BEGIN(PromiseResolver)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(PromiseResolver)
@@ -46,40 +50,66 @@ void PromiseResolver::ResolvedCallback(J
 
 void PromiseResolver::RejectedCallback(JSContext* aCx,
                                        JS::Handle<JS::Value> aValue) {
   mPromise->MaybeRejectWithClone(aCx, aValue);
 }
 
 PromiseResolver::~PromiseResolver() { mPromise = nullptr; }
 
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DocumentL10n, mDocument, mDOMLocalization,
-                                      mContentSink, mReady)
+NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DocumentL10n)
+  tmp->DisconnectMutations();
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mMutations)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMLocalization)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DocumentL10n)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMutations)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMLocalization)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(DocumentL10n)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(DocumentL10n)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(DocumentL10n)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
 NS_INTERFACE_MAP_END
 
 DocumentL10n::DocumentL10n(Document* aDocument)
     : mDocument(aDocument), mState(DocumentL10nState::Initialized) {
   mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink());
+  mMutations = new mozilla::dom::l10n::Mutations(this);
 }
 
 DocumentL10n::~DocumentL10n() {
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->RemoveObserver(this, INTL_APP_LOCALES_CHANGED);
   }
 
   Preferences::RemoveObservers(this, kObservedPrefs);
+
+  DisconnectMutations();
+}
+
+void DocumentL10n::DisconnectMutations() {
+  if (mMutations) {
+    mDocument->RemoveMutationObserver(mMutations);
+    mMutations->Disconnect();
+  }
 }
 
 bool DocumentL10n::Init(nsTArray<nsString>& aResourceIds) {
   nsCOMPtr<mozIDOMLocalizationJSM> jsm =
       do_ImportModule("resource://gre/modules/DOMLocalization.jsm");
   MOZ_RELEASE_ASSERT(jsm);
 
   Unused << jsm->GetDOMLocalization(getter_AddRefs(mDOMLocalization));
@@ -123,42 +153,29 @@ void DocumentL10n::RegisterObservers() {
     obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true);
   }
 }
 
 NS_IMETHODIMP
 DocumentL10n::Observe(nsISupports* aSubject, const char* aTopic,
                       const char16_t* aData) {
   if (!strcmp(aTopic, INTL_APP_LOCALES_CHANGED)) {
-    if (mDOMLocalization) {
-      mDOMLocalization->OnChange();
-    }
+    OnChange();
   } else {
     MOZ_ASSERT(!strcmp("nsPref:changed", aTopic));
     nsDependentString pref(aData);
     if (pref.EqualsLiteral(L10N_PSEUDO_PREF) ||
         pref.EqualsLiteral(INTL_UI_DIRECTION_PREF)) {
-      if (mDOMLocalization) {
-        mDOMLocalization->OnChange();
-      }
+      OnChange();
     }
   }
 
   return NS_OK;
 }
 
-void DocumentL10n::Destroy() {
-  if (mDOMLocalization) {
-    Element* elem = mDocument->GetDocumentElement();
-    if (elem) {
-      mDOMLocalization->DisconnectRoot(elem);
-    }
-  }
-}
-
 JSObject* DocumentL10n::WrapObject(JSContext* aCx,
                                    JS::Handle<JSObject*> aGivenProto) {
   return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 already_AddRefed<Promise> DocumentL10n::MaybeWrapPromise(
     Promise* aInnerPromise) {
   // For system principal we don't need to wrap the
@@ -311,33 +328,33 @@ void DocumentL10n::GetAttributes(JSConte
     }
     aResult.mArgs.Construct();
     aResult.mArgs.Value() = &json.toObject();
   }
 }
 
 class LocalizationHandler : public PromiseNativeHandler {
  public:
-  explicit LocalizationHandler(nsINode* aNode) { mNode = aNode; };
+  explicit LocalizationHandler(DocumentL10n* aDocumentL10n) {
+    mDocumentL10n = aDocumentL10n;
+  };
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_CLASS(LocalizationHandler)
 
   nsTArray<nsCOMPtr<Element>>& Elements() { return mElements; }
 
   void SetReturnValuePromise(Promise* aReturnValuePromise) {
     mReturnValuePromise = aReturnValuePromise;
   }
 
   virtual void ResolvedCallback(JSContext* aCx,
                                 JS::Handle<JS::Value> aValue) override {
     ErrorResult rv;
 
-    RefPtr<DocumentL10n> docL10n = mNode->OwnerDoc()->GetL10n();
-
     nsTArray<L10nValue> l10nData;
     if (aValue.isObject()) {
       JS::ForOfIterator iter(aCx);
       if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
         mReturnValuePromise->MaybeRejectWithUndefined();
         return;
       }
       if (!iter.valueIsIterable()) {
@@ -370,189 +387,234 @@ class LocalizationHandler : public Promi
       }
     }
 
     if (mElements.Length() != l10nData.Length()) {
       mReturnValuePromise->MaybeRejectWithUndefined();
       return;
     }
 
-    if (docL10n) {
-      docL10n->PauseObserving(rv);
-      if (NS_WARN_IF(rv.Failed())) {
-        mReturnValuePromise->MaybeRejectWithUndefined();
-        return;
-      }
+    mDocumentL10n->PauseObserving(rv);
+    if (NS_WARN_IF(rv.Failed())) {
+      mReturnValuePromise->MaybeRejectWithUndefined();
+      return;
     }
 
     nsTArray<DOMOverlaysError> errors;
     for (size_t i = 0; i < l10nData.Length(); ++i) {
       Element* elem = mElements[i];
       mozilla::dom::l10n::DOMOverlays::TranslateElement(*elem, l10nData[i],
                                                         errors, rv);
       if (NS_WARN_IF(rv.Failed())) {
         mReturnValuePromise->MaybeRejectWithUndefined();
         return;
       }
     }
 
-    if (docL10n) {
-      docL10n->ResumeObserving(rv);
-      if (NS_WARN_IF(rv.Failed())) {
-        mReturnValuePromise->MaybeRejectWithUndefined();
-        return;
-      }
+    mDocumentL10n->ResumeObserving(rv);
+    if (NS_WARN_IF(rv.Failed())) {
+      mReturnValuePromise->MaybeRejectWithUndefined();
+      return;
     }
 
-    nsTArray<JS::Value> jsErrors;
-    SequenceRooter<JS::Value> rooter(aCx, &jsErrors);
-    for (auto& error : errors) {
-      JS::RootedValue jsError(aCx);
-      if (!ToJSValue(aCx, error, &jsError)) {
-        mReturnValuePromise->MaybeRejectWithUndefined();
-        return;
-      }
-      jsErrors.AppendElement(jsError);
-    }
+    DocumentL10n::ReportDOMOverlaysErrors(mDocumentL10n->GetDocument(), errors);
 
-    mReturnValuePromise->MaybeResolve(jsErrors);
+    mReturnValuePromise->MaybeResolveWithUndefined();
   }
 
   virtual void RejectedCallback(JSContext* aCx,
                                 JS::Handle<JS::Value> aValue) override {
     mReturnValuePromise->MaybeRejectWithClone(aCx, aValue);
   }
 
  private:
   ~LocalizationHandler() = default;
 
   nsTArray<nsCOMPtr<Element>> mElements;
-  RefPtr<nsINode> mNode;
+  RefPtr<DocumentL10n> mDocumentL10n;
   RefPtr<Promise> mReturnValuePromise;
 };
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalizationHandler)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(LocalizationHandler)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalizationHandler)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(LocalizationHandler)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalizationHandler)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mElements)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mNode)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReturnValuePromise)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalizationHandler)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElements)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNode)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReturnValuePromise)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
-already_AddRefed<Promise> DocumentL10n::TranslateFragment(JSContext* aCx,
-                                                          nsINode& aNode,
+already_AddRefed<Promise> DocumentL10n::TranslateFragment(nsINode& aNode,
                                                           ErrorResult& aRv) {
-  Sequence<L10nKey> l10nKeys;
-  SequenceRooter<L10nKey> rooter(aCx, &l10nKeys);
-  RefPtr<LocalizationHandler> nativeHandler = new LocalizationHandler(&aNode);
-  nsTArray<nsCOMPtr<Element>>& domElements = nativeHandler->Elements();
+  Sequence<OwningNonNull<Element>> elements;
+
+  GetTranslatables(aNode, elements, aRv);
+
+  return TranslateElements(elements, aRv);
+}
+
+void DocumentL10n::GetTranslatables(nsINode& aNode,
+                                    Sequence<OwningNonNull<Element>>& aElements,
+                                    ErrorResult& aRv) {
   nsIContent* node =
       aNode.IsContent() ? aNode.AsContent() : aNode.GetFirstChild();
   for (; node; node = node->GetNextNode(&aNode)) {
     if (!node->IsElement()) {
       continue;
     }
 
     Element* domElement = node->AsElement();
 
     if (!domElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) {
       continue;
     }
 
+    if (!aElements.AppendElement(*domElement, fallible)) {
+      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+      return;
+    }
+  }
+}
+
+already_AddRefed<Promise> DocumentL10n::TranslateElements(
+    const Sequence<OwningNonNull<Element>>& aElements, ErrorResult& aRv) {
+  JS::RootingContext* rcx = RootingCx();
+  Sequence<L10nKey> l10nKeys;
+  SequenceRooter<L10nKey> rooter(rcx, &l10nKeys);
+  RefPtr<LocalizationHandler> nativeHandler = new LocalizationHandler(this);
+  nsTArray<nsCOMPtr<Element>>& domElements = nativeHandler->Elements();
+  domElements.SetCapacity(aElements.Length());
+
+  nsIGlobalObject* global = mDocument->GetScopeObject();
+  if (!global) {
+    return nullptr;
+  }
+
+  AutoEntryScript aes(global, "DocumentL10n GetAttributes");
+  JSContext* cx = aes.cx();
+
+  for (auto& domElement : aElements) {
+    if (!domElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) {
+      continue;
+    }
+
     L10nKey* key = l10nKeys.AppendElement(fallible);
     if (!key) {
       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
       return nullptr;
     }
 
-    GetAttributes(aCx, *domElement, *key, aRv);
+    GetAttributes(cx, *domElement, *key, aRv);
     if (aRv.Failed()) {
       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
       return nullptr;
     }
 
     if (!domElements.AppendElement(domElement, fallible)) {
       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
       return nullptr;
     }
   }
 
-  nsIGlobalObject* global = mDocument->GetScopeObject();
-  if (!global) {
-    return nullptr;
-  }
-
   RefPtr<Promise> promise = Promise::Create(global, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  RefPtr<Promise> callbackResult = FormatMessages(aCx, l10nKeys, aRv);
+  RefPtr<Promise> callbackResult = FormatMessages(cx, l10nKeys, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   nativeHandler->SetReturnValuePromise(promise);
   callbackResult->AppendNativeHandler(nativeHandler);
 
   return MaybeWrapPromise(promise);
 }
 
-already_AddRefed<Promise> DocumentL10n::TranslateElements(
-    const Sequence<OwningNonNull<Element>>& aElements, ErrorResult& aRv) {
-  AutoTArray<RefPtr<Element>, 10> elements;
-  elements.SetCapacity(aElements.Length());
-  for (auto& element : aElements) {
-    elements.AppendElement(element);
-  }
-  RefPtr<Promise> promise;
-  aRv = mDOMLocalization->TranslateElements(elements, getter_AddRefs(promise));
-  if (aRv.Failed()) {
-    return nullptr;
-  }
-  return MaybeWrapPromise(promise);
-}
-
 void DocumentL10n::PauseObserving(ErrorResult& aRv) {
-  aRv = mDOMLocalization->PauseObserving();
+  mMutations->PauseObserving();
 }
 
 void DocumentL10n::ResumeObserving(ErrorResult& aRv) {
-  aRv = mDOMLocalization->ResumeObserving();
+  mMutations->ResumeObserving();
+}
+
+/* static */
+void DocumentL10n::ReportDOMOverlaysErrors(
+    Document* aDocument, nsTArray<mozilla::dom::DOMOverlaysError>& aErrors) {
+  nsAutoString msg;
+
+  for (auto& error : aErrors) {
+    if (error.mCode.WasPassed()) {
+      msg = NS_LITERAL_STRING("[fluent-dom] ");
+      switch (error.mCode.Value()) {
+        case DOMOverlays_Binding::ERROR_FORBIDDEN_TYPE:
+          msg += NS_LITERAL_STRING("An element of forbidden type \"") +
+                 error.mTranslatedElementName.Value() +
+                 NS_LITERAL_STRING(
+                     "\" was found in the translation. Only safe text-level "
+                     "elements and elements with data-l10n-name are allowed.");
+          break;
+        case DOMOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING:
+          msg += NS_LITERAL_STRING("An element named \"") +
+                 error.mL10nName.Value() +
+                 NS_LITERAL_STRING("\" wasn't found in the source.");
+          break;
+        case DOMOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH:
+          msg += NS_LITERAL_STRING("An element named \"") +
+                 error.mL10nName.Value() +
+                 NS_LITERAL_STRING(
+                     "\" was found in the translation but its type ") +
+                 error.mTranslatedElementName.Value() +
+                 NS_LITERAL_STRING(
+                     " didn't match the element found in the source ") +
+                 error.mSourceElementName.Value() + NS_LITERAL_STRING(".");
+          break;
+        case DOMOverlays_Binding::ERROR_UNKNOWN:
+        default:
+          msg += NS_LITERAL_STRING(
+              "Unknown error happened while translation of an element.");
+          break;
+      }
+      nsContentUtils::ReportToConsoleNonLocalized(
+          msg, nsIScriptError::warningFlag, NS_LITERAL_CSTRING("DOM"),
+          aDocument);
+    }
+  }
 }
 
 class L10nReadyHandler final : public PromiseNativeHandler {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler)
 
   explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n)
       : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {}
 
   void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
     mDocumentL10n->InitialDocumentTranslationCompleted();
-    mPromise->MaybeResolveWithClone(aCx, aValue);
+    mPromise->MaybeResolveWithUndefined();
   }
 
   void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
     mDocumentL10n->InitialDocumentTranslationCompleted();
-    mPromise->MaybeRejectWithClone(aCx, aValue);
+    mPromise->MaybeRejectWithUndefined();
   }
 
  private:
   ~L10nReadyHandler() = default;
 
   RefPtr<Promise> mPromise;
   RefPtr<DocumentL10n> mDocumentL10n;
 };
@@ -569,43 +631,81 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nRea
 void DocumentL10n::TriggerInitialDocumentTranslation() {
   if (mState >= DocumentL10nState::InitialTranslationTriggered) {
     return;
   }
 
   mState = DocumentL10nState::InitialTranslationTriggered;
 
   Element* elem = mDocument->GetDocumentElement();
-  if (elem) {
-    mDOMLocalization->ConnectRoot(elem);
+  if (!elem) {
+    return;
   }
 
-  RefPtr<Promise> promise;
-  mDOMLocalization->TranslateRoots(getter_AddRefs(promise));
+  Sequence<OwningNonNull<Element>> elements;
+  ErrorResult rv;
+
+  GetTranslatables(*elem, elements, rv);
+
+  mMutations->ConnectRoot(elem);
+
+  RefPtr<Promise> promise = TranslateElements(elements, rv);
   if (!promise) {
     return;
   }
 
   RefPtr<PromiseNativeHandler> l10nReadyHandler =
       new L10nReadyHandler(mReady, this);
   promise->AppendNativeHandler(l10nReadyHandler);
 }
 
 void DocumentL10n::InitialDocumentTranslationCompleted() {
   if (mState >= DocumentL10nState::InitialTranslationCompleted) {
     return;
   }
 
+  Element* documentElement = mDocument->GetDocumentElement();
+  if (documentElement) {
+    SetRootInfo(documentElement);
+  }
+
   mState = DocumentL10nState::InitialTranslationCompleted;
 
   mDocument->InitialDocumentTranslationCompleted();
 
   // In XUL scenario mContentSink is nullptr.
   if (mContentSink) {
     mContentSink->InitialDocumentTranslationCompleted();
     mContentSink = nullptr;
   }
+  mDocument->AddMutationObserverUnlessExists(mMutations);
 }
 
 Promise* DocumentL10n::Ready() { return mReady; }
 
+void DocumentL10n::OnChange() {
+  if (mDOMLocalization) {
+    mDOMLocalization->OnChange();
+  }
+}
+
+void DocumentL10n::SetRootInfo(Element* aElement) {
+  nsAutoCString primaryLocale;
+  LocaleService::GetInstance()->GetAppLocaleAsBCP47(primaryLocale);
+  aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::lang,
+                    NS_ConvertUTF8toUTF16(primaryLocale), true);
+
+  nsAutoString dir;
+  if (LocaleService::GetInstance()->IsAppLocaleRTL()) {
+    nsGkAtoms::rtl->ToString(dir);
+  } else {
+    nsGkAtoms::ltr->ToString(dir);
+  }
+
+  uint32_t nameSpace = aElement->GetNameSpaceID();
+  nsAtom* dirAtom =
+      nameSpace == kNameSpaceID_XUL ? nsGkAtoms::localedir : nsGkAtoms::dir;
+
+  aElement->SetAttr(kNameSpaceID_None, dirAtom, dir, true);
+}
+
 }  // namespace dom
 }  // namespace mozilla
--- a/intl/l10n/DocumentL10n.h
+++ b/intl/l10n/DocumentL10n.h
@@ -14,24 +14,28 @@
 #include "nsIObserver.h"
 #include "nsWrapperCache.h"
 #include "nsWeakReference.h"
 #include "js/TypeDecls.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/l10n/Mutations.h"
+#include "mozilla/dom/DOMOverlaysBinding.h"
 
 namespace mozilla {
 namespace dom {
 
-class Document;
-class Element;
+namespace l10n {
+class Mutations;
+}
 struct L10nKey;
 
 class PromiseResolver final : public PromiseNativeHandler {
  public:
   NS_DECL_ISUPPORTS
 
   explicit PromiseResolver(Promise* aPromise);
   void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override;
@@ -66,29 +70,30 @@ class DocumentL10n final : public nsIObs
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(DocumentL10n,
                                                          nsIObserver)
   NS_DECL_NSIOBSERVER
 
  public:
   explicit DocumentL10n(Document* aDocument);
   bool Init(nsTArray<nsString>& aResourceIds);
-  void Destroy();
 
  protected:
   virtual ~DocumentL10n();
 
   RefPtr<Document> mDocument;
   RefPtr<Promise> mReady;
   DocumentL10nState mState;
   nsCOMPtr<mozIDOMLocalization> mDOMLocalization;
   nsCOMPtr<nsIContentSink> mContentSink;
+  RefPtr<l10n::Mutations> mMutations;
 
   already_AddRefed<Promise> MaybeWrapPromise(Promise* aPromise);
   void RegisterObservers();
+  void DisconnectMutations();
 
  public:
   Document* GetParentObject() const { return mDocument; };
 
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
   /**
@@ -116,29 +121,38 @@ class DocumentL10n final : public nsIObs
       const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
 
   void SetAttributes(JSContext* aCx, Element& aElement, const nsAString& aId,
                      const Optional<JS::Handle<JSObject*>>& aArgs,
                      ErrorResult& aRv);
   void GetAttributes(JSContext* aCx, Element& aElement, L10nKey& aResult,
                      ErrorResult& aRv);
 
-  already_AddRefed<Promise> TranslateFragment(JSContext* aCx, nsINode& aNode,
-                                              ErrorResult& aRv);
+  already_AddRefed<Promise> TranslateFragment(nsINode& aNode, ErrorResult& aRv);
+
+  void GetTranslatables(nsINode& aNode,
+                        Sequence<OwningNonNull<Element>>& aElements,
+                        ErrorResult& aRv);
+
   already_AddRefed<Promise> TranslateElements(
       const Sequence<OwningNonNull<Element>>& aElements, ErrorResult& aRv);
 
   void PauseObserving(ErrorResult& aRv);
   void ResumeObserving(ErrorResult& aRv);
+  static void ReportDOMOverlaysErrors(
+      Document* aDocument, nsTArray<mozilla::dom::DOMOverlaysError>& aErrors);
 
   Promise* Ready();
 
   void TriggerInitialDocumentTranslation();
 
   void InitialDocumentTranslationCompleted();
 
   Document* GetDocument() { return mDocument; };
+
+  void OnChange();
+ protected:
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_DocumentL10n_h