Bug 1455649 - DocumentL10n, part 3 - Plug DocumentL10n life cycle into DOM hooks. r=smaug
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 06 Sep 2018 18:28:40 -0700
changeset 435140 ad0728ae9f896a68b3f39702652ac669c541f61f
parent 435139 e522c31a0306a7afdc2ce8a587602343bcce5b43
child 435141 709a197ccc2e5959d14df8e049ce60da3c727f5a
push id107568
push userzbraniecki@mozilla.com
push dateFri, 07 Sep 2018 01:35:23 +0000
treeherdermozilla-inbound@709a197ccc2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1455649
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1455649 - DocumentL10n, part 3 - Plug DocumentL10n life cycle into DOM hooks. r=smaug
dom/base/nsDocument.cpp
dom/html/HTMLLinkElement.cpp
dom/html/HTMLSharedElement.cpp
dom/xml/nsXMLContentSink.cpp
dom/xul/XULDocument.cpp
intl/l10n/moz.build
intl/l10n/test/chrome.ini
intl/l10n/test/dom/test_docl10n.html
intl/l10n/test/dom/test_docl10n.xhtml
intl/l10n/test/dom/test_docl10n.xul
intl/l10n/test/dom/test_docl10n_ready_rejected.html
intl/l10n/test/dom/test_docl10n_unpriv_iframe.html
intl/l10n/test/mochitest.ini
intl/l10n/test/test_mozdomlocalization.js
xpcom/ds/StaticAtoms.py
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -8906,16 +8906,20 @@ nsIDocument::SetReadyStateInternal(Ready
         break;
     }
   }
   // At the time of loading start, we don't have timing object, record time.
   if (READYSTATE_LOADING == rs) {
     mLoadingTimeStamp = mozilla::TimeStamp::Now();
   }
 
+  if (READYSTATE_INTERACTIVE == rs) {
+    TriggerInitialDocumentTranslation();
+  }
+
   RecordNavigationTiming(rs);
 
   RefPtr<AsyncEventDispatcher> asyncDispatcher =
     new AsyncEventDispatcher(this,
                              NS_LITERAL_STRING("readystatechange"),
                              CanBubble::eNo,
                              ChromeOnlyDispatch::eNo);
   asyncDispatcher->RunDOMEventWhenSafe();
--- a/dom/html/HTMLLinkElement.cpp
+++ b/dom/html/HTMLLinkElement.cpp
@@ -139,16 +139,20 @@ HTMLLinkElement::BindToTree(nsIDocument*
     doc->RegisterPendingLinkUpdate(this);
     TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
   }
 
   void (HTMLLinkElement::*update)() = &HTMLLinkElement::UpdateStyleSheetInternal;
   nsContentUtils::AddScriptRunner(
     NewRunnableMethod("dom::HTMLLinkElement::BindToTree", this, update));
 
+  if (aDocument && this->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, eIgnoreCase)) {
+    aDocument->LocalizationLinkAdded(this);
+  }
+
   CreateAndDispatchEvent(aDocument, NS_LITERAL_STRING("DOMLinkAdded"));
 
   return rv;
 }
 
 void
 HTMLLinkElement::LinkAdded()
 {
@@ -175,16 +179,20 @@ HTMLLinkElement::UnbindFromTree(bool aDe
   // be under a different xml:base, so forget the cached state now.
   Link::ResetLinkState(false, Link::ElementHasHref());
 
   // If this is reinserted back into the document it will not be
   // from the parser.
   nsIDocument* oldDoc = GetUncomposedDoc();
   ShadowRoot* oldShadowRoot = GetContainingShadow();
 
+  if (oldDoc && this->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, eIgnoreCase)) {
+    oldDoc->LocalizationLinkRemoved(this);
+  }
+
   CreateAndDispatchEvent(oldDoc, NS_LITERAL_STRING("DOMLinkRemoved"));
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 
   Unused << UpdateStyleSheetInternal(oldDoc, oldShadowRoot);
 }
 
 bool
 HTMLLinkElement::ParseAttribute(int32_t aNamespaceID,
@@ -287,16 +295,46 @@ HTMLLinkElement::AfterSetAttr(int32_t aN
   }
 
   if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::href) {
     mTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
         this, aValue ? aValue->GetStringValue() : EmptyString(),
         aSubjectPrincipal);
   }
 
