Bug 1455649: Implement document.l10n in webidl. draft
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 19 Apr 2018 16:37:49 -0700
changeset 794878 a781ca6ff20c1684465a2cef718e76b105292430
parent 792231 d76dd026cce57fd55a7b29f41379414d670cb243
push id109807
push userdtownsend@mozilla.com
push dateMon, 14 May 2018 19:08:52 +0000
bugs1455649
milestone62.0a1
Bug 1455649: Implement document.l10n in webidl. This implements document.webidl available for all documents that cannot be loaded by webpages, this includes untrusted content UI in the content processes. The webidl code is implemented in c++. There is small listener for new html:link localization elements. The first one found instantiates the DocumentL10n object and all link elements are subsequently passed along to it. All API calls are forwarded through mozDocumentL10nHelper to the underlying DOMLocalization object. The result is that for unprivileged web-content document.l10n is not exposed. For UI content document.l10n is visible but accessing it will throw an exception until a html:link localization element is inserted into the document. MozReview-Commit-ID: 9BXEwCOIoy8
browser/components/preferences/applicationManager.xul
browser/components/preferences/blocklists.xul
browser/components/preferences/clearSiteData.xul
browser/components/preferences/colors.xul
browser/components/preferences/connection.xul
browser/components/preferences/containers.xul
browser/components/preferences/fonts.xul
browser/components/preferences/in-content/preferences.xul
browser/components/preferences/languages.xul
browser/components/preferences/permissions.xul
browser/components/preferences/selectBookmark.xul
browser/components/preferences/siteDataRemoveSelected.xul
browser/components/preferences/siteDataSettings.xul
browser/components/preferences/sitePermissions.xul
browser/components/preferences/translation.xul
browser/installer/package-manifest.in
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/html/HTMLBodyElement.cpp
dom/html/HTMLLinkElement.cpp
dom/webidl/Document.webidl
dom/webidl/DocumentL10n.webidl
dom/webidl/moz.build
dom/xml/nsXMLContentSink.cpp
dom/xul/XULDocument.cpp
dom/xul/nsXULElement.cpp
dom/xul/nsXULElement.h
intl/l10n/DOMLocalization.jsm
intl/l10n/DocumentL10n.cpp
intl/l10n/DocumentL10n.h
intl/l10n/l10n.manifest
intl/l10n/moz.build
intl/l10n/mozDocumentL10nHelper.js
intl/l10n/mozIDocumentL10nHelper.idl
intl/l10n/test/dom/test_domloc.xul
intl/l10n/test/dom/test_domloc_mutations.html
intl/l10n/test/dom/test_domloc_translateRoots.html
xpcom/ds/nsGkAtomList.h
--- a/browser/components/preferences/applicationManager.xul
+++ b/browser/components/preferences/applicationManager.xul
@@ -2,26 +2,28 @@
 <!-- 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/"?>
 
 <dialog id="appManager"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         buttons="accept,cancel"
         onload="gAppManagerDialog.onLoad();"
         ondialogaccept="gAppManagerDialog.onOK();"
         ondialogcancel="gAppManagerDialog.onCancel();"
         data-l10n-id="app-manager-window"
         data-l10n-attrs="title, style"
         persist="screenX screenY">
 
-  <link rel="localization" href="browser/preferences/applicationManager.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/applicationManager.ftl"/>
+  </linkset>
 
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript"
           src="chrome://global/content/preferencesBindings.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/preferences/applicationManager.js"/>
 
--- a/browser/components/preferences/blocklists.xul
+++ b/browser/components/preferences/blocklists.xul
@@ -7,24 +7,26 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
 
 <window id="BlocklistsDialog" class="windowDialog"
         windowtype="Browser:Blocklists"
         data-l10n-id="blocklist-window"
         data-l10n-attrs="title, style"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         onload="gBlocklistManager.onLoad();"
         onunload="gBlocklistManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gBlocklistManager.onWindowKeyPress(event);">
 
-  <link rel="localization" href="branding/brand.ftl"/>
-  <link rel="localization" href="browser/preferences/blocklists.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="branding/brand.ftl"/>
+    <html:link rel="localization" href="browser/preferences/blocklists.ftl"/>
+  </linkset>
 
   <script src="chrome://global/content/treeUtils.js"/>
   <script src="chrome://browser/content/preferences/blocklists.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <keyset>
--- a/browser/components/preferences/clearSiteData.xul
+++ b/browser/components/preferences/clearSiteData.xul
@@ -6,23 +6,26 @@
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/preferences/clearSiteData.css" type="text/css"?>
 
 <window id="ClearSiteDataDialog" class="windowDialog"
         windowtype="Browser:ClearSiteData"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="clear-site-data-window"
         data-l10n-attrs="title, style"
         persist="screenX screenY width height">
 
-  <link rel="localization" href="branding/brand.ftl"/>
-  <link rel="localization" href="browser/preferences/clearSiteData.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="branding/brand.ftl"/>
+    <html:link rel="localization" href="browser/preferences/clearSiteData.ftl"/>
+  </linkset>
+
   <script src="chrome://browser/content/preferences/clearSiteData.js"/>
 
   <keyset>
     <key data-l10n-id="clear-site-data-close-key" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
   <vbox class="contentPane largeDialogContainer" flex="1">
     <description control="url" data-l10n-id="clear-site-data-description"/>
--- a/browser/components/preferences/colors.xul
+++ b/browser/components/preferences/colors.xul
@@ -5,26 +5,28 @@
 # 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/.
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
 <dialog id="ColorsDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="colors-window"
         data-l10n-attrs="title, style"
         buttons="accept,cancel,help"
         persist="lastSelected screenX screenY"
         role="dialog"
         helpTopic="prefs-fonts-and-colors"
         ondialoghelp="openPrefsHelp()">
 
-  <link rel="localization" href="browser/preferences/colors.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/colors.ftl"/>
+  </linkset>
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
 
   <keyset>
     <key data-l10n-id="colors-close-key" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
 
