Bug 1503657 - Implement Fluent DOMOverlays in C++. r=smaug,Pike
authorZibi Braniecki <zbraniecki@mozilla.com>
Wed, 24 Apr 2019 05:05:11 +0000
changeset 470594 cae9cd8bbdeacc0ac21a2c2bf2ade0db9bcf6a66
parent 470593 8607ae75fc828066fb97c078281013b3e5d9507d
child 470595 87829648b0e550022c77399a239c3d016d448aad
push id35909
push usernerli@mozilla.com
push dateWed, 24 Apr 2019 09:53:59 +0000
treeherdermozilla-central@87829648b0e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, Pike
bugs1503657
milestone68.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 1503657 - Implement Fluent DOMOverlays in C++. r=smaug,Pike Differential Revision: https://phabricator.services.mozilla.com/D27200
dom/bindings/Bindings.conf
dom/chrome-webidl/DOMOverlays.webidl
dom/chrome-webidl/moz.build
dom/html/HTMLTemplateElement.h
dom/l10n/DOMOverlays.cpp
dom/l10n/DOMOverlays.h
dom/l10n/components.conf
dom/l10n/moz.build
dom/l10n/tests/gtest/TestDOMOverlays.cpp
dom/l10n/tests/gtest/moz.build
dom/l10n/tests/mochitest/test_domoverlays.xul
dom/l10n/tests/mochitest/test_domoverlays_attributes.html
dom/l10n/tests/mochitest/test_domoverlays_extra_text_markup.html
dom/l10n/tests/mochitest/test_domoverlays_functional_children.html
dom/l10n/tests/mochitest/test_domoverlays_text_children.html
intl/l10n/DOMLocalization.jsm
xpcom/ds/StaticAtoms.py
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -234,16 +234,20 @@ DOMInterfaces = {
     },
     'implicitJSContext': [ 'filename', 'lineNumber', 'stack' ],
 },
 
 'DOMMatrixReadOnly': {
     'headerFile': 'mozilla/dom/DOMMatrix.h',
 },
 