+  // If a link's `rel` attribute was changed from or to `localization`,
+  // update the list of localization links.
+  if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::rel) {
+    nsIDocument* doc = GetComposedDoc();
+    if (doc) {
+      if ((aValue && aValue->Equals(nsGkAtoms::localization, eIgnoreCase)) &&
+          (!aOldValue || !aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase))) {
+        doc->LocalizationLinkAdded(this);
+      } else if ((aOldValue && aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase)) &&
+                 (!aValue || !aValue->Equals(nsGkAtoms::localization, eIgnoreCase))) {
+        doc->LocalizationLinkRemoved(this);
+      }
+    }
+  }
+
+  // If the link has `rel=localization` and its `href` attribute is changed,
+  // update the list of localization links.
+  if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::href && 
+      AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, eIgnoreCase)) {
+    nsIDocument* doc = GetComposedDoc();
+    if (doc) {
+      if (aOldValue) {
+        doc->LocalizationLinkRemoved(this);
+      }
+      if (aValue) {
+        doc->LocalizationLinkAdded(this);
+      }
+    }
+  }
+
   if (aValue) {
     if (aNameSpaceID == kNameSpaceID_None &&
         (aName == nsGkAtoms::href ||
          aName == nsGkAtoms::rel ||
          aName == nsGkAtoms::title ||
          aName == nsGkAtoms::media ||
          aName == nsGkAtoms::type ||
          aName == nsGkAtoms::as ||
--- a/dom/html/HTMLSharedElement.cpp
+++ b/dom/html/HTMLSharedElement.cpp
@@ -56,16 +56,21 @@ HTMLSharedElement::GetHref(nsAString& aV
   uri->GetSpec(spec);
   CopyUTF8toUTF16(spec, aValue);
 }
 
 void
 HTMLSharedElement::DoneAddingChildren(bool aHaveNotified)
 {
   if (mNodeInfo->Equals(nsGkAtoms::head)) {
+    nsCOMPtr<nsIDocument> doc = GetUncomposedDoc();
+    if (doc) {
+      doc->OnL10nResourceContainerParsed();
+    }
+
     RefPtr<AsyncEventDispatcher> asyncDispatcher =
       new AsyncEventDispatcher(this,
                               NS_LITERAL_STRING("DOMHeadElementParsed"),
                               CanBubble::eYes,
                               ChromeOnlyDispatch::eYes);
     // Always run async in order to avoid running script when the content
     // sink isn't expecting it.
     asyncDispatcher->PostDOMEvent();
--- a/dom/xml/nsXMLContentSink.cpp
+++ b/dom/xml/nsXMLContentSink.cpp
@@ -572,16 +572,17 @@ nsXMLContentSink::CloseElement(nsIConten
 
   // Some HTML nodes need DoneAddingChildren() called to initialize
   // 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::head ||
         nodeInfo->NameAtom() == nsGkAtoms::object))
       || nodeInfo->NameAtom() == nsGkAtoms::title
       ) {
     aContent->DoneAddingChildren(HaveNotifiedForCurrentContent());
   }
 
   if (IsMonolithicContainer(nodeInfo)) {
     mInMonolithicContainer--;
--- a/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -71,16 +71,17 @@
 #include "nsIStyleSheetLinkingElement.h"
 #include "nsIObserverService.h"
 #include "nsNodeUtils.h"
 #include "nsIXULWindow.h"
 #include "nsXULPopupManager.h"
 #include "nsCCUncollectableMarker.h"
 #include "nsURILoader.h"
 #include "mozilla/BasicEvents.h"
+#include "mozilla/dom/DocumentL10n.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/NodeInfoInlines.h"
 #include "mozilla/dom/ProcessingInstruction.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/XULDocumentBinding.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/LoadInfo.h"
 #include "mozilla/Preferences.h"
@@ -1040,16 +1041,20 @@ 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->IsXULElement(nsGkAtoms::link)) {
+        LocalizationLinkAdded(aElement);
+    } else if (aElement->IsXULElement(nsGkAtoms::linkset)) {
+        OnL10nResourceContainerParsed();
     }
 
     return NS_OK;
 }
 
 nsresult
 XULDocument::AddSubtreeToDocument(nsIContent* aContent)
 {
@@ -1828,23 +1833,30 @@ XULDocument::DoneWalking()
         // the |if (!mDocumentLoaded)| check above and since
         // mInitialLayoutComplete will be false will follow the else branch
         // there too.  See the big comment there for how such reentry can
         // happen.
         mDocumentLoaded = true;
 
         NotifyPossibleTitleChange(false);
 
+        // For performance reasons, we want to trigger the DocumentL10n's `TriggerInitialDocumentTranslation` within the same
+        // microtask that will be created for a `MozBeforeInitialXULLayout`
+        // event listener.
+        AddEventListener(NS_LITERAL_STRING("MozBeforeInitialXULLayout"), mDocumentL10n, true, false);
+
         nsContentUtils::DispatchTrustedEvent(
             this,
             static_cast<nsIDocument*>(this),
             NS_LITERAL_STRING("MozBeforeInitialXULLayout"),
             CanBubble::eYes,
             Cancelable::eNo);
 
+        RemoveEventListener(NS_LITERAL_STRING("MozBeforeInitialXULLayout"), mDocumentL10n, true);
+
         // Before starting layout, check whether we're a toplevel chrome
         // window.  If we are, setup some state so that we don't have to restyle
         // the whole tree after StartLayout.
         if (nsCOMPtr<nsIXULWindow> win = GetXULWindowIfToplevelChrome()) {
             // We're the chrome document!
             win->BeforeStartLayout();
         }
 
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -31,15 +31,16 @@ UNIFIED_SOURCES += [
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 SPHINX_TREES['l10n'] = 'docs'
 
 FINAL_LIBRARY = 'xul'
--- a/intl/l10n/test/chrome.ini
+++ b/intl/l10n/test/chrome.ini
@@ -13,8 +13,13 @@
 [dom/test_domloc_overlay_missing_all.html]
 [dom/test_domloc_overlay_missing_children.html]
 [dom/test_domloc_overlay_sanitized.html]
 [dom/test_domloc.xul]
 
 [dom/test_mozdom_translateElements.html]
 [dom/test_mozdom_translateFragment.html]
 [dom/test_mozdom_translateRoots.html]
+
+[dom/test_docl10n.xul]
+[dom/test_docl10n.xhtml]
+[dom/test_docl10n.html]
+[dom/test_docl10n_ready_rejected.html]
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/dom/test_docl10n.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test DocumentL10n in HTML environment</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+  <script type="application/javascript">
+  "use strict";
+  SimpleTest.waitForExplicitFinish();
+
+  document.addEventListener("DOMContentLoaded", async function() {
+    await document.l10n.ready;
+
+    let desc = document.getElementById("main-desc");
+    is(desc.textContent.length > 0, true);
+
+    // Test for manual value formatting
+    let msg = await document.l10n.formatValue("id-heading");
+    is(msg.length > 0, true);
+
+    let label = document.getElementById("label1");
+    document.l10n.setAttributes(
+      label,
+      "date-crashed-heading",
+      {
+        name: "John"
+      }
+    );
+
+    // Test for l10n.getAttributes
+    let l10nArgs = document.l10n.getAttributes(label);
+    is(l10nArgs.id, "date-crashed-heading");
+    is(l10nArgs.args.name, "John");
+
+    let verifyL10n = () => {
+      if (label.textContent.length > 0) {
+        window.removeEventListener("MozAfterPaint", verifyL10n);
+        SimpleTest.finish();
+      }
+    };
+    window.addEventListener("MozAfterPaint", verifyL10n);
+  }, { once: true});
+  </script>
+</head>
+<body>
+  <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+  <p id="label1"></p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/dom/test_docl10n.xhtml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta charset="utf-8"></meta>
+  <title>Test DocumentL10n in HTML environment</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"></link>
+  <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+  <script type="application/javascript">
+  "use strict";
+  SimpleTest.waitForExplicitFinish();
+
+  document.addEventListener("DOMContentLoaded", async function() {
+    await document.l10n.ready;
+
+    let desc = document.getElementById("main-desc");
+    is(desc.textContent.length > 0, true);
+
+    // Test for manual value formatting
+    let msg = await document.l10n.formatValue("id-heading");
+    is(msg.length > 0, true);
+
+    let label = document.getElementById("label1");
+    document.l10n.setAttributes(
+      label,
+      "date-crashed-heading",
+      {
+        name: "John"
+      }
+    );
+
+    // Test for l10n.getAttributes
+    let l10nArgs = document.l10n.getAttributes(label);
+    is(l10nArgs.id, "date-crashed-heading");
+    is(l10nArgs.args.name, "John");
+
+    let verifyL10n = () => {
+      if (label.textContent.length > 0) {
+        window.removeEventListener("MozAfterPaint", verifyL10n);
+        SimpleTest.finish();
+      }
+    };
+    window.addEventListener("MozAfterPaint", verifyL10n);
+  }, { once: true});
+  </script>
+</head>
+<body>
+  <h1 id="main-desc" data-l10n-id="crash-reports-title"></h1>
+
+  <p id="label1" />
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/dom/test_docl10n.xul
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
+        title="Testing DocumentL10n in XUL environment">
+
+  <linkset>
+    <link rel="localization" href="crashreporter/aboutcrashes.ftl"/>
+  </linkset>
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript">
+  <![CDATA[
+  SimpleTest.waitForExplicitFinish();
+
+  document.addEventListener("DOMContentLoaded", async function() {
+    await document.l10n.ready;
+
+    let desc = document.getElementById("main-desc");
+    is(desc.textContent.length > 0, true);
+
+    // Test for manual value formatting
+    let msg = await document.l10n.formatValue("id-heading");
+    is(msg.length > 0, true);
+
+    let label = document.getElementById("label1");
+    document.l10n.setAttributes(
+      label,
+      "date-crashed-heading",
+      {
+        name: "John"
+      }
+    );
+
+    // Test for l10n.getAttributes
+    let l10nArgs = document.l10n.getAttributes(label);
+    is(l10nArgs.id, "date-crashed-heading");
+    is(l10nArgs.args.name, "John");
+
+    let verifyL10n = () => {
+      if (label.textContent.length > 0) {
+        window.removeEventListener("MozAfterPaint", verifyL10n);
+        SimpleTest.finish();
+      }
+    };
+    window.addEventListener("MozAfterPaint", verifyL10n);
+  }, { once: true});
+  ]]>
+  </script>
+
+  <description id="main-desc" data-l10n-id="crash-reports-title"/>
+
+  <label id="label1" />
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/dom/test_docl10n_ready_rejected.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test mozIDOMLocalization.ready rejected state</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <link rel="localization" href="path/to_non_existing.ftl"/>
+  <script type="application/javascript">
+  "use strict";
+  SimpleTest.waitForExplicitFinish();
+
+  document.addEventListener("DOMContentLoaded", async function() {
+      document.l10n.ready.then(() => {
+        is(1, 2, "the ready should not resolve");
+        SimpleTest.finish();
+      }, (err) => {
+        is(1, 1, "the ready should reject");
+        SimpleTest.finish();
+      });
+  });
+  </script>
+</head>
+<body>
+  <h1 data-l10n-id="non-existing-id"></h1>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/dom/test_docl10n_unpriv_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Ensure unprivilaged document cannot access document.l10n in an iframe</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="application/javascript">
+  "use strict";
+  SimpleTest.waitForExplicitFinish();
+
+  addLoadEvent(function() {
+    let frame = document.getElementById("frame");
+    let frame2 = document.getElementById("frame2");
+
+    is("l10n" in frame.contentDocument, false);
+    is("l10n" in frame2.contentDocument, false);
+  });
+  addLoadEvent(SimpleTest.finish);
+  </script>
+</head>
+<body>
+  <iframe id="frame" src="about:blank"></iframe>
+  <iframe id="frame2" src="about:crashes"></iframe>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/mochitest.ini
@@ -0,0 +1,1 @@
+[dom/test_docl10n_unpriv_iframe.html]
--- a/intl/l10n/test/test_mozdomlocalization.js
+++ b/intl/l10n/test/test_mozdomlocalization.js
@@ -68,17 +68,17 @@ add_task(async function test_format_mess
 
   let msgs = await domLocalization.formatMessages([{"id": "key"}], 1);
   equal(loadCounter, 1);
   equal(msgs.length, 1);
   equal(msgs[0].value, "[en] Value");
 });
 
 add_task(async function test_format_values() {
-  let msgs = await domLocalization.formatValues([{"id": "key"}], 1);
+  let msgs = await domLocalization.formatValues([{"id": "key"}]);
   equal(msgs.length, 1);
   equal(msgs[0], "[en] Value");
 });
 
 add_task(async function test_format_value() {
   let msg = await domLocalization.formatValue("key");
   equal(msg, "[en] Value");
 });
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -541,26 +541,28 @@ STATIC_ATOMS = [
     Atom("leftmargin", "leftmargin"),
     Atom("legend", "legend"),
     Atom("length", "length"),
     Atom("letterValue", "letter-value"),
     Atom("level", "level"),
     Atom("li", "li"),
     Atom("line", "line"),
     Atom("link", "link"),
+    Atom("linkset", "linkset"),
     # Atom("list", "list"),  # "list" is present below
     Atom("listbox", "listbox"),
     Atom("listener", "listener"),
     Atom("listheader", "listheader"),
     Atom("listing", "listing"),
     Atom("listitem", "listitem"),
     Atom("load", "load"),
     Atom("triggeringprincipal", "triggeringprincipal"),
     Atom("localedir", "localedir"),
     Atom("localName", "local-name"),
+    Atom("localization", "localization"),
     Atom("longdesc", "longdesc"),
     Atom("loop", "loop"),
     Atom("low", "low"),
     Atom("lowerAlpha", "lower-alpha"),
     Atom("lowerFirst", "lower-first"),
     Atom("lowerRoman", "lower-roman"),
     Atom("lowest", "lowest"),
     Atom("lowsrc", "lowsrc"),