--- a/browser/components/preferences/connection.xul
+++ b/browser/components/preferences/connection.xul
@@ -4,32 +4,34 @@
    - 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
 <dialog id="ConnectionsDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="connection-window"
         data-l10n-attrs="title, style"
         buttons="accept,cancel,help"
         persist="lastSelected screenX screenY"
         role="dialog"
         onbeforeaccept="return gConnectionsDialog.beforeAccept();"
         onload="gConnectionsDialog.checkForSystemProxy();"
         helpTopic="prefs-connection-settings"
         ondialoghelp="openPrefsHelp()">
 
-  <link rel="localization" href="browser/preferences/connection.ftl"/>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/connection.ftl"/>
 
-  <!-- Used for extension-controlled lockdown message -->
-  <link rel="localization" href="browser/preferences/preferences.ftl"/>
-  <link rel="localization" href="branding/brand.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+    <!-- Used for extension-controlled lockdown message -->
+    <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
+    <html:link rel="localization" href="branding/brand.ftl"/>
+  </linkset>
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
   <script type="application/javascript" src="chrome://browser/content/preferences/in-content/extensionControlled.js"/>
 
   <keyset>
     <key data-l10n-id="connection-close-key" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
--- a/browser/components/preferences/containers.xul
+++ b/browser/components/preferences/containers.xul
@@ -5,24 +5,26 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/containers.css" type="text/css"?>
 
 <window id="ContainersDialog" class="windowDialog"
         windowtype="Browser:Permissions"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-attrs="title, style"
         onload="gContainersManager.onLoad();"
         onunload="gContainersManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gContainersManager.onWindowKeyPress(event);">
 
-  <link rel="localization" href="browser/preferences/containers.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/containers.ftl"/>
+  </linkset>
 
   <script src="chrome://global/content/treeUtils.js"/>
   <script src="chrome://browser/content/preferences/containers.js"/>
 
   <keyset>
     <key data-l10n-id="containers-window-close" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
--- a/browser/components/preferences/fonts.xul
+++ b/browser/components/preferences/fonts.xul
@@ -5,27 +5,29 @@
    - 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
 <dialog id="FontsDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="fonts-window"
         data-l10n-attrs="title"
         buttons="accept,cancel,help"
         persist="lastSelected screenX screenY"
         role="dialog"
         helpTopic="prefs-fonts-and-colors"
         ondialoghelp="openPrefsHelp()"
         onbeforeaccept="return gFontsDialog.onBeforeAccept();">
 
-  <link rel="localization" href="browser/preferences/fonts.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/fonts.ftl"/>
+  </linkset>
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
 
   <keyset>
     <key data-l10n-id="fonts-window-close" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
 
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -32,32 +32,32 @@
 ]>
 
 <page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
       xmlns:html="http://www.w3.org/1999/xhtml"
       disablefastfind="true"
       data-l10n-id="pref-page"
       data-l10n-attrs="title">
 
-  <link rel="localization" href="branding/brand.ftl"/>
-  <link rel="localization" href="browser/branding/sync-brand.ftl"/>
-  <link rel="localization" href="browser/preferences/preferences.ftl"/>
+  <linkset>
+    <html:link rel="localization" href="branding/brand.ftl"/>
+    <html:link rel="localization" href="browser/branding/sync-brand.ftl"/>
+    <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
 
-  <!-- Links below are only used for search-l10n-ids into subdialogs -->
-  <link rel="localization" href="browser/preferences/blocklists.ftl"/>
-  <link rel="localization" href="browser/preferences/clearSiteData.ftl"/>
-  <link rel="localization" href="browser/preferences/colors.ftl"/>
-  <link rel="localization" href="browser/preferences/connection.ftl"/>
-  <link rel="localization" href="browser/preferences/fonts.ftl"/>
-  <link rel="localization" href="browser/preferences/languages.ftl"/>
-  <link rel="localization" href="browser/preferences/permissions.ftl"/>
-  <link rel="localization" href="browser/preferences/selectBookmark.ftl"/>
-  <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
-
-  <script type="text/javascript" src="chrome://global/content/l10n.js"></script>
+    <!-- Links below are only used for search-l10n-ids into subdialogs -->
+    <html:link rel="localization" href="browser/preferences/blocklists.ftl"/>
+    <html:link rel="localization" href="browser/preferences/clearSiteData.ftl"/>
+    <html:link rel="localization" href="browser/preferences/colors.ftl"/>
+    <html:link rel="localization" href="browser/preferences/connection.ftl"/>
+    <html:link rel="localization" href="browser/preferences/fonts.ftl"/>
+    <html:link rel="localization" href="browser/preferences/languages.ftl"/>
+    <html:link rel="localization" href="browser/preferences/permissions.ftl"/>
+    <html:link rel="localization" href="browser/preferences/selectBookmark.ftl"/>
+    <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+  </linkset>
 
   <html:link rel="shortcut icon"
               href="chrome://browser/skin/settings.svg"/>
 
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript"
           src="chrome://global/content/preferencesBindings.js"/>
--- a/browser/components/preferences/languages.xul
+++ b/browser/components/preferences/languages.xul
@@ -5,27 +5,29 @@
    - 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
 <dialog id="LanguagesDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="languages-window"
         data-l10n-attrs="title, style"
         buttons="accept,cancel,help"
         persist="lastSelected screenX screenY"
         role="dialog"
         onload="gLanguagesDialog.init();"
         helpTopic="prefs-languages"
         ondialoghelp="openPrefsHelp()">
 
-  <link rel="localization" href="browser/preferences/languages.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/languages.ftl"/>
+  </linkset>
 
   <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
   <script type="application/javascript" src="chrome://browser/content/preferences/languages.js"/>
 
   <keyset>
     <key data-l10n-id="languages-close-key" modifiers="accel" oncommand="Preferences.close(event)"/>
   </keyset>