+'DOMOverlays': {
+    'nativeType': 'mozilla::dom::l10n::DOMOverlays',
+},
+
 'DOMPointReadOnly': {
     'headerFile': 'mozilla/dom/DOMPoint.h',
 },
 
 'DOMRectList': {
     'headerFile': 'mozilla/dom/DOMRect.h',
 },
 
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/DOMOverlays.webidl
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+dictionary DOMOverlaysError {
+  short code;
+  DOMString translatedElementName;
+  DOMString sourceElementName;
+  DOMString l10nName;
+};
+
+[ChromeOnly]
+namespace DOMOverlays {
+  const unsigned short ERROR_FORBIDDEN_TYPE = 1;
+  const unsigned short ERROR_NAMED_ELEMENT_MISSING = 2;
+  const unsigned short ERROR_NAMED_ELEMENT_TYPE_MISMATCH = 3;
+  const unsigned short ERROR_UNKNOWN = 4;
+
+  sequence<DOMOverlaysError>? translateElement(Element element, optional L10nValue translation);
+};
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -31,16 +31,17 @@ with Files("WebExtension*.webidl"):
 PREPROCESSED_WEBIDL_FILES = [
     'ChromeUtils.webidl',
 ]
 
 WEBIDL_FILES = [
     'BrowsingContext.webidl',
     'ChannelWrapper.webidl',
     'DominatorTree.webidl',
+    'DOMOverlays.webidl',
     'Flex.webidl',
     'HeapSnapshot.webidl',
     'InspectorUtils.webidl',
     'IteratorResult.webidl',
     'JSWindowActor.webidl',
     'MatchGlob.webidl',
     'MatchPattern.webidl',
     'MessageManager.webidl',
--- a/dom/html/HTMLTemplateElement.h
+++ b/dom/html/HTMLTemplateElement.h
@@ -17,16 +17,18 @@ namespace dom {
 class HTMLTemplateElement final : public nsGenericHTMLElement {
  public:
   explicit HTMLTemplateElement(
       already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
 
+  NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTemplateElement, _template);
+
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTemplateElement,
                                            nsGenericHTMLElement)
 
   virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
 
   DocumentFragment* Content() { return mContent; }
 
  protected:
new file mode 100644
--- /dev/null
+++ b/dom/l10n/DOMOverlays.cpp
@@ -0,0 +1,483 @@
+#include "DOMOverlays.h"
+#include "mozilla/dom/HTMLTemplateElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "nsHtml5StringParser.h"
+#include "nsTextNode.h"
+
+using namespace mozilla::dom::l10n;
+using namespace mozilla::dom;
+using namespace mozilla;
+
+bool DOMOverlays::IsAttrNameLocalizable(
+    const nsAtom* nameAtom, Element* aElement,
+    nsTArray<nsString>* aExplicitlyAllowed) {
+  nsAutoString name;
+  nameAtom->ToString(name);
+
+  if (aExplicitlyAllowed->Contains(name)) {
+    return true;
+  }
+
+  nsAtom* elemName = aElement->NodeInfo()->NameAtom();
+
+  uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
+
+  if (nameSpace == kNameSpaceID_XHTML) {
+    // Is it a globally safe attribute?
+    if (nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::aria_label ||
+        nameAtom == nsGkAtoms::aria_valuetext) {
+      return true;
+    }
+
+    // Is it allowed on this element?
+    if (elemName == nsGkAtoms::a) {
+      return nameAtom == nsGkAtoms::download;
+    }
+    if (elemName == nsGkAtoms::area) {
+      return nameAtom == nsGkAtoms::download || nameAtom == nsGkAtoms::alt;
+    }
+    if (elemName == nsGkAtoms::input) {
+      // Special case for value on HTML inputs with type button, reset, submit
+      if (nameAtom == nsGkAtoms::value) {
+        HTMLInputElement* input = HTMLInputElement::FromNode(aElement);
+        if (input) {
+          uint32_t type = input->ControlType();
+          if (type == NS_FORM_INPUT_SUBMIT || type == NS_FORM_INPUT_BUTTON ||
+              type == NS_FORM_INPUT_RESET) {
+            return true;
+          }
+        }
+      }
+      return nameAtom == nsGkAtoms::alt || nameAtom == nsGkAtoms::placeholder;
+    }
+    if (elemName == nsGkAtoms::menuitem) {
+      return nameAtom == nsGkAtoms::label;
+    }
+    if (elemName == nsGkAtoms::menu) {
+      return nameAtom == nsGkAtoms::label;
+    }
+    if (elemName == nsGkAtoms::optgroup) {
+      return nameAtom == nsGkAtoms::label;
+    }
+    if (elemName == nsGkAtoms::option) {
+      return nameAtom == nsGkAtoms::label;
+    }
+    if (elemName == nsGkAtoms::track) {
+      return nameAtom == nsGkAtoms::label;
+    }
+    if (elemName == nsGkAtoms::img) {
+      return nameAtom == nsGkAtoms::alt;
+    }
+    if (elemName == nsGkAtoms::textarea) {
+      return nameAtom == nsGkAtoms::placeholder;
+    }
+    if (elemName == nsGkAtoms::th) {
+      return nameAtom == nsGkAtoms::abbr;
+    }
+
+  } else if (nameSpace == kNameSpaceID_XUL) {
+    // Is it a globally safe attribute?
+    if (nameAtom == nsGkAtoms::accesskey || nameAtom == nsGkAtoms::aria_label ||
+        nameAtom == nsGkAtoms::aria_valuetext || nameAtom == nsGkAtoms::label ||
+        nameAtom == nsGkAtoms::title || nameAtom == nsGkAtoms::tooltiptext) {
+      return true;
+    }
+
+    // Is it allowed on this element?
+    if (elemName == nsGkAtoms::description) {
+      return nameAtom == nsGkAtoms::value;
+    }
+    if (elemName == nsGkAtoms::key) {
+      return nameAtom == nsGkAtoms::key || nameAtom == nsGkAtoms::keycode;
+    }
+    if (elemName == nsGkAtoms::label) {
+      return nameAtom == nsGkAtoms::value;
+    }
+    if (elemName == nsGkAtoms::textbox) {
+      return nameAtom == nsGkAtoms::placeholder || nameAtom == nsGkAtoms::value;
+    }
+  }
+
+  return false;
+}
+
+already_AddRefed<nsINode> DOMOverlays::CreateTextNodeFromTextContent(
+    Element* aElement, ErrorResult& aRv) {
+  nsAutoString content;
+  aElement->GetTextContent(content, aRv);
+
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
+  return aElement->OwnerDoc()->CreateTextNode(content);
+}
+
+class AttributeNameValueComparator {
+ public:
+  bool Equals(const AttributeNameValue& aAttribute,
+              const nsAttrName* aAttrName) const {
+    return aAttrName->Equals(aAttribute.mName);
+  }
+};
+
+void DOMOverlays::OverlayAttributes(
+    const Nullable<Sequence<AttributeNameValue>>& aTranslation,
+    Element* aToElement, ErrorResult& aRv) {
+  nsTArray<nsString> explicitlyAllowed;
+
+  nsAutoString l10nAttrs;
+  aToElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nattrs, l10nAttrs);
+
+  HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ',');
+  while (tokenizer.hasMoreTokens()) {
+    const nsAString& token = tokenizer.nextToken();
+    if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) {
+      explicitlyAllowed.AppendElement(token);
+    }
+  }
+
+  uint32_t i = aToElement->GetAttrCount();
+  while (i > 0) {
+    const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1);
+
+    if (IsAttrNameLocalizable(attrName->LocalName(), aToElement,
+                              &explicitlyAllowed) &&
+        (aTranslation.IsNull() ||
+         !aTranslation.Value().Contains(attrName,
+                                        AttributeNameValueComparator()))) {
+      nsAutoString name;
+      attrName->LocalName()->ToString(name);
+      aToElement->RemoveAttribute(name, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+    }
+    i--;
+  }
+
+  if (aTranslation.IsNull()) {
+    return;
+  }
+
+  for (auto& attribute : aTranslation.Value()) {
+    nsString attrName = attribute.mName;
+    RefPtr<nsAtom> nameAtom = NS_Atomize(attrName);
+    if (IsAttrNameLocalizable(nameAtom, aToElement, &explicitlyAllowed)) {
+      nsString value = attribute.mValue;
+      if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value,
+                                   eCaseMatters)) {
+        aToElement->SetAttr(nameAtom, value, aRv);
+        if (NS_WARN_IF(aRv.Failed())) {
+          return;
+        }
+      }
+    }
+  }
+}
+
+void DOMOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
+                                    ErrorResult& aRv) {
+  Nullable<Sequence<AttributeNameValue>> attributes;
+  uint32_t attrCount = aFromElement->GetAttrCount();
+
+  if (attrCount == 0) {
+    attributes.SetNull();
+  } else {
+    Sequence<AttributeNameValue> sequence;
+
+    uint32_t i = 0;
+    while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) {
+      AttributeNameValue* attr = sequence.AppendElement(fallible);
+      MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None),
+                 "No namespaced attributes allowed.");
+      info.mName->LocalName()->ToString(attr->mName);
+      info.mValue->ToString(attr->mValue);
+    }
+
+    attributes.SetValue(sequence);
+  }
+
+  return OverlayAttributes(attributes, aToElement, aRv);
+}
+
+void DOMOverlays::ShallowPopulateUsing(Element* aFromElement,
+                                       Element* aToElement, ErrorResult& aRv) {
+  nsAutoString content;
+  aFromElement->GetTextContent(content, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+
+  aToElement->SetTextContent(content, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+
+  OverlayAttributes(aFromElement, aToElement, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+}
+
+already_AddRefed<nsINode> DOMOverlays::GetNodeForNamedElement(
+    Element* aSourceElement, Element* aTranslatedChild,
+    nsTArray<DOMOverlaysError>& aErrors, ErrorResult& aRv) {
+  nsAutoString childName;
+  aTranslatedChild->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nname,
+                            childName);
+  RefPtr<Element> sourceChild = nullptr;
+
+  nsINodeList* childNodes = aSourceElement->ChildNodes();
+  for (uint32_t i = 0; i < childNodes->Length(); i++) {
+    nsINode* childNode = childNodes->Item(i);
+
+    if (!childNode->IsElement()) {
+      continue;
+    }
+    Element* childElement = childNode->AsElement();
+
+    if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname,
+                                  childName, eCaseMatters)) {
+      sourceChild = childElement;
+      break;
+    }
+  }
+
+  if (!sourceChild) {
+    DOMOverlaysError error;
+    error.mCode.Construct(DOMOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING);
+    error.mL10nName.Construct(childName);
+    aErrors.AppendElement(error);
+    return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
+  }
+
+  nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom();
+  nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom();
+  if (sourceChildName != translatedChildName &&
+      // Create a specific exception for img vs. image mismatches,
+      // see bug 1543493
+      !(translatedChildName == nsGkAtoms::img &&
+        sourceChildName == nsGkAtoms::image)) {
+    DOMOverlaysError error;
+    error.mCode.Construct(
+        DOMOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH);
+    error.mL10nName.Construct(childName);
+    error.mTranslatedElementName.Construct(
+        aTranslatedChild->NodeInfo()->LocalName());
+    error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName());
+    aErrors.AppendElement(error);
+    return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
+  }
+
+  aSourceElement->RemoveChild(*sourceChild, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  RefPtr<nsINode> clone = sourceChild->CloneNode(false, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  return clone.forget();
+}
+
+bool DOMOverlays::IsElementAllowed(Element* aElement) {
+  uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
+  if (nameSpace != kNameSpaceID_XHTML) {
+    return false;
+  }
+
+  nsAtom* nameAtom = aElement->NodeInfo()->NameAtom();
+
+  return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong ||
+         nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s ||
+         nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q ||
+         nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr ||
+         nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time ||
+         nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var ||
+         nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd ||
+         nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup ||
+         nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b ||
+         nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark ||
+         nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo ||
+         nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br ||
+         nameAtom == nsGkAtoms::wbr;
+}
+
+already_AddRefed<Element> DOMOverlays::CreateSanitizedElement(
+    Element* aElement, ErrorResult& aRv) {
+  // Start with an empty element of the same type to remove nested children
+  // and non-localizable attributes defined by the translation.
+
+  ElementCreationOptionsOrString options;
+  RefPtr<Element> clone = aElement->OwnerDoc()->CreateElement(
+      aElement->NodeInfo()->LocalName(), options, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
+  ShallowPopulateUsing(aElement, clone, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  return clone.forget();
+}
+
+void DOMOverlays::OverlayChildNodes(DocumentFragment* aFromFragment,
+                                    Element* aToElement,
+                                    nsTArray<DOMOverlaysError>& aErrors,
+                                    ErrorResult& aRv) {
+  nsINodeList* childNodes = aFromFragment->ChildNodes();
+  for (uint32_t i = 0; i < childNodes->Length(); i++) {
+    nsINode* childNode = childNodes->Item(i);
+
+    if (!childNode->IsElement()) {
+      // Keep the translated text node.
+      continue;
+    }
+
+    RefPtr<Element> childElement = childNode->AsElement();
+
+    if (childElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nname)) {
+      RefPtr<nsINode> sanitized =
+          GetNodeForNamedElement(aToElement, childElement, aErrors, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+      aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+      continue;
+    }
+
+    if (IsElementAllowed(childElement)) {
+      RefPtr<Element> sanitized = CreateSanitizedElement(childElement, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+      aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+      continue;
+    }
+
+    DOMOverlaysError error;
+    error.mCode.Construct(DOMOverlays_Binding::ERROR_FORBIDDEN_TYPE);
+    error.mTranslatedElementName.Construct(
+        childElement->NodeInfo()->LocalName());
+    aErrors.AppendElement(error);
+
+    // If all else fails, replace the element with its text content.
+    RefPtr<nsINode> textNode = CreateTextNodeFromTextContent(childElement, aRv);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return;
+    }
+
+    aFromFragment->ReplaceChild(*textNode, *childNode, aRv);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return;
+    }
+  }
+
+  while (aToElement->HasChildren()) {
+    aToElement->RemoveChildNode(aToElement->GetLastChild(), true);
+  }
+  aToElement->AppendChild(*aFromFragment, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+}
+
+void DOMOverlays::TranslateElement(
+    const GlobalObject& aGlobal, Element& aElement,
+    const L10nValue& aTranslation,
+    Nullable<nsTArray<DOMOverlaysError>>& aErrors) {
+  nsTArray<DOMOverlaysError> errors;
+
+  ErrorResult rv;
+
+  TranslateElement(aElement, aTranslation, errors, rv);
+  if (NS_WARN_IF(rv.Failed())) {
+    DOMOverlaysError error;
+    error.mCode.Construct(DOMOverlays_Binding::ERROR_UNKNOWN);
+    errors.AppendElement(error);
+  }
+  if (!errors.IsEmpty()) {
+    aErrors.SetValue(errors);
+  }
+}
+
+bool DOMOverlays::ContainsMarkup(const nsAString& aStr) {
+  // We use our custom ContainsMarkup rather than the
+  // one from FragmentOrElement.cpp, because we don't
+  // want to trigger HTML parsing on every `Preferences & Options`
+  // type of string.
+  const char16_t* start = aStr.BeginReading();
+  const char16_t* end = aStr.EndReading();
+
+  while (start != end) {
+    char16_t c = *start;
+    if (c == char16_t('<')) {
+      return true;
+    }
+    ++start;
+
+    if (c == char16_t('&') && start != end) {
+      c = *start;
+      if (c == char16_t('#') || (c >= char16_t('0') && c <= char16_t('9')) ||
+          (c >= char16_t('a') && c <= char16_t('z')) ||
+          (c >= char16_t('A') && c <= char16_t('Z'))) {
+        return true;
+      }
+      ++start;
+    }
+  }
+
+  return false;
+}
+
+void DOMOverlays::TranslateElement(Element& aElement,
+                                   const L10nValue& aTranslation,
+                                   nsTArray<DOMOverlaysError>& aErrors,
+                                   ErrorResult& aRv) {
+  if (!aTranslation.mValue.IsVoid()) {
+    if (!ContainsMarkup(aTranslation.mValue)) {
+      // If the translation doesn't contain any markup skip the overlay logic.
+      aElement.SetTextContent(aTranslation.mValue, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+    } else {
+      // Else parse the translation's HTML into a DocumentFragment,
+      // sanitize it and replace the element's content.
+      RefPtr<DocumentFragment> fragment =
+          new DocumentFragment(aElement.OwnerDoc()->NodeInfoManager());
+      nsContentUtils::ParseFragmentHTML(
+          aTranslation.mValue, fragment, nsGkAtoms::_template,
+          kNameSpaceID_XHTML, false, true);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+
+      OverlayChildNodes(fragment, &aElement, aErrors, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
+    }
+  }
+
+  // Even if the translation doesn't define any localizable attributes, run
+  // overlayAttributes to remove any localizable attributes set by previous
+  // translations.
+  OverlayAttributes(aTranslation.mAttributes, &aElement, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/l10n/DOMOverlays.h
@@ -0,0 +1,118 @@
+#ifndef mozilla_dom_l10n_DOMOverlays_h__
+#define mozilla_dom_l10n_DOMOverlays_h__
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/L10nUtilsBinding.h"
+#include "mozilla/dom/DOMOverlaysBinding.h"
+
+namespace mozilla {
+namespace dom {
+namespace l10n {
+
+class DOMOverlays {
+ public:
+  /**
+   * Translate an element.
+   *
+   * Translate the element's text content and attributes. Some HTML markup is
+   * allowed in the translation. The element's children with the data-l10n-name
+   * attribute will be treated as arguments to the translation. If the
+   * translation defines the same children, their attributes and text contents
+   * will be used for translating the matching source child.
+   */
+  static void TranslateElement(
+      const GlobalObject& aGlobal, Element& aElement,
+      const L10nValue& aTranslation,
+      Nullable<nsTArray<mozilla::dom::DOMOverlaysError>>& aErrors);
+  static void TranslateElement(
+      Element& aElement, const L10nValue& aTranslation,
+      nsTArray<mozilla::dom::DOMOverlaysError>& aErrors, ErrorResult& aRv);
+
+ private:
+  /**
+   * Check if attribute is allowed for the given element.
+   *
+   * This method is used by the sanitizer when the translation markup contains
+   * DOM attributes, or when the translation has traits which map to DOM
+   * attributes.
+   *
+   * `aExplicitlyAllowed` can be passed as a list of attributes explicitly
+   * allowed on this element.
+   */
+  static bool IsAttrNameLocalizable(const nsAtom* nameAtom, Element* aElement,
+                                    nsTArray<nsString>* aExplicitlyAllowed);
+
+  /**
+   * Create a text node from text content of an Element.
+   */
+  static already_AddRefed<nsINode> CreateTextNodeFromTextContent(
+      Element* aElement, ErrorResult& aRv);
+
+  /**
+   * Transplant localizable attributes of an element to another element.
+   *
+   * Any localizable attributes already set on the target element will be
+   * cleared.
+   */
+  static void OverlayAttributes(
+      const Nullable<Sequence<AttributeNameValue>>& aTranslation,
+      Element* aToElement, ErrorResult& aRv);
+  static void OverlayAttributes(Element* aFromElement, Element* aToElement,
+                                ErrorResult& aRv);
+
+  /**
+   * Helper to set textContent and localizable attributes on an element.
+   */
+  static void ShallowPopulateUsing(Element* aFromElement, Element* aToElement,
+                                   ErrorResult& aRv);
+
+  /**
+   * Sanitize a child element created by the translation.
+   *
+   * Try to find a corresponding child in sourceElement and use it as the base
+   * for the sanitization. This will preserve functional attributes defined on
+   * the child element in the source HTML.
+   */
+  static already_AddRefed<nsINode> GetNodeForNamedElement(
+      Element* aSourceElement, Element* aTranslatedChild,
+      nsTArray<DOMOverlaysError>& aErrors, ErrorResult& aRv);
+
+  /**
+   * Check if element is allowed in the translation.
+   *
+   * This method is used by the sanitizer when the translation markup contains
+   * an element which is not present in the source code.
+   */
+  static bool IsElementAllowed(Element* aElement);
+
+  /**
+   * Sanitize an allowed element.
+   *
+   * Text-level elements allowed in translations may only use safe attributes
+   * and will have any nested markup stripped to text content.
+   */
+  static already_AddRefed<Element> CreateSanitizedElement(Element* aElement,
+                                                          ErrorResult& aRv);
+
+  /**
+   * Replace child nodes of an element with child nodes of another element.
+   *
+   * The contents of the target element will be cleared and fully replaced with
+   * sanitized contents of the source element.
+   */
+  static void OverlayChildNodes(DocumentFragment* aFromFragment,
+                                Element* aToElement,
+                                nsTArray<DOMOverlaysError>& aErrors,
+                                ErrorResult& aRv);
+
+  /**
+   * A helper used to test if the string contains HTML markup.
+   */
+  static bool ContainsMarkup(const nsAString& aStr);
+};
+
+}  // namespace l10n
+}  // namespace dom
+}  // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/l10n/components.conf
@@ -0,0 +1,8 @@
+Classes = [
+    {
+        'cid': '{8d85597c-3a92-11e9-9ffc-73d225b2d53f}',
+        'contract_ids': ['@mozilla.org/dom/l10n/domoverlays;1'],
+        'type': 'mozilla::dom::l10n::DOMOverlays',
+        'headers': ['/dom/l10n/DOMOverlays.h'],
+    },
+]
--- a/dom/l10n/moz.build
+++ b/dom/l10n/moz.build
@@ -2,9 +2,26 @@
 # vim: set filetype=python:
 # 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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "Internationalization")
 
+EXPORTS.mozilla.dom.l10n += [
+    'DOMOverlays.h',
+]
+
+UNIFIED_SOURCES += [
+    'DOMOverlays.cpp',
+]
+
+LOCAL_INCLUDES += [
+    '/dom/base',
+]
+
+FINAL_LIBRARY = 'xul'
+
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
+
+if CONFIG['ENABLE_TESTS']:
+    DIRS += ['tests/gtest']
new file mode 100644
--- /dev/null
+++ b/dom/l10n/tests/gtest/TestDOMOverlays.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "gtest/gtest.h"
+#include "mozilla/dom/l10n/DOMOverlays.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DOMOverlaysBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/L10nUtilsBinding.h"
+#include "mozilla/NullPrincipal.h"
+#include "nsNetUtil.h"
+
+using mozilla::NullPrincipal;
+using namespace mozilla::dom;
+using namespace mozilla::dom::l10n;
+
+already_AddRefed<Document> SetUpDocument() {
+  nsCOMPtr<nsIURI> uri;
+  NS_NewURI(getter_AddRefs(uri), "about:blank");
+  nsCOMPtr<nsIPrincipal> principal =
+      NullPrincipal::CreateWithoutOriginAttributes();
+  nsCOMPtr<Document> document;
+  nsresult rv = NS_NewDOMDocument(getter_AddRefs(document),
+                                  EmptyString(),  // aNamespaceURI
+                                  EmptyString(),  // aQualifiedName
+                                  nullptr,        // aDoctype
+                                  uri, uri, principal,
+                                  false,    // aLoadedAsData
+                                  nullptr,  // aEventObject
+                                  DocumentFlavorHTML);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return nullptr;
+  }
+  return document.forget();
+}
+
+/**
+ * This test verifies that the basic C++ DOMOverlays API
+ * works correctly.
+ */
+TEST(DOM_L10n_DOMOverlays, Initial)
+{
+  mozilla::ErrorResult rv;
+
+  // 1. Set up an HTML document.
+  nsCOMPtr<Document> doc = SetUpDocument();
+
+  // 2. Create a simple Element with a child.
+  //
+  //   <div>
+  //     <a data-l10n-name="link" href="https://www.mozilla.org"></a>
+  //   </div>
+  //
+  RefPtr<Element> elem = doc->CreateHTMLElement(nsGkAtoms::div);
+  RefPtr<Element> span = doc->CreateHTMLElement(nsGkAtoms::a);
+  span->SetAttribute(NS_LITERAL_STRING("data-l10n-name"),
+                     NS_LITERAL_STRING("link"), rv);
+  span->SetAttribute(NS_LITERAL_STRING("href"),
+                     NS_LITERAL_STRING("https://www.mozilla.org"), rv);
+  elem->AppendChild(*span, rv);
+
+  // 3. Create an L10nValue with a translation for the element.
+  L10nValue translation;
+  translation.mValue.AssignLiteral(
+      "Hello <a data-l10n-name=\"link\">World</a>.");
+
+  // 4. Translate the element.
+  nsTArray<DOMOverlaysError> errors;
+  DOMOverlays::TranslateElement(*elem, translation, errors, rv);
+
+  nsAutoString textContent;
+  elem->GetInnerHTML(textContent, rv);
+
+  // 5. Verify that the innerHTML matches the expectations.
+  ASSERT_STREQ(NS_ConvertUTF16toUTF8(textContent).get(),
+               "Hello <a data-l10n-name=\"link\" "
+               "href=\"https://www.mozilla.org\">World</a>.");
+}
new file mode 100644
--- /dev/null
+++ b/dom/l10n/tests/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+    'TestDOMOverlays.cpp',
+]
+
+FINAL_LIBRARY = 'xul-gtest'
--- a/dom/l10n/tests/mochitest/test_domoverlays.xul
+++ b/dom/l10n/tests/mochitest/test_domoverlays.xul
@@ -11,28 +11,28 @@
   <linkset>
     <html:link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
   </linkset>
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
   <script type="application/javascript">
   <![CDATA[
-  const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
+  /* global DOMOverlays */
 
   function elem(name) {
     return function(str) {
       const element = document.createElement(name);
       // eslint-disable-next-line no-unsanitized/property
       element.innerHTML = str;
       return element;
     };
   }
 
-  const { translateElement } = DOMLocalization.DOMOverlays;
+  const { translateElement } = DOMOverlays;
 
   SimpleTest.waitForExplicitFinish();
 
   {
     // Allowed attribute
     const element = elem("description")``;
     const translation = {
       value: null,
--- a/dom/l10n/tests/mochitest/test_domoverlays_attributes.html
+++ b/dom/l10n/tests/mochitest/test_domoverlays_attributes.html
@@ -1,30 +1,29 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMOverlays Top-level attributes</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
+  /* global DOMOverlays */
   "use strict";
 
-  const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
-
   function elem(name) {
     return function(str) {
       const element = document.createElement(name);
       // eslint-disable-next-line no-unsanitized/property
       element.innerHTML = str;
       return element;
     };
   }
 
-  const { translateElement } = DOMLocalization.DOMOverlays;
+  const { translateElement } = DOMOverlays;
 
   {
     // Allowed attribute
     const element = elem("div")``;
     const translation = {
       value: null,
       attributes: [
         {name: "title", value: "FOO"},
--- a/dom/l10n/tests/mochitest/test_domoverlays_extra_text_markup.html
+++ b/dom/l10n/tests/mochitest/test_domoverlays_extra_text_markup.html
@@ -1,30 +1,29 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMOverlays Localized text markup</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
+  /* global DOMOverlays */
   "use strict";
 
-  const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
-
   function elem(name) {
     return function(str) {
       const element = document.createElement(name);
       // eslint-disable-next-line no-unsanitized/property
       element.innerHTML = str;
       return element;
     };
   }
 
-  const { translateElement } = DOMLocalization.DOMOverlays;
+  const { translateElement } = DOMOverlays;
 
   // Localized text markup
   {
     // allowed element
     const element = elem("div")`Foo`;
     const translation = {
       value: "FOO <em>BAR</em> BAZ",
       attributes: null,
--- a/dom/l10n/tests/mochitest/test_domoverlays_functional_children.html
+++ b/dom/l10n/tests/mochitest/test_domoverlays_functional_children.html
@@ -1,29 +1,29 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMOverlays functional children test</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
+  /* global DOMOverlays */
   "use strict";
-  const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
 
   function elem(name) {
     return function(str) {
       const element = document.createElement(name);
       // eslint-disable-next-line no-unsanitized/property
       element.innerHTML = str;
       return element;
     };
   }
 
-  const { translateElement } = DOMLocalization.DOMOverlays;
+  const { translateElement } = DOMOverlays;
 
   // Child without name
   {
     // in source
     const element = elem("div")`
       <a>Foo</a>`;
     const translation = {
       value: "FOO",
--- a/dom/l10n/tests/mochitest/test_domoverlays_text_children.html
+++ b/dom/l10n/tests/mochitest/test_domoverlays_text_children.html
@@ -1,29 +1,29 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test DOMOverlays Text-semantic argument elements</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
+  /* global DOMOverlays */
   "use strict";
-  const {DOMLocalization} = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm");
 
   function elem(name) {
     return function(str) {
       const element = document.createElement(name);
       // eslint-disable-next-line no-unsanitized/property
       element.innerHTML = str;
       return element;
     };
   }
 
-  const { translateElement } = DOMLocalization.DOMOverlays;
+  const { translateElement } = DOMOverlays;
 
   {
     // without data-l10n-name
     const element = elem("div")`
       <em class="bar"></em>`;
     const translation = {
       value: '<em title="FOO">FOO</em>',
       attributes: null,
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -849,17 +849,15 @@ class DOMLocalization extends Localizati
   getKeysForElement(element) {
     return {
       id: element.getAttribute(L10NID_ATTR_NAME),
       args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null),
     };
   }
 }
 
-DOMLocalization.DOMOverlays = { translateElement };
-
 /**
  * Helper function which allows us to construct a new
  * DOMLocalization from DocumentL10n.
  */
 var getDOMLocalization = () => new DOMLocalization();
 
 var EXPORTED_SYMBOLS = ["DOMLocalization", "getDOMLocalization"];
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -284,16 +284,17 @@ STATIC_ATOMS = [
     Atom("cutoutregion", "cutoutregion"),
     Atom("cycler", "cycler"),
     Atom("dashed", "dashed"),
     Atom("data", "data"),
     Atom("datalist", "datalist"),
     Atom("datal10nid", "data-l10n-id"),
     Atom("datal10nargs", "data-l10n-args"),
     Atom("datal10nattrs", "data-l10n-attrs"),
+    Atom("datal10nname", "data-l10n-name"),
     Atom("dataType", "data-type"),
     Atom("dateTime", "date-time"),
     Atom("date", "date"),
     Atom("datetime", "datetime"),
     Atom("dd", "dd"),
     Atom("decimal", "decimal"),
     Atom("decimalFormat", "decimal-format"),
     Atom("decimalSeparator", "decimal-separator"),