Backed out 3 changesets (bug 1453480, bug 1453878) for c2 failures at intl/l10n/test/dom/test_domloc_overlay.htm and browser-chrome failures at browser/components/preferences/in-content/tests/browser_fluent.js on a CLOSED TREE
authorCoroiu Cristina <ccoroiu@mozilla.com>
Fri, 13 Apr 2018 08:51:13 +0300
changeset 469196 de2f038f711e379b9ac4b7eb85fb5387b532ffc7
parent 469195 48d1719348251e2406619adee9dc4bee5bcd9db8
child 469197 ad1a87f7ffa532f18f6ced9f93a2f51c790f1b35
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1453480, 1453878
milestone61.0a1
backs out8dd86546cc66f05745a460dc2766fcea2b756238
6b5e7c13eb8c7c77dec98f0eec17b4a26fdbf060
385de3e4dca0432925d639d3eaa22d4b883c86f8
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
Backed out 3 changesets (bug 1453480, bug 1453878) for c2 failures at intl/l10n/test/dom/test_domloc_overlay.htm and browser-chrome failures at browser/components/preferences/in-content/tests/browser_fluent.js on a CLOSED TREE Backed out changeset 8dd86546cc66 (bug 1453878) Backed out changeset 6b5e7c13eb8c (bug 1453480) Backed out changeset 385de3e4dca0 (bug 1453480)
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/components/preferences/in-content/privacy.xul
browser/components/preferences/in-content/searchResults.xul
browser/components/preferences/in-content/tests/browser_fluent.js
browser/locales/en-US/browser/preferences/preferences.ftl
intl/l10n/DOMLocalization.jsm
intl/l10n/Localization.jsm
intl/l10n/MessageContext.jsm
intl/l10n/test/dom/test_domloc_overlay.html
intl/l10n/test/dom/test_domloc_overlay_missing_children.html
intl/l10n/test/dom/test_domloc_overlay_repeated.html
intl/l10n/test/dom/test_domloc_repeated_l10nid.html
python/l10n/fluent_migrations/bug_1453480_preferences_dom2_resources.py
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -453,17 +453,17 @@ var gMainPane = {
     let archResource = Services.appinfo.is64Bit
       ? "aboutDialog.architecture.sixtyFourBit"
       : "aboutDialog.architecture.thirtyTwoBit";
     let arch = bundle.GetStringFromName(archResource);
     version += ` (${arch})`;
 
     document.l10n.setAttributes(
       document.getElementById("updateAppInfo"),
-      "update-application-version",
+      "update-application-info",
       { version }
     );
 
     // Show a release notes link if we have a URL.
     let relNotesLink = document.getElementById("releasenotes");
     let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL");
     if (relNotesPrefType != Services.prefs.PREF_INVALID) {
       let relNotesURL = Services.urlFormatter.formatURLPref("app.releaseNotesURL");
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -437,17 +437,17 @@
 <!-- Update -->
 <groupbox id="updateApp" data-category="paneGeneral" hidden="true">
   <caption class="search-header" hidden="true"><label data-l10n-id="update-application-title"/></caption>
 
   <label data-l10n-id="update-application-description"/>
   <hbox align="center">
     <vbox flex="1">
       <description id="updateAppInfo">
-        <html:a id="releasenotes" data-l10n-name="learn-more" class="learnMore text-link" hidden="true"/>
+        <html:a id="releasenotes" class="learnMore text-link" hidden="true"/>
       </description>
       <description id="distribution" class="text-blurb" hidden="true"/>
       <description id="distributionId" class="text-blurb" hidden="true"/>
     </vbox>
 #ifdef MOZ_UPDATER
     <spacer flex="1"/>
     <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
     <vbox>
@@ -610,18 +610,18 @@
           <menuitem label="4" value="4"/>
           <menuitem label="5" value="5"/>
           <menuitem label="6" value="6"/>
           <menuitem label="7" value="7"/>
         </menupopup>
       </menulist>
     </hbox>
     <description id="contentProcessCountEnabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-enabled-desc"/>
-    <description id="contentProcessCountDisabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-blocked-desc">
-      <html:a class="text-link" data-l10n-name="learn-more" href="https://wiki.mozilla.org/Electrolysis"/>
+    <description id="contentProcessCountDisabledDescription" class="tip-caption" data-l10n-id="performance-limit-content-process-disabled-desc">
+      <html:a class="text-link" href="https://wiki.mozilla.org/Electrolysis"/>
     </description>
   </vbox>
 </groupbox>
 
 <hbox id="browsingCategory"
       class="subcategory"
       hidden="true"
       data-category="paneGeneral">
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -276,18 +276,18 @@
 </groupbox>
 
 <!-- Tracking -->
 <groupbox id="trackingGroup" data-category="panePrivacy" hidden="true">
   <caption><label data-l10n-id="tracking-header"/></caption>
   <vbox>
     <hbox align="start">
       <vbox flex="1">
-        <description data-l10n-id="tracking-desc">
-          <a id="trackingProtectionLearnMore" data-l10n-name="learn-more" target="_blank" class="learnMore text-link"/>
+        <description data-l10n-id="tracking-description">
+          <a id="trackingProtectionLearnMore" target="_blank" class="learnMore text-link"/>
         </description>
       </vbox>
       <spacer flex="1"/>
     </hbox>
     <hbox>
       <vbox id="trackingProtectionBox" flex="1" hidden="true">
         <vbox>
           <hbox id="trackingProtectionExtensionContentLabel" align="center" hidden="true">
--- a/browser/components/preferences/in-content/searchResults.xul
+++ b/browser/components/preferences/in-content/searchResults.xul
@@ -10,19 +10,19 @@
   <label class="header-name" flex="1" data-l10n-id="search-results-header" />
 </hbox>
 
 <groupbox id="no-results-message"
           data-hidden-from-search="true"
           data-category="paneSearchResults"
           hidden="true">
   <vbox class="no-results-container">
-    <label id="sorry-message" data-l10n-id="search-results-empty-message">
-      <html:span data-l10n-name="query" id="sorry-message-query"/>
+    <label id="sorry-message" data-l10n-id="search-results-sorry-message">
+      <html:span id="sorry-message-query"/>
     </label>
-    <label id="need-help" data-l10n-id="search-results-help-link">
-      <a class="text-link" data-l10n-name="url" target="_blank"></a>
+    <label id="need-help" data-l10n-id="search-results-need-help">
+      <a class="text-link" target="_blank"></a>
     </label>
   </vbox>
   <vbox class="no-results-container" align="center">
     <image></image>
   </vbox>
 </groupbox>
--- a/browser/components/preferences/in-content/tests/browser_fluent.js
+++ b/browser/components/preferences/in-content/tests/browser_fluent.js
@@ -34,15 +34,15 @@ add_task(async function() {
     ["performance-default-content-process-count", { num: defaultProcessCount }]
   ]);
 
   let elem = doc.querySelector(
     `#contentProcessCount > menupopup > menuitem[value="${defaultProcessCount}"]`);
 
   Assert.deepEqual(msg, {
     value: null,
-    attributes: [
+    attrs: [
       {name: "label", value: elem.getAttribute("label")}
     ]
   });
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -75,24 +75,24 @@ should-restart-title = Restart { -brand-
 should-restart-ok = Restart { -brand-short-name } now
 cancel-no-restart-button = Cancel
 restart-later = Restart Later
 
 ## Preferences UI Search Results
 
 search-results-header = Search Results
 
-# `<span data-l10n-name="query"></span>` will be replaced by the search term.
-search-results-empty-message =
+# `<span></span>` will be replaced by the search term.
+search-results-sorry-message =
     { PLATFORM() ->
-        [windows] Sorry! There are no results in Options for “<span data-l10n-name="query"></span>”.
-       *[other] Sorry! There are no results in Preferences for “<span data-l10n-name="query"></span>”.
+        [windows] Sorry! There are no results in Options for “<span></span>”.
+       *[other] Sorry! There are no results in Preferences for “<span></span>”.
     }
 
-search-results-help-link = Need help? Visit <a data-l10n-name="url">{ -brand-short-name } Support</a>
+search-results-need-help = Need help? Visit <a>{ -brand-short-name } Support</a>
 
 ## General Section
 
 startup-header = Startup
 
 # { -brand-short-name } will be 'Firefox Developer Edition',
 # since this setting is only exposed in Firefox Developer Edition
 separate-profile-mode =
@@ -262,17 +262,17 @@ play-drm-content =
     .accesskey = P
 
 play-drm-content-learn-more = Learn more
 
 update-application-title = { -brand-short-name } Updates
 
 update-application-description = Keep { -brand-short-name } up to date for the best performance, stability, and security.
 
-update-application-version = Version { $version } <a data-l10n-name="learn-more">What’s new</a>
+update-application-info = Version { $version } <a>What's new</a>
 
 update-history =
     .label = Show Update History…
     .accesskey = p
 
 update-application-allow-description = Allow { -brand-short-name } to
 
 update-application-auto =
@@ -310,17 +310,17 @@ performance-settings-learn-more = Learn 
 performance-allow-hw-accel =
     .label = Use hardware acceleration when available
     .accesskey = r
 
 performance-limit-content-process-option = Content process limit
     .accesskey = l
 
 performance-limit-content-process-enabled-desc = Additional content processes can improve performance when using multiple tabs, but will also use more memory.
-performance-limit-content-process-blocked-desc = Modifying the number of content processes is only possible with multiprocess { -brand-short-name }. <a data-l10n-name="learn-more">Learn how to check if multiprocess is enabled</a>
+performance-limit-content-process-disabled-desc = Modifying the number of content processes is only possible with multiprocess { -brand-short-name }. <a>Learn how to check if multiprocess is enabled</a>
 
 # Variables:
 #   $num - default value of the `dom.ipc.processCount` pref.
 performance-default-content-process-count =
     .label = { $num } (default)
 
 ## General Section - Browsing
 
@@ -693,17 +693,17 @@ addressbar-locbar-openpage-option =
     .accesskey = O
 
 addressbar-suggestions-settings = Change preferences for search engine suggestions
 
 ## Privacy Section - Tracking
 
 tracking-header = Tracking Protection
 
-tracking-desc = Tracking Protection blocks online trackers that collect your browsing data across multiple websites. <a data-l10n-name="learn-more">Learn more about Tracking Protection and your privacy</a>
+tracking-description = Tracking Protection blocks online trackers that collect your browsing data across multiple websites. <a>Learn more about Tracking Protection and your privacy</a>
 
 tracking-mode-label = Use Tracking Protection to block known trackers
 
 tracking-mode-always =
     .label = Always
     .accesskey = y
 tracking-mode-private =
     .label = Only in private windows
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -11,38 +11,35 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 
-/* fluent-dom@0.2.0 */
+/* fluent@0.6.3 */
 
 const { Localization } =
   ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
 
 // Match the opening angle bracket (<) in HTML tags, and HTML entities like
 // &amp;, &#0038;, &#x0026;.
 const reOverlay = /<|&#?\w+;/;
 
 /**
- * Elements allowed in translations even if they are not present in the source
- * HTML. They are text-level elements as defined by the HTML5 spec:
- * https://www.w3.org/TR/html5/text-level-semantics.html with the exception of:
+ * The list of elements that are allowed to be inserted into a localization.
  *
- *   - a - because we don't allow href on it anyways,
- *   - ruby, rt, rp - because we don't allow nested elements to be inserted.
+ * Source: https://www.w3.org/TR/html5/text-level-semantics.html
  */
-const TEXT_LEVEL_ELEMENTS = {
+const LOCALIZABLE_ELEMENTS = {
   "http://www.w3.org/1999/xhtml": [
-    "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data",
+    "a", "em", "strong", "small", "s", "cite", "q", "dfn", "abbr", "data",
     "time", "code", "var", "samp", "kbd", "sub", "sup", "i", "b", "u",
-    "mark", "bdi", "bdo", "span", "br", "wbr"
+    "mark", "ruby", "rt", "rp", "bdi", "bdo", "span", "br", "wbr"
   ],
 };
 
 const LOCALIZABLE_ATTRIBUTES = {
   "http://www.w3.org/1999/xhtml": {
     global: ["title", "aria-label", "aria-valuetext", "aria-moz-hint"],
     a: ["download"],
     area: ["download", "alt"],
@@ -64,196 +61,177 @@ const LOCALIZABLE_ATTRIBUTES = {
     key: ["key", "keycode"],
     textbox: ["placeholder"],
     toolbarbutton: ["tooltiptext"],
   }
 };
 
 
 /**
- * Translate an element.
+ * Overlay translation onto a DOM 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.
- *
- * @param   {Element} element
- * @param   {Object} translation
+ * @param   {Element} targetElement
+ * @param   {string|Object} translation
  * @private
  */
-function translateElement(element, translation) {
+function overlayElement(targetElement, translation) {
   const value = translation.value;
 
   if (typeof value === "string") {
     if (!reOverlay.test(value)) {
       // If the translation doesn't contain any markup skip the overlay logic.
-      element.textContent = value;
+      targetElement.textContent = value;
     } else {
       // Else parse the translation's HTML using an inert template element,
-      // sanitize it and replace the element's content.
-      const templateElement = element.ownerDocument.createElementNS(
-        "http://www.w3.org/1999/xhtml", "template"
-      );
+      // sanitize it and replace the targetElement's content.
+      const templateElement = targetElement.ownerDocument.createElementNS(
+        "http://www.w3.org/1999/xhtml", "template");
       // eslint-disable-next-line no-unsanitized/property
       templateElement.innerHTML = value;
-      overlayChildNodes(templateElement.content, element);
+      targetElement.appendChild(
+        // The targetElement will be cleared at the end of sanitization.
+        sanitizeUsing(templateElement.content, targetElement)
+      );
     }
   }
 
-  // Even if the translation doesn't define any localizable attributes, run
-  // overlayAttributes to remove any localizable attributes set by previous
-  // translations.
-  overlayAttributes(translation, element);
-}
-
-/**
- * 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.
- *
- * @param {DocumentFragment} fromElement - The source of children to overlay.
- * @param {Element} toElement - The target of the overlay.
- * @private
- */
-function overlayChildNodes(fromElement, toElement) {
-  const content = toElement.ownerDocument.createDocumentFragment();
-
-  for (const childNode of fromElement.childNodes) {
-    content.appendChild(sanitizeUsing(toElement, childNode));
-  }
-
-  toElement.textContent = "";
-  toElement.appendChild(content);
-}
-
-/**
- * Transplant localizable attributes of an element to another element.
- *
- * Any localizable attributes already set on the target element will be
- * cleared.
- *
- * @param   {Element|Object} fromElement - The source of child nodes to overlay.
- * @param   {Element} toElement - The target of the overlay.
- * @private
- */
-function overlayAttributes(fromElement, toElement) {
-  const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs")
-    ? toElement.getAttribute("data-l10n-attrs")
+  const explicitlyAllowed = targetElement.hasAttribute("data-l10n-attrs")
+    ? targetElement.getAttribute("data-l10n-attrs")
       .split(",").map(i => i.trim())
     : null;
 
-  // Remove existing localizable attributes.
-  for (const attr of Array.from(toElement.attributes)) {
-    if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) {
-      toElement.removeAttribute(attr.name);
+  // Remove localizable attributes which may have been set by a previous
+  // translation.
+  for (const attr of Array.from(targetElement.attributes)) {
+    if (isAttrNameLocalizable(attr.name, targetElement, explicitlyAllowed)) {
+      targetElement.removeAttribute(attr.name);
     }
   }
 
-  // fromElement might be a {value, attributes} object as returned by
-  // Localization.messageFromContext. In which case attributes may be null to
-  // save GC cycles.
-  if (!fromElement.attributes) {
-    return;
-  }
-
-  // Set localizable attributes.
-  for (const attr of Array.from(fromElement.attributes)) {
-    if (isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed)) {
-      toElement.setAttribute(attr.name, attr.value);
+  if (translation.attrs) {
+    for (const {name, value} of translation.attrs) {
+      if (isAttrNameLocalizable(name, targetElement, explicitlyAllowed)) {
+        targetElement.setAttribute(name, value);
+      }
     }
   }
 }
 
 /**
- * Sanitize a child node created by the translation.
+ * Sanitize `translationFragment` using `sourceElement` to add functional
+ * HTML attributes to children.  `sourceElement` will have all its child nodes
+ * removed.
  *
- * If childNode has the data-l10n-name attribute, try to find a corresponding
- * child in sourceElement and use it as the base for the sanitization. This
- * will preserve functional attribtues defined on the child element in the
- * source HTML.
+ * The sanitization is conducted according to the following rules:
+ *
+ *   - Allow text nodes.
+ *   - Replace forbidden children with their textContent.
+ *   - Remove forbidden attributes from allowed children.
  *
- * This function must return new nodes or clones in all code paths. The
- * returned nodes are immediately appended to the intermediate DocumentFragment
- * which also _removes_ them from the constructed <template> containing the
- * translation, which in turn breaks the for…of iteration over its child nodes.
+ * Additionally when a child of the same type is present in `sourceElement` its
+ * attributes will be merged into the translated child.  Whitelisted attributes
+ * of the translated child will then overwrite the ones present in the source.
+ *
+ * The overlay logic is subject to the following limitations:
  *
- * @param   {Element} sourceElement - The source for data-l10n-name lookups.
- * @param   {Element} childNode - The child node to be sanitized.
- * @returns {Element}
+ *   - Children are always cloned.  Event handlers attached to them are lost.
+ *   - Nested HTML in source and in translations is not supported.
+ *   - Multiple children of the same type will be matched in order.
+ *
+ * @param {DocumentFragment} translationFragment
+ * @param {Element} sourceElement
+ * @returns {DocumentFragment}
  * @private
  */
-function sanitizeUsing(sourceElement, childNode) {
-  if (childNode.nodeType === childNode.TEXT_NODE) {
-    return childNode.cloneNode(false);
+function sanitizeUsing(translationFragment, sourceElement) {
+  const ownerDocument = translationFragment.ownerDocument;
+  // Take one node from translationFragment at a time and check it against
+  // the allowed list or try to match it with a corresponding element
+  // in the source.
+  for (const childNode of translationFragment.childNodes) {
+
+    if (childNode.nodeType === childNode.TEXT_NODE) {
+      continue;
+    }
+
+    // If the child is forbidden just take its textContent.
+    if (!isElementLocalizable(childNode)) {
+      const text = ownerDocument.createTextNode(childNode.textContent);
+      translationFragment.replaceChild(text, childNode);
+      continue;
+    }
+
+    // Start the sanitization with an empty element.
+    const mergedChild = ownerDocument.createElement(childNode.localName);
+
+    // Explicitly discard nested HTML by serializing childNode to a TextNode.
+    mergedChild.textContent = childNode.textContent;
+
+    // If a child of the same type exists in sourceElement, take its functional
+    // (i.e. non-localizable) attributes. This also removes the child from
+    // sourceElement.
+    const sourceChild = shiftNamedElement(sourceElement, childNode.localName);
+
+    // Find the union of all safe attributes: localizable attributes from
+    // childNode and functional attributes from sourceChild.
+    const safeAttributes = sanitizeAttrsUsing(childNode, sourceChild);
+
+    for (const attr of safeAttributes) {
+      mergedChild.setAttribute(attr.name, attr.value);
+    }
+
+    translationFragment.replaceChild(mergedChild, childNode);
   }
 
-  if (childNode.hasAttribute("data-l10n-name")) {
-    const childName = childNode.getAttribute("data-l10n-name");
-    const sourceChild = sourceElement.querySelector(
-      `[data-l10n-name="${childName}"]`
-    );
+  // SourceElement might have been already modified by shiftNamedElement.
+  // Let's clear it to make sure other code doesn't rely on random leftovers.
+  sourceElement.textContent = "";
+
+  return translationFragment;
+}
 
-    if (!sourceChild) {
-      console.warn(
-        `An element named "${childName}" wasn't found in the source.`
-      );
-    } else if (sourceChild.localName !== childNode.localName) {
-      console.warn(
-        `An element named "${childName}" was found in the translation ` +
-        `but its type ${childNode.localName} didn't match the element ` +
-        `found in the source (${sourceChild.localName}).`
-      );
-    } else {
-      // Remove it from sourceElement so that the translation cannot use
-      // the same reference name again.
-      sourceElement.removeChild(sourceChild);
-      // We can't currently guarantee that a translation won't remove
-      // sourceChild from the element completely, which could break the app if
-      // it relies on an event handler attached to the sourceChild. Let's make
-      // this limitation explicit for now by breaking the identitiy of the
-      // sourceChild by cloning it. This will destroy all event handlers
-      // attached to sourceChild via addEventListener and via on<name>
-      // properties.
-      const clone = sourceChild.cloneNode(false);
-      return shallowPopulateUsing(childNode, clone);
-    }
+/**
+ * Sanitize and merge attributes.
+ *
+ * Only localizable attributes from the translated child element and only
+ * functional attributes from the source child element are considered safe.
+ *
+ * @param {Element} translatedElement
+ * @param {Element} sourceElement
+ * @returns {Array<Attr>}
+ * @private
+ */
+function sanitizeAttrsUsing(translatedElement, sourceElement) {
+  const localizedAttrs = Array.from(translatedElement.attributes).filter(
+    attr => isAttrNameLocalizable(attr.name, translatedElement)
+  );
+
+  if (!sourceElement) {
+    return localizedAttrs;
   }
 
-  if (isElementAllowed(childNode)) {
-    // Start with an empty element of the same type to remove nested children
-    // and non-localizable attributes defined by the translation.
-    const clone = childNode.ownerDocument.createElement(childNode.localName);
-    return shallowPopulateUsing(childNode, clone);
-  }
-
-  console.warn(
-    `An element of forbidden type "${childNode.localName}" was found in ` +
-    "the translation. Only elements with data-l10n-name can be overlaid " +
-    "onto source elements of the same data-l10n-name."
+  const functionalAttrs = Array.from(sourceElement.attributes).filter(
+    attr => !isAttrNameLocalizable(attr.name, sourceElement)
   );
 
-  // If all else fails, convert the element to its text content.
-  return childNode.ownerDocument.createTextNode(childNode.textContent);
+  return localizedAttrs.concat(functionalAttrs);
 }
 
 /**
  * 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.
  *
  * @param   {Element} element
  * @returns {boolean}
  * @private
  */
-function isElementAllowed(element) {
-  const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI];
+function isElementLocalizable(element) {
+  const allowed = LOCALIZABLE_ELEMENTS[element.namespaceURI];
   return allowed && allowed.includes(element.localName);
 }
 
 /**
  * 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
@@ -304,27 +282,31 @@ function isAttrNameLocalizable(name, ele
       return true;
     }
   }
 
   return false;
 }
 
 /**
- * Helper to set textContent and localizable attributes on an element.
+ * Remove and return the first child of the given type.
  *
- * @param   {Element} fromElement
- * @param   {Element} toElement
- * @returns {Element}
+ * @param {DOMFragment} element
+ * @param {string}      localName
+ * @returns {Element | null}
  * @private
  */
-function shallowPopulateUsing(fromElement, toElement) {
-  toElement.textContent = fromElement.textContent;
-  overlayAttributes(fromElement, toElement);
-  return toElement;
+function shiftNamedElement(element, localName) {
+  for (const child of element.children) {
+    if (child.localName === localName) {
+      element.removeChild(child);
+      return child;
+    }
+  }
+  return null;
 }
 
 /**
  * Sanitizes a translation before passing them to Node.localize API.
  *
  * It returns `false` if the translation contains DOM Overlays and should
  * not go into Node.localize.
  *
@@ -345,28 +327,29 @@ function shallowPopulateUsing(fromElemen
  * @returns boolean
  * @private
  */
 function sanitizeTranslationForNodeLocalize(l10nItem, translation) {
   if (reOverlay.test(translation.value)) {
     return false;
   }
 
-  if (translation.attributes) {
+  if (translation.attrs) {
     const explicitlyAllowed = l10nItem.l10nAttrs === null ? null :
       l10nItem.l10nAttrs.split(",").map(i => i.trim());
-    for (const [j, {name}] of translation.attributes.entries()) {
+    for (const [j, {name}] of translation.attrs.entries()) {
       if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) {
-        translation.attributes.splice(j, 1);
+        translation.attrs.splice(j, 1);
       }
     }
   }
   return true;
 }
 
+
 const L10NID_ATTR_NAME = "data-l10n-id";
 const L10NARGS_ATTR_NAME = "data-l10n-args";
 
 const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`;
 
 /**
  * The `DOMLocalization` class is responsible for fetching resources and
  * formatting translations.
@@ -555,19 +538,17 @@ class DOMLocalization extends Localizati
    * Translate mutations detected by the `MutationObserver`.
    *
    * @private
    */
   translateMutations(mutations) {
     for (const mutation of mutations) {
       switch (mutation.type) {
         case "attributes":
-          if (mutation.target.hasAttribute("data-l10n-id")) {
-            this.pendingElements.add(mutation.target);
-          }
+          this.pendingElements.add(mutation.target);
           break;
         case "childList":
           for (const addedNode of mutation.addedNodes) {
             if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
               if (addedNode.childElementCount) {
                 for (const element of this.getTranslatables(addedNode)) {
                   this.pendingElements.add(element);
                 }
@@ -631,18 +612,16 @@ class DOMLocalization extends Localizati
 
           const hasOnlyText =
             sanitizeTranslationForNodeLocalize(l10nItems[i], translation);
           if (!hasOnlyText) {
             // Removing from translations to make Node.localize skip it.
             // We will translate it below using JS DOM Overlays.
             overlayTranslations[i] = translations[i];
             translations[i] = undefined;
-          } else {
-            translations[i].attrs = translations[i].attributes;
           }
         }
 
         // We pause translation observing here because Node.localize
         // will translate the whole DOM next, using the `translations`.
         //
         // The observer will be resumed after DOM Overlays are localized
         // in the next microtask.
@@ -650,17 +629,17 @@ class DOMLocalization extends Localizati
         return translations;
       };
 
       return frag.localize(getTranslationsForItems.bind(this))
         .then(untranslatedElements => {
           for (let i = 0; i < overlayTranslations.length; i++) {
             if (overlayTranslations[i] !== undefined &&
                 untranslatedElements[i] !== undefined) {
-              translateElement(untranslatedElements[i], overlayTranslations[i]);
+              overlayElement(untranslatedElements[i], overlayTranslations[i]);
             }
           }
           this.resumeObserving();
         })
         .catch(() => this.resumeObserving());
     }
     return this.translateElements(this.getTranslatables(frag));
   }
@@ -695,17 +674,17 @@ class DOMLocalization extends Localizati
    * @param {Array<Object>}  translations
    * @private
    */
   applyTranslations(elements, translations) {
     this.pauseObserving();
 
     for (let i = 0; i < elements.length; i++) {
       if (translations[i] !== undefined) {
-        translateElement(elements[i], translations[i]);
+        overlayElement(elements[i], translations[i]);
       }
     }
 
     this.resumeObserving();
   }
 
   /**
    * Collects all translatable child elements of the element.
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -11,17 +11,17 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 
-/* fluent-dom@0.2.0 */
+/* fluent@0.6.3 */
 
 /* eslint no-console: ["error", { allow: ["warn", "error"] }] */
 /* global console */
 
 const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
 const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {});
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
@@ -322,25 +322,25 @@ function valueFromContext(ctx, errors, i
  * @returns {Object}
  * @private
  */
 function messageFromContext(ctx, errors, id, args) {
   const msg = ctx.getMessage(id);
 
   const formatted = {
     value: ctx.format(msg, args, errors),
-    attributes: null,
+    attrs: null,
   };
 
   if (msg.attrs) {
-    formatted.attributes = [];
-    for (const [name, attr] of Object.entries(msg.attrs)) {
-      const value = ctx.format(attr, args, errors);
+    formatted.attrs = [];
+    for (const name in msg.attrs) {
+      const value = ctx.format(msg.attrs[name], args, errors);
       if (value !== null) {
-        formatted.attributes.push({name, value});
+        formatted.attrs.push({ name, value });
       }
     }
   }
 
   return formatted;
 }
 
 /**
--- a/intl/l10n/MessageContext.jsm
+++ b/intl/l10n/MessageContext.jsm
@@ -1603,45 +1603,41 @@ function Pattern(env, ptn) {
     errors.push(new RangeError("Cyclic reference"));
     return new FluentNone();
   }
 
   // Tag the pattern as dirty for the purpose of the current resolution.
   dirty.add(ptn);
   const result = [];
 
-  // Wrap interpolations with Directional Isolate Formatting characters
-  // only when the pattern has more than one element.
-  const useIsolating = ctx._useIsolating && ptn.length > 1;
-
   for (const elem of ptn) {
     if (typeof elem === "string") {
       result.push(elem);
       continue;
     }
 
     const part = Type(env, elem).toString(ctx);
 
-    if (useIsolating) {
+    if (ctx._useIsolating) {
       result.push(FSI);
     }
 
     if (part.length > MAX_PLACEABLE_LENGTH) {
       errors.push(
         new RangeError(
           "Too many characters in placeable " +
           `(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
         )
       );
       result.push(part.slice(MAX_PLACEABLE_LENGTH));
     } else {
       result.push(part);
     }
 
-    if (useIsolating) {
+    if (ctx._useIsolating) {
       result.push(PDI);
     }
   }
 
   dirty.delete(ptn);
   return result.join("");
 }
 
@@ -1774,26 +1770,18 @@ class MessageContext {
    * @returns {Array<Error>}
    */
   addMessages(source) {
     const [entries, errors] = parse(source);
     for (const id in entries) {
       if (id.startsWith("-")) {
         // Identifiers starting with a dash (-) define terms. Terms are private
         // and cannot be retrieved from MessageContext.
-        if (this._terms.has(id)) {
-          errors.push(`Attempt to override an existing term: "${id}"`);
-          continue;
-        }
         this._terms.set(id, entries[id]);
       } else {
-        if (this._messages.has(id)) {
-          errors.push(`Attempt to override an existing message: "${id}"`);
-          continue;
-        }
         this._messages.set(id, entries[id]);
       }
     }
 
     return errors;
   }
 
   /**
--- a/intl/l10n/test/dom/test_domloc_overlay.html
+++ b/intl/l10n/test/dom/test_domloc_overlay.html
@@ -10,17 +10,17 @@
   const { DOMLocalization } =
     ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function* mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages("title = <strong>Hello</strong> World");
-    mc.addMessages(`title2 = This is <a data-l10n-name="link">a link</a>!`);
+    mc.addMessages("title2 = This is <a>a link</a>!");
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
@@ -45,12 +45,12 @@
 
     a.click();
   };
   </script>
 </head>
 <body>
   <p data-l10n-id="title" />
   <p data-l10n-id="title2">
-    <a href="http://www.mozilla.org" data-l10n-name="link"></a>
+    <a href="http://www.mozilla.org"></a>
   </p>
 </body>
 </html>
--- a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
@@ -9,17 +9,17 @@
   "use strict";
   const { DOMLocalization } =
     ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function* mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
-    mc.addMessages(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`);
+    mc.addMessages("title = Visit <a>Mozilla</a> or <a>Firefox</a> website!");
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
@@ -41,14 +41,14 @@
     is(linkList.length, 2, "There should be exactly two links in the result.");
 
     SimpleTest.finish();
   };
   </script>
 </head>
 <body>
   <p data-l10n-id="title">
-    <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
-    <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
-    <a href="http://www.w3.org" data-l10n-name="w3-link"></a>
+    <a href="http://www.mozilla.org"></a>
+    <a href="http://www.firefox.com"></a>
+    <a href="http://www.w3.org"></a>
   </p>
 </body>
 </html>
--- a/intl/l10n/test/dom/test_domloc_overlay_repeated.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_repeated.html
@@ -9,17 +9,17 @@
   "use strict";
   const { DOMLocalization } =
     ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function* mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
-    mc.addMessages(`title = Visit <a data-l10n-name="mozilla-link">Mozilla</a> or <a data-l10n-name="firefox-link">Firefox</a> website!`);
+    mc.addMessages("title = Visit <a>Mozilla</a> or <a>Firefox</a> website!");
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
@@ -39,13 +39,13 @@
     is(linkList[1].textContent, "Firefox");
 
     SimpleTest.finish();
   };
   </script>
 </head>
 <body>
   <p data-l10n-id="title">
-    <a href="http://www.mozilla.org" data-l10n-name="mozilla-link"></a>
-    <a href="http://www.firefox.com" data-l10n-name="firefox-link"></a>
+    <a href="http://www.mozilla.org"></a>
+    <a href="http://www.firefox.com"></a>
   </p>
 </body>
 </html>
--- a/intl/l10n/test/dom/test_domloc_repeated_l10nid.html
+++ b/intl/l10n/test/dom/test_domloc_repeated_l10nid.html
@@ -12,17 +12,17 @@
   const { MessageContext } =
     ChromeUtils.import("resource://gre/modules/MessageContext.jsm", {});
 
   async function* mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages(`
 key1 = Translation For Key 1
 
-key2 = Visit <a data-l10n-name="link">this link<a/>.
+key2 = Visit <a>this link<a/>.
     `);
     yield mc;
   }
 
   SimpleTest.waitForExplicitFinish();
   addLoadEvent(async () => {
     const domLoc = new DOMLocalization(
       window,
@@ -48,16 +48,16 @@ key2 = Visit <a data-l10n-name="link">th
   });
   </script>
 </head>
 <body>
   <h1 id="elem1" data-l10n-id="key1"></h1>
   <h2 id="elem2" data-l10n-id="key1"></h2>
 
   <p id="elem3" data-l10n-id="key2">
-    <a href="http://www.mozilla.org" data-l10n-name="link"></a>
+    <a href="http://www.mozilla.org"></a>
   </p>
 
   <p id="elem4" data-l10n-id="key2">
-    <a href="http://www.firefox.com" data-l10n-name="link"></a>
+    <a href="http://www.firefox.com"></a>
   </p>
 </body>
 </html>
deleted file mode 100644
--- a/python/l10n/fluent_migrations/bug_1453480_preferences_dom2_resources.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# coding=utf8
-
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-
-from __future__ import absolute_import
-import fluent.syntax.ast as FTL
-from fluent.migrate.helpers import MESSAGE_REFERENCE, EXTERNAL_ARGUMENT
-from fluent.migrate import COPY, CONCAT, REPLACE
-
-def migrate(ctx):
-    """Bug 1453480 - Migrate Fluent resources to use DOM Overlays, part {index}."""
-
-    ctx.add_transforms(
-        'browser/browser/preferences/preferences.ftl',
-        'browser/browser/preferences/preferences.ftl',
-        [
-            FTL.Message(
-                id=FTL.Identifier('search-results-empty-message'),
-                value=FTL.Pattern(
-                    elements=[
-                        FTL.Placeable(
-                            expression=FTL.SelectExpression(
-                                expression=FTL.CallExpression(
-                                    callee=FTL.Function('PLATFORM')
-                                ),
-                                variants=[
-                                    FTL.Variant(
-                                        key=FTL.VariantName('windows'),
-                                        default=False,
-                                        value=REPLACE(
-                                            'browser/chrome/browser/preferences/preferences.properties',
-                                            'searchResults.sorryMessageWin',
-                                            {
-                                                '%S': FTL.TextElement('<span data-l10n-name="query"></span>')
-                                            }
-                                        )
-                                    ),
-                                    FTL.Variant(
-                                        key=FTL.VariantName('other'),
-                                        default=True,
-                                        value=REPLACE(
-                                            'browser/chrome/browser/preferences/preferences.properties',
-                                            'searchResults.sorryMessageUnix',
-                                            {
-                                                '%S': FTL.TextElement('<span data-l10n-name="query"></span>')
-                                            }
-                                        )
-                                    )
-                                ]
-                            )
-                        )
-                    ]
-                )
-            ),
-            FTL.Message(
-                id=FTL.Identifier('search-results-help-link'),
-                value=REPLACE(
-                    'browser/chrome/browser/preferences/preferences.properties',
-                    'searchResults.needHelp3',
-                    {
-                        '%S': CONCAT(
-                            FTL.TextElement('<a data-l10n-name="url">'),
-                            REPLACE(
-                                'browser/chrome/browser/preferences/preferences.properties',
-                                'searchResults.needHelpSupportLink',
-                                {
-                                    '%S': MESSAGE_REFERENCE('-brand-short-name'),
-                                }
-                            ),
-                            FTL.TextElement('</a>')
-                        )
-                    }
-                )
-            ),
-            FTL.Message(
-                id=FTL.Identifier('update-application-version'),
-                value=CONCAT(
-                    COPY(
-                        'browser/chrome/browser/preferences/advanced.dtd',
-                        'updateApplication.version.pre'
-                    ),
-                    EXTERNAL_ARGUMENT('version'),
-                    COPY(
-                        'browser/chrome/browser/preferences/advanced.dtd',
-                        'updateApplication.version.post'
-                    ),
-                    FTL.TextElement(' <a data-l10n-name="learn-more">'),
-                    COPY(
-                        'browser/chrome/browser/aboutDialog.dtd',
-                        'releaseNotes.link'
-                    ),
-                    FTL.TextElement('</a>')
-                )
-            ),
-            FTL.Message(
-                id=FTL.Identifier('performance-limit-content-process-blocked-desc'),
-                value=CONCAT(
-                    REPLACE(
-                        'browser/chrome/browser/preferences/advanced.dtd',
-                        'limitContentProcessOption.disabledDescription',
-                        {
-                            '&brandShortName;': MESSAGE_REFERENCE('-brand-short-name')
-                        }
-                    ),
-                    FTL.TextElement(' <a data-l10n-name="learn-more">'),
-                    COPY(
-                        'browser/chrome/browser/preferences/advanced.dtd',
-                        'limitContentProcessOption.disabledDescriptionLink'
-                    ),
-                    FTL.TextElement('</a>')
-                )
-            ),
-            FTL.Message(
-                id=FTL.Identifier('tracking-desc'),
-                value=CONCAT(
-                    COPY(
-                        'browser/chrome/browser/preferences/privacy.dtd',
-                        'trackingProtection3.description'
-                    ),
-                    FTL.TextElement(' <a data-l10n-name="learn-more">'),
-                    COPY(
-                        'browser/chrome/browser/preferences/privacy.dtd',
-                        'trackingProtectionLearnMore2.label'
-                    ),
-                    FTL.TextElement('</a>')
-                )
-            ),
-        ]
-    )