--- a/browser/components/preferences/permissions.xul
+++ b/browser/components/preferences/permissions.xul
@@ -7,23 +7,25 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
 
 <window id="PermissionsDialog" class="windowDialog"
         windowtype="Browser:Permissions"
         data-l10n-id="permissions-window"
         data-l10n-attrs="title, style"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         onload="gPermissionManager.onLoad();"
         onunload="gPermissionManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gPermissionManager.onWindowKeyPress(event);">
 
-  <link rel="localization" href="browser/preferences/permissions.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/permissions.ftl"/>
+  </linkset>
 
   <script src="chrome://global/content/treeUtils.js"/>
   <script src="chrome://browser/content/preferences/permissions.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <keyset>
--- a/browser/components/preferences/selectBookmark.xul
+++ b/browser/components/preferences/selectBookmark.xul
@@ -6,24 +6,26 @@
 
 <?xml-stylesheet href="chrome://browser/content/places/places.css"?>
 
 <?xml-stylesheet href="chrome://global/skin/"?>
 <?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
 
 <dialog id="selectBookmarkDialog"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="select-bookmark-window"
         data-l10n-attrs="title, style"
         persist="screenX screenY width height" screenX="24" screenY="24"
         onload="SelectBookmarkDialog.init();"
         ondialogaccept="SelectBookmarkDialog.accept();">
 
-  <link rel="localization" href="browser/preferences/selectBookmark.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/selectBookmark.ftl"/>
+  </linkset>
 
   <script type="application/javascript"
           src="chrome://browser/content/preferences/selectBookmark.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript"><![CDATA[
--- a/browser/components/preferences/siteDataRemoveSelected.xul
+++ b/browser/components/preferences/siteDataRemoveSelected.xul
@@ -11,20 +11,22 @@
 <dialog id="SiteDataRemoveSelectedDialog"
         windowtype="Browser:SiteDataRemoveSelected"
         width="500"
         data-l10n-id="site-data-removing-window"
         data-l10n-attrs="title"
         onload="gSiteDataRemoveSelected.init();"
         ondialogaccept="gSiteDataRemoveSelected.ondialogaccept(); return true;"
         ondialogcancel="gSiteDataRemoveSelected.ondialogcancel(); return true;"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
 
-  <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+  </linkset>
 
   <script src="chrome://browser/content/preferences/siteDataRemoveSelected.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <vbox id="contentContainer">
     <hbox flex="1">
--- a/browser/components/preferences/siteDataSettings.xul
+++ b/browser/components/preferences/siteDataSettings.xul
@@ -9,23 +9,25 @@
 <?xml-stylesheet href="chrome://browser/content/preferences/siteDataSettings.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/siteDataSettings.css" type="text/css"?>
 
 <window id="SiteDataSettingsDialog" windowtype="Browser:SiteDataSettings"
         data-l10n-id="site-data-settings-window"
         data-l10n-attrs="title"
         class="windowDialog"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         style="width: 45em;"
         onload="gSiteDataSettings.init();"
         onkeypress="gSiteDataSettings.onKeyPress(event);"
         persist="screenX screenY width height">
 
-  <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+  </linkset>
 
   <script src="chrome://browser/content/preferences/siteDataSettings.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
   <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
 
   <vbox flex="1">
--- a/browser/components/preferences/sitePermissions.xul
+++ b/browser/components/preferences/sitePermissions.xul
@@ -6,25 +6,27 @@
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/preferences/sitePermissions.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
 
 <window id="SitePermissionsDialog" class="windowDialog"
         windowtype="Browser:SitePermissions"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         data-l10n-id="permissions-window"
         data-l10n-attrs="style"
         onload="gSitePermissionsManager.onLoad();"
         onunload="gSitePermissionsManager.uninit();"
         persist="screenX screenY width height"
         onkeypress="gSitePermissionsManager.onWindowKeyPress(event);">
 
-  <link rel="localization" href="browser/preferences/permissions.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/permissions.ftl"/>
+  </linkset>
 
   <script src="chrome://browser/content/preferences/sitePermissions.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <keyset>
     <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/>
--- a/browser/components/preferences/translation.xul
+++ b/browser/components/preferences/translation.xul
@@ -7,23 +7,25 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
 
 <window id="TranslationDialog" class="windowDialog"
         windowtype="Browser:TranslationExceptions"
         data-l10n-id="translation-window"
         data-l10n-attrs="title, style"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         onload="gTranslationExceptions.onLoad();"
         onunload="gTranslationExceptions.uninit();"
         persist="screenX screenY width height"
         onkeypress="gTranslationExceptions.onWindowKeyPress(event);">
 
-  <link rel="localization" href="browser/preferences/translation.ftl"/>
-  <script type="application/javascript" src="chrome://global/content/l10n.js"></script>
+  <linkset>
+    <html:link rel="localization" href="browser/preferences/translation.ftl"/>
+  </linkset>
 
   <script src="chrome://browser/content/preferences/translation.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <keyset>
     <key data-l10n-id="translation-close-key" modifiers="accel" oncommand="window.close();"/>
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -355,16 +355,19 @@
 @RESPATH@/components/TestInterfaceJSMaplike.js
 #endif
 
 #if defined(MOZ_DEBUG) || defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)
 @RESPATH@/browser/components/testComponents.manifest
 @RESPATH@/browser/components/startupRecorder.js
 #endif
 
+@RESPATH@/components/mozDocumentL10nHelper.js
+@RESPATH@/components/l10n.manifest
+
 ; [Extensions]
 @RESPATH@/components/extensions-toolkit.manifest
 @RESPATH@/components/extension-process-script.js
 @RESPATH@/browser/components/extensions-browser.manifest
 
 ; Modules
 @RESPATH@/browser/modules/*
 @RESPATH@/modules/*
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -204,16 +204,17 @@
 #include "nsWrapperCacheInlines.h"
 #include "nsSandboxFlags.h"
 #include "mozilla/dom/AnimatableBinding.h"
 #include "mozilla/dom/AnonymousContent.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/ClientInfo.h"
 #include "mozilla/dom/ClientState.h"
 #include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DocumentL10n.h"
 #include "mozilla/dom/DocumentTimeline.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/HTMLBodyElement.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/ImageTracker.h"
 #include "mozilla/dom/MediaQueryList.h"
 #include "mozilla/dom/NodeFilterBinding.h"
 #include "mozilla/OwningNonNull.h"
@@ -1866,16 +1867,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   }
 
   // Traverse all nsIDocument pointer members.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityInfo)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDisplayDocument)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyForIdle)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAboutCapabilities)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n)
 
   // Traverse all nsDocument nsCOMPtrs.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptGlobalObject)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStyleSheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheetSetList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptLoader)
@@ -2021,16 +2023,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mForms);
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mScripts);
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mApplets);
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchors);
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOrientationPendingPromise)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle);
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAboutCapabilities)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n)
 
   tmp->mParentDocument = nullptr;
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadingImages)
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntersectionObservers)
 
   tmp->ClearAllBoxObjects();
@@ -2219,16 +2222,44 @@ nsIDocument::Reset(nsIChannel* aChannel,
       mDocumentBaseURI = baseURI;
       mChromeXHRDocBaseURI = nullptr;
     }
   }
 
   mChannel = aChannel;
 }
 
+bool
+PrincipalAllowsL10n(nsIPrincipal* principal) {
+  // Fast track privileged contexts
+  if (nsContentUtils::IsSystemPrincipal(principal)) {
+    return true;
+  }
+
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = principal->GetURI(getter_AddRefs(uri));
+  if (NS_FAILED(rv) || !uri) {
+    return false;
+  }
+
+  bool isAbout;
+  rv = uri->SchemeIs("about", &isAbout);
+  if (NS_FAILED(rv) || (!isAbout)) {
+    return false;
+  }
+
+  bool isNonWeb;
+  rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_DANGEROUS_TO_LOAD, &isNonWeb);
+  if (NS_FAILED(rv) || !isNonWeb) {
+    return false;
+  }
+
+  return true;
+}
+
 void
 nsIDocument::ResetToURI(nsIURI* aURI,
                         nsILoadGroup* aLoadGroup,
                         nsIPrincipal* aPrincipal)
 {
   NS_PRECONDITION(aURI, "Null URI passed to ResetToURI");
 
   MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug,
@@ -3186,16 +3217,22 @@ nsIDocument::SetPrincipal(nsIPrincipal *
     bool isHTTPS;
     if (!uri || NS_FAILED(uri->SchemeIs("https", &isHTTPS)) ||
         isHTTPS) {
       mAllowDNSPrefetch = false;
     }
   }
   mNodeInfoManager->SetDocumentPrincipal(aNewPrincipal);
 
+  if (aNewPrincipal && PrincipalAllowsL10n(aNewPrincipal)) {
+    mDocumentL10n = new DocumentL10n(this);
+  } else {
+    mDocumentL10n = nullptr;
+  }
+
 #ifdef DEBUG
   // Validate that the docgroup is set correctly by calling its getter and
   // triggering its sanity check.
   //
   // If we're setting the principal to null, we don't want to perform the check,
   // as the document is entering an intermediate state where it does not have a
   // principal. It will be given another real principal shortly which we will
   // check. It's not unsafe to have a document which has a null principal in the
@@ -3379,16 +3416,103 @@ nsIDocument::GetAboutCapabilities(ErrorR
     }
     mAboutCapabilities = new AboutCapabilities(jsImplObj, sgo);
   }
   RefPtr<AboutCapabilities> aboutCapabilities =
     static_cast<AboutCapabilities*>(mAboutCapabilities.get());
   return aboutCapabilities.forget();
 }
 
+class LoadLocalizationsRunnable : public mozilla::Runnable
+{
+public:
+  explicit LoadLocalizationsRunnable(nsIDocument* aDoc)
+    : mozilla::Runnable("LoadLocalizationsRunnable")
+    , mDoc(aDoc)
+  {
+  }
+
+  NS_IMETHOD Run() override
+  {
+    mDoc->mDocumentL10n->LoadLocalizations();
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIDocument> mDoc;
+};
+
+void
+nsIDocument::LoadDocumentL10n()
+{
+  if (mDocumentL10n) {
+    nsContentUtils::AddScriptRunner(new LoadLocalizationsRunnable(this));
+  }
+}
+
+bool
+nsDocument::DocumentSupportsL10n(JSContext* aCx, JSObject* aObject)
+{
+  return PrincipalAllowsL10n(nsContentUtils::SubjectPrincipal(aCx));
+}
+
+class HandleLocalizationId : public mozilla::Runnable
+{
+public:
+  explicit HandleLocalizationId(nsIDocument* aDoc, const nsAString& aResourceId)
+    : mozilla::Runnable("HandleLocalizationId")
+    , mDoc(aDoc)
+    , mResourceId(aResourceId)
+  {
+  }
+
+  NS_IMETHOD Run() override
+  {
+    mDoc->mDocumentL10n->AddResourceId(mResourceId);
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIDocument> mDoc;
+  nsString mResourceId;
+};
+
+void
+nsIDocument::LocalizationLinkAdded(Element* aLinkElement)
+{
+  if (!mDocumentL10n) {
+    return;
+  }
+
+  Element* parent = aLinkElement->GetParentElement();
+  if (!parent) {
+    return;
+  }
+
+  Element* head = GetHeadElement();
+  if (parent != head && !parent->NodeInfo()->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)) {
+    // TODO log a warning
+    return;
+  }
+
+  nsString href;
+  aLinkElement->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
+  nsContentUtils::AddScriptRunner(new HandleLocalizationId(this, href));
+}
+
+already_AddRefed<DocumentL10n>
+nsIDocument::GetL10n(ErrorResult& aRv)
+{
+  if (!mDocumentL10n) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  RefPtr<DocumentL10n> l10n(mDocumentL10n);
+  return l10n.forget();
+}
+
 bool
 nsDocument::IsElementAnimateEnabled(JSContext* aCx, JSObject* /*unused*/)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   return nsContentUtils::IsSystemCaller(aCx) ||
          nsContentUtils::AnimationsAPICoreEnabled() ||
          nsContentUtils::AnimationsAPIElementAnimateEnabled();
@@ -5200,16 +5324,20 @@ nsIDocument::DispatchContentLoadedEvents
 
   // Unpin references to preloaded images
   mPreloadingImages.Clear();
 
   // DOM manipulation after content loaded should not care if the element
   // came from the preloader.
   mPreloadedPreconnects.Clear();
 
+  if (mDocumentL10n) {
+    mDocumentL10n->NotifyLoaded();
+  }
+
   if (mTiming) {
     mTiming->NotifyDOMContentLoadedStart(nsIDocument::GetDocumentURI());
   }
 
   // Dispatch observer notification to notify observers document is interactive.
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
     nsIPrincipal* principal = NodePrincipal();
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -156,16 +156,17 @@ public:
                                      nsISupports* aContainer,
                                      nsIStreamListener **aDocListener,
                                      bool aReset = true,
                                      nsIContentSink* aContentSink = nullptr) override = 0;
 
   virtual void StopDocumentLoad() override;
 
   static bool CallerIsTrustedAboutPage(JSContext* aCx, JSObject* aObject);
+  static bool DocumentSupportsL10n(JSContext* aCx, JSObject* aObject);
   static bool IsElementAnimateEnabled(JSContext* aCx, JSObject* aObject);
   static bool IsWebAnimationsEnabled(JSContext* aCx, JSObject* aObject);
   static bool IsWebAnimationsEnabled(mozilla::dom::CallerType aCallerType);
 
   virtual void EndUpdate(nsUpdateType aUpdateType) override;
   virtual void BeginLoad() override;
   virtual void EndLoad() override;
 
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -113,16 +113,18 @@ class nsRange;
 class nsSMILAnimationController;
 class nsSVGElement;
 class nsTextNode;
 class nsUnblockOnloadEvent;
 class nsWindowSizes;
 class nsDOMCaretPosition;
 class nsViewportInfo;
 class nsIGlobalObject;
+class LoadLocalizationsRunnable;
+class HandleLocalizationId;
 
 namespace mozilla {
 class AbstractThread;
 class CSSStyleSheet;
 class Encoding;
 class ErrorResult;
 class EventStates;
 class EventListenerManager;
@@ -145,16 +147,17 @@ class Attr;
 class BoxObject;
 class ClientInfo;
 class ClientState;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocGroup;
 class DocumentFragment;
+class DocumentL10n;
 class DocumentTimeline;
 class DocumentType;
 class DOMImplementation;
 class DOMIntersectionObserver;
 class DOMStringList;
 class Element;
 struct ElementCreationOptions;
 class Event;
@@ -3191,16 +3194,19 @@ public:
   void GetInputEncoding(nsAString& aInputEncoding) const;
   already_AddRefed<mozilla::dom::Location> GetLocation() const;
   void GetReferrer(nsAString& aReferrer) const;
   void GetLastModified(nsAString& aLastModified) const;
   void GetReadyState(nsAString& aReadyState) const;
 
   already_AddRefed<mozilla::dom::AboutCapabilities> GetAboutCapabilities(
     ErrorResult& aRv);
+  void LocalizationLinkAdded(Element* aLinkElement);
+  void LoadDocumentL10n();
+  already_AddRefed<mozilla::dom::DocumentL10n> GetL10n(ErrorResult& aRv);
 
   void GetTitle(nsAString& aTitle);
   void SetTitle(const nsAString& aTitle, mozilla::ErrorResult& rv);
   void GetDir(nsAString& aDirection) const;
   void SetDir(const nsAString& aDirection);
   nsIHTMLCollection* Images();
   nsIHTMLCollection* Embeds();
   nsIHTMLCollection* Plugins()
@@ -3868,16 +3874,19 @@ protected:
   // focus has never occurred then mLastFocusTime.IsNull() will be true.
   mozilla::TimeStamp mLastFocusTime;
 
   mozilla::EventStates mDocumentState;
 
   RefPtr<mozilla::dom::Promise> mReadyForIdle;
 
   RefPtr<mozilla::dom::AboutCapabilities> mAboutCapabilities;
+  RefPtr<mozilla::dom::DocumentL10n> mDocumentL10n;
+  friend class LoadLocalizationsRunnable;
+  friend class HandleLocalizationId;
 
   // True if BIDI is enabled.
   bool mBidiEnabled : 1;
   // True if a MathML element has ever been owned by this document.
   bool mMathMLEnabled : 1;
 
   // True if this document is the initial document for a window.  This should
   // basically be true only for documents that exist in newly-opened windows or
--- a/dom/html/HTMLBodyElement.cpp
+++ b/dom/html/HTMLBodyElement.cpp
@@ -294,16 +294,20 @@ nsresult
 HTMLBodyElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                             nsIContent* aBindingParent,
                             bool aCompileEventHandlers)
 {
   nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
                                                  aBindingParent,
                                                  aCompileEventHandlers);
   NS_ENSURE_SUCCESS(rv, rv);
+
+  // We now have a body, load any localizations from the head.
+  OwnerDoc()->LoadDocumentL10n();
+
   return mAttrsAndChildren.ForceMapped(this, OwnerDoc());
 }
 
 nsresult
 HTMLBodyElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
                               const nsAttrValue* aValue,
                               const nsAttrValue* aOldValue,
                               nsIPrincipal* aSubjectPrincipal,
--- a/dom/html/HTMLLinkElement.cpp
+++ b/dom/html/HTMLLinkElement.cpp
@@ -144,16 +144,23 @@ HTMLLinkElement::BindToTree(nsIDocument*
   if (IsInComposedDoc()) {
     TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
   }
 
   void (HTMLLinkElement::*update)() = &HTMLLinkElement::UpdateStyleSheetInternal;
   nsContentUtils::AddScriptRunner(
     NewRunnableMethod("dom::HTMLLinkElement::BindToTree", this, update));
 
+  if (aDocument) {
+    nsAutoString rel;
+    GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel);
+    if (rel.EqualsLiteral("localization"))
+      aDocument->LocalizationLinkAdded(this);
+  }
+
   CreateAndDispatchEvent(aDocument, NS_LITERAL_STRING("DOMLinkAdded"));
 
   return rv;
 }
 
 void
 HTMLLinkElement::LinkAdded()
 {
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -497,16 +497,20 @@ partial interface Document {
   readonly attribute FlashClassification documentFlashClassification;
 };
 
 // Allows about: pages to query aboutCapabilities
 partial interface Document {
   [Throws, Func="nsDocument::CallerIsTrustedAboutPage"] readonly attribute AboutCapabilities aboutCapabilities;
 };
 
+partial interface Document {
+  [Throws, Func="nsDocument::DocumentSupportsL10n"] readonly attribute DocumentL10n l10n;
+};
+
 Document implements XPathEvaluator;
 Document implements GlobalEventHandlers;
 Document implements DocumentAndElementEventHandlers;
 Document implements TouchEventHandlers;
 Document implements ParentNode;
 Document implements OnErrorEventHandlerForNodes;
 Document implements GeometryUtils;
 Document implements FontFaceSource;
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DocumentL10n.webidl
@@ -0,0 +1,20 @@
+/* -*- Mode: IDL; 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 L10nKey {
+  required DOMString id;
+
+  object? args = null;
+};
+
+[NoInterfaceObject]
+interface DocumentL10n {
+  [Throws] void setAttributes(Element aElement, DOMString aId, optional object aArgs);
+
+  [Throws] Promise<DOMString> formatValue(DOMString aId, optional object aArgs);
+
+  [Throws] Promise<sequence<DOMString>> formatValues(sequence<L10nKey> aKeys);
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -460,16 +460,17 @@ WEBIDL_FILES = [
     'DataTransferItemList.webidl',
     'DecoderDoctorNotification.webidl',
     'DedicatedWorkerGlobalScope.webidl',
     'DelayNode.webidl',
     'DeviceMotionEvent.webidl',
     'Directory.webidl',
     'Document.webidl',
     'DocumentFragment.webidl',
+    'DocumentL10n.webidl',
     'DocumentOrShadowRoot.webidl',
     'DocumentTimeline.webidl',
     'DocumentType.webidl',
     'DOMError.webidl',
     'DOMException.webidl',
     'DOMImplementation.webidl',
     'DOMMatrix.webidl',
     'DOMParser.webidl',
--- a/dom/xml/nsXMLContentSink.cpp
+++ b/dom/xml/nsXMLContentSink.cpp
@@ -542,16 +542,17 @@ nsXMLContentSink::CloseElement(nsIConten
   // properly (eg form state restoration).
   if ((nodeInfo->NamespaceID() == kNameSpaceID_XHTML &&
        (nodeInfo->NameAtom() == nsGkAtoms::select ||
         nodeInfo->NameAtom() == nsGkAtoms::textarea ||
         nodeInfo->NameAtom() == nsGkAtoms::video ||
         nodeInfo->NameAtom() == nsGkAtoms::audio ||
         nodeInfo->NameAtom() == nsGkAtoms::object))
       || nodeInfo->NameAtom() == nsGkAtoms::title
+      || nodeInfo->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)
       ) {
     aContent->DoneAddingChildren(HaveNotifiedForCurrentContent());
   }
 
   if (IsMonolithicContainer(nodeInfo)) {
     mInMonolithicContainer--;
   }
 
--- a/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -1447,16 +1447,18 @@ XULDocument::AddElementToDocumentPost(El
     if (aElement == GetRootElement()) {
         ResetDocumentDirection();
     }
 
     // We need to pay special attention to the keyset tag to set up a listener
     if (aElement->NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) {
         // Create our XUL key listener and hook it up.
         nsXBLService::AttachGlobalKeyHandler(aElement);
+    } else if (aElement->NodeInfo()->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)) {
+        aElement->DoneAddingChildren(true);
     }
 
     return NS_OK;
 }
 
 nsresult
 XULDocument::AddSubtreeToDocument(nsIContent* aContent)
 {
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -1269,16 +1269,24 @@ nsXULElement::DestroyContent()
             frameLoader->Destroy();
         }
         slots->mFrameLoaderOrOpener = nullptr;
     }
 
     nsStyledElement::DestroyContent();
 }
 
+void
+nsXULElement::DoneAddingChildren(bool aHaveNotified)
+{
+    // This is only called for the listset tag to let us initialize the
+    // localizations.
+    OwnerDoc()->LoadDocumentL10n();
+}
+
 #ifdef DEBUG
 void
 nsXULElement::List(FILE* out, int32_t aIndent) const
 {
     nsCString prefix("XUL");
     if (HasSlots()) {
       prefix.Append('*');
     }
--- a/dom/xul/nsXULElement.h
+++ b/dom/xul/nsXULElement.h
@@ -366,16 +366,17 @@ public:
     // nsIContent
     virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                                 nsIContent* aBindingParent,
                                 bool aCompileEventHandlers) override;
     virtual void UnbindFromTree(bool aDeep, bool aNullParent) override;
     virtual void RemoveChildAt_Deprecated(uint32_t aIndex, bool aNotify) override;
     virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
     virtual void DestroyContent() override;
+    virtual void DoneAddingChildren(bool aHaveNotified) override;
 
 #ifdef DEBUG
     virtual void List(FILE* out, int32_t aIndent) const override;
     virtual void DumpContent(FILE* out, int32_t aIndent,bool aDumpAll) const override
     {
     }
 #endif
 
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -519,16 +519,21 @@ class DOMLocalization extends Localizati
   }
 
   /**
    * Translate all roots associated with this `DOMLocalization`.
    *
    * @returns {Promise}
    */
   translateRoots() {
+    // Bail out early if there are no registered translations.
+    if (this.resourceIds.length == 0) {
+      return Promise.resolve();
+    }
+
     const roots = Array.from(this.roots);
     return Promise.all(
       roots.map(root => this.translateFragment(root))
     );
   }
 
   /**
    * Pauses the `MutationObserver`.
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "mozilla/dom/DocumentL10n.h"
+#include "mozilla/dom/DocumentL10nBinding.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/Promise.h"
+#include "nsISupports.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DocumentL10n, mDocument, mHelper)
+
+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(nsISupports)
+NS_INTERFACE_MAP_END
+
+DocumentL10n::DocumentL10n(nsIDocument* aDocument)
+  : mDocument(aDocument)
+  , mHelper(nullptr)
+  , mLoading(true)
+{
+}
+
+DocumentL10n::~DocumentL10n()
+{
+  mHelper = nullptr;
+}
+
+bool
+DocumentL10n::EnsureHelper()
+{
+  if (mHelper) {
+    return true;
+  }
+
+  nsresult rv;
+  nsCOMPtr<mozIDocumentL10nHelper> helper = do_CreateInstance("@mozilla.org/intl/documentl10nhelper;1", &rv);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  rv = helper->Init(mDocument, mLoading);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  mHelper = helper;
+
+  if (mPendingResources.Length()) {
+    rv = PassPendingResources();
+    NS_ENSURE_SUCCESS(rv, false);
+  }
+
+  return true;
+}
+
+nsresult
+DocumentL10n::PassPendingResources()
+{
+  nsresult rv = mHelper->AddResourceIds(reinterpret_cast<const char16_t**>(mPendingResources.Elements()), mPendingResources.Length());
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mPendingResources.Clear();
+  return NS_OK;
+}
+
+void
+DocumentL10n::NotifyLoaded()
+{
+  mLoading = false;
+}
+
+void
+DocumentL10n::LoadLocalizations()
+{
+  if (mPendingResources.Length()) {
+    EnsureHelper();
+  }
+}
+
+JSObject*
+DocumentL10n::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return DocumentL10nBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+DocumentL10n::AddResourceId(const nsAString& aResourceId)
+{
+  mPendingResources.AppendElement(ToNewUnicode(aResourceId));
+
+  if (mHelper) {
+    PassPendingResources();
+  }
+}
+
+void
+DocumentL10n::SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+  if (!EnsureHelper()) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  JS::RootedValue args(cx);
+
+  if (aArgs.WasPassed()) {
+    args = JS::ObjectValue(*aArgs.Value());
+  } else {
+    args = JS::UndefinedValue();
+  }
+
+  nsresult rv = mHelper->SetAttributes(&aElement, aId, args);
+
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+  }
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+  if (!EnsureHelper()) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  JS::RootedValue args(cx);
+
+  if (aArgs.WasPassed()) {
+    args = JS::ObjectValue(*aArgs.Value());
+  } else {
+    args = JS::UndefinedValue();
+  }
+
+  nsCOMPtr<nsISupports> jsPromise;
+  nsresult rv = mHelper->FormatValue(aId, args, getter_AddRefs(jsPromise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = do_QueryObject(jsPromise);
+  if (!promise) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv)
+{
+  if (!EnsureHelper()) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  nsTArray<JS::HandleValue> jsKeys;
+  jsKeys.SetCapacity(aKeys.Length());
+
+  for (auto& key : aKeys) {
+    JS::RootedValue jsKey(cx);
+    if (!ToJSValue(cx, key, &jsKey)) {
+      aRv.Throw(NS_ERROR_UNEXPECTED);
+      return nullptr;
+    }
+
+    jsKeys.AppendElement(jsKey);
+  }
+
+  nsCOMPtr<nsISupports> jsPromise;
+  nsresult rv = mHelper->FormatValues(jsKeys.Elements(), jsKeys.Length(), getter_AddRefs(jsPromise));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = do_QueryObject(jsPromise);
+  if (!promise) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef mozilla_dom_DocumentL10n_h
+#define mozilla_dom_DocumentL10n_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsIDocument.h"
+#include "mozIDocumentL10nHelper.h"
+
+namespace mozilla {
+namespace dom {
+
+class Element;
+class Promise;
+struct L10nKey;
+
+class DocumentL10n final : public nsISupports,
+                           public nsWrapperCache
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DocumentL10n)
+
+public:
+  explicit DocumentL10n(nsIDocument* aDocument);
+
+protected:
+  virtual ~DocumentL10n();
+
+  nsCOMPtr<nsIDocument> mDocument;
+  nsCOMPtr<mozIDocumentL10nHelper> mHelper;
+  nsTArray<UniquePtr<char16_t>> mPendingResources;
+  bool mLoading;
+
+  bool EnsureHelper();
+  nsresult PassPendingResources();
+
+public:
+  void NotifyLoaded();
+  void LoadLocalizations();
+
+  nsIDocument* GetParentObject() const { return mDocument; };
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  void AddResourceId(const nsAString& aResourceId);
+
+  void SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+
+  already_AddRefed<Promise> FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+
+  already_AddRefed<Promise> FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_DocumentL10n_h
new file mode 100644
--- /dev/null
+++ b/intl/l10n/l10n.manifest
@@ -0,0 +1,2 @@
+component {29cc3895-8835-4c5b-b53a-0c0d1a458dee} mozDocumentL10nHelper.js
+contract @mozilla.org/intl/documentl10nhelper;1 {29cc3895-8835-4c5b-b53a-0c0d1a458dee}
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -6,16 +6,39 @@
 
 EXTRA_JS_MODULES += [
     'DOMLocalization.jsm',
     'L10nRegistry.jsm',
     'Localization.jsm',
     'MessageContext.jsm',
 ]
 
+XPIDL_SOURCES += [
+    'mozIDocumentL10nHelper.idl',
+]
+
+XPIDL_MODULE = 'locale'
+
+EXTRA_COMPONENTS += [
+    'l10n.manifest',
+    'mozDocumentL10nHelper.js',
+]
+
+EXPORTS.mozilla.dom += [
+    'DocumentL10n.h',
+]
+
+UNIFIED_SOURCES += [
+    'DocumentL10n.cpp',
+]
+
+LOCAL_INCLUDES += [
+    '/dom/base',
+]
+
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 SPHINX_TREES['l10n'] = 'docs'
 
new file mode 100644
--- /dev/null
+++ b/intl/l10n/mozDocumentL10nHelper.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { DOMLocalization } = ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {});
+
+function mozDocumentL10nHelper() {
+  XPCOMUtils.defineLazyGetter(this, "localization", () => {
+    return new DOMLocalization(this.document.defaultView, []);
+  });
+}
+
+mozDocumentL10nHelper.prototype = {
+  document: null,
+
+  init(document, isLoading) {
+    this.document = document;
+
+    if (!isLoading) {
+      this.translateDocument();
+    } else {
+      let event = document.contentType === "application/vnd.mozilla.xul+xml" ?
+                  "MozBeforeInitialXULLayout" : "readystate";
+
+      document.addEventListener(event, () => {
+        this.translateDocument();
+      }, { once: true });
+    }
+  },
+
+  translateDocument() {
+    this.localization.registerObservers();
+    this.localization.connectRoot(this.document.documentElement);
+    this.localization.translateRoots();
+  },
+
+  addResourceIds(resourceIds) {
+    this.localization.addResourceIds(resourceIds);
+    this.localization.ctxs.touchNext(2);
+  },
+
+  setAttributes(element, id, args) {
+    return this.localization.setAttributes(element, id, args);
+  },
+
+  formatValues(keys, length) {
+    return this.localization.formatValues(keys);
+  },
+
+  formatValue(id, args) {
+    return this.localization.formatValue(id, args);
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.mozIDocumentL10nHelper]),
+  classID: Components.ID("{29cc3895-8835-4c5b-b53a-0c0d1a458dee}"),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([mozDocumentL10nHelper]);
new file mode 100644
--- /dev/null
+++ b/intl/l10n/mozIDocumentL10nHelper.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+webidl Document;
+webidl Element;
+
+[scriptable, uuid(7c468500-541f-4fe0-98c9-92a53b63ec8d)]
+interface mozIDocumentL10nHelper : nsISupports
+{
+  /**
+   * Initializes the DOMLocalization object for the given document.
+   *
+   * @param  document
+   *         The document to monitor for l10n.
+   */
+  void init(in Document document, in boolean isLoading);
+
+  void addResourceIds([array, size_is(aLength)] in wstring aResourceIds, in unsigned long aLength);
+
+  void setAttributes(in Element aElement, in DOMString aId, [optional] in jsval aArgs);
+
+  nsISupports formatValue(in DOMString aId, [optional] in jsval aArgs);
+
+  nsISupports formatValues([array, size_is(aLength)] in jsval aKeys, in unsigned long aLength);
+};
--- a/intl/l10n/test/dom/test_domloc.xul
+++ b/intl/l10n/test/dom/test_domloc.xul
@@ -29,17 +29,17 @@ new-tab
     yield mc;
   }
 
   SimpleTest.waitForExplicitFinish();
 
 
   const domLoc = new DOMLocalization(
     window,
-    [],
+    ["dummy"],
     generateMessages
   );
 
   async function foo() {
     domLoc.connectRoot(document);
     await domLoc.translateRoots();
 
     is(document.getElementById('file-menu').getAttribute('label'), 'File');
--- a/intl/l10n/test/dom/test_domloc_mutations.html
+++ b/intl/l10n/test/dom/test_domloc_mutations.html
@@ -19,17 +19,17 @@
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
-      [],
+      ["dummy"],
       mockGenerateMessages
     );
 
     const h1 = document.querySelectorAll("h1")[0];
 
     domLoc.connectRoot(document.body);
 
     await domLoc.translateRoots();
--- a/intl/l10n/test/dom/test_domloc_translateRoots.html
+++ b/intl/l10n/test/dom/test_domloc_translateRoots.html
@@ -19,17 +19,17 @@
     yield mc;
   }
 
   window.onload = async function() {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
-      [],
+      ["dummy"],
       mockGenerateMessages
     );
 
     const frag1 = document.querySelectorAll("div")[0];
     const frag2 = document.querySelectorAll("div")[1];
     const h1 = document.querySelectorAll("h1")[0];
     const h2 = document.querySelectorAll("h2")[0];
 
--- a/xpcom/ds/nsGkAtomList.h
+++ b/xpcom/ds/nsGkAtomList.h
@@ -626,16 +626,17 @@ GK_ATOM(leftmargin, "leftmargin")
 GK_ATOM(leftpadding, "leftpadding")
 GK_ATOM(legend, "legend")
 GK_ATOM(length, "length")
 GK_ATOM(letterValue, "letter-value")
 GK_ATOM(level, "level")
 GK_ATOM(li, "li")
 GK_ATOM(line, "line")
 GK_ATOM(link, "link")
+GK_ATOM(linkset, "linkset")
 //GK_ATOM(list, "list")  # "list" is present below
 GK_ATOM(listbox, "listbox")
 GK_ATOM(listboxbody, "listboxbody")
 GK_ATOM(listcell, "listcell")
 GK_ATOM(listcol, "listcol")
 GK_ATOM(listcols, "listcols")
 GK_ATOM(listener, "listener")
 GK_ATOM(listhead, "listhead")