Bug 1455649 - Implement DocumentLocalization XPIDL API. draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Tue, 22 May 2018 10:50:54 -0700
changeset 800077 366f3cb06f6447bc0fdd83b50f74b879a937b1a9
parent 798691 d36cd8bdbc5c0df1d1d7a167f5fedb95c3a3648e
child 800078 4916a7ff2afb87fd966bbf270265789e21d38ffb
push id111260
push userbmo:gandalf@aviary.pl
push dateFri, 25 May 2018 20:47:52 +0000
bugs1455649
milestone62.0a1
Bug 1455649 - Implement DocumentLocalization XPIDL API. MozReview-Commit-ID: A4Mf5nY5MKb
browser/installer/package-manifest.in
intl/l10n/DOMLocalization.jsm
intl/l10n/Localization.jsm
intl/l10n/moz.build
intl/l10n/mozDocumentLocalization.js
intl/l10n/mozDocumentLocalization.manifest
intl/l10n/mozIDocumentLocalization.idl
intl/l10n/test/test_documentlocalization.js
intl/l10n/test/xpcshell.ini
--- 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/mozDocumentLocalization.js
+@RESPATH@/components/mozDocumentLocalization.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/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -545,16 +545,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`.
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -238,16 +238,23 @@ class Localization {
   /**
    * Register weak observers on events that will trigger cache invalidation
    */
   registerObservers() {
     Services.obs.addObserver(this, "intl:app-locales-changed", true);
   }
 
   /**
+   * Unregister observers.
+   */
+  unregisterObservers() {
+    Services.obs.removeObserver(this, "intl:app-locales-changed");
+  }
+
+  /**
    * Default observer handler method.
    *
    * @param {String} subject
    * @param {String} topic
    * @param {Object} data
    */
   observe(subject, topic, data) {
     switch (topic) {
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -6,16 +6,27 @@
 
 EXTRA_JS_MODULES += [
     'DOMLocalization.jsm',
     'L10nRegistry.jsm',
     'Localization.jsm',
     'MessageContext.jsm',
 ]
 
+XPIDL_SOURCES += [
+    'mozIDocumentLocalization.idl',
+]
+
+XPIDL_MODULE = 'locale'
+
+EXTRA_COMPONENTS += [
+    'mozDocumentLocalization.js',
+    'mozDocumentLocalization.manifest',
+]
+
 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/mozDocumentLocalization.js
@@ -0,0 +1,121 @@
+/* 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", {});
+const { PromiseUtils } = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm", {});
+
+class mozDocumentLocalization {
+  constructor() {
+    this._resourceIds = new Set();
+
+    const deferredL10nContainerParsed = PromiseUtils.defer();
+    const deferredDOMParsed = PromiseUtils.defer();
+
+    this._localization = deferredL10nContainerParsed.promise.then(() => {
+      const l10n = new DOMLocalization(this._document.defaultView, Array.from(this._resourceIds));
+      l10n.ctxs.touchNext(2);
+      this._resolveLocalization = null;
+      return l10n;
+    });
+
+    this._resolveLocalization = deferredL10nContainerParsed.resolve;
+
+    this.ready = deferredDOMParsed.promise.then(async () => {
+      if (this._resourceIds.size > 0) {
+        const l10n = await this._localization;
+        l10n.registerObservers();
+        l10n.connectRoot(this._document.documentElement);
+        await l10n.translateRoots();
+      }
+    });
+    this._resolveReady = deferredDOMParsed.resolve;
+  }
+
+  init(document, isL10nContainerParsed, isDOMParsed) {
+    console.log('mozDocumentLocalization::init');
+    this._document = document;
+    if (isL10nContainerParsed) {
+      console.log('mozDocumentLocalization::init l10nContainerParsed');
+      this.onL10nResourceContainerParsed();
+    }
+    if (isDOMParsed) {
+      console.log('mozDocumentLocalization::init DOMParsed');
+      this.onDOMParsed();
+    }
+  }
+
+  onL10nResourceContainerParsed() {
+    console.log('mozDocumentLocalization::onL10nResourceContainerParsed');
+    this._resolveLocalization();
+  }
+
+  async onDOMParsed() {
+    console.log('mozDocumentLocalization::onDOMParsed');
+    this._resolveReady();
+  }
+
+  async addResourceId(resourceId) {
+    console.log(`mozDocumentLocalization::addResourceId for "${resourceId}"`)
+    if (this._resolveLocalization) {
+      console.log(`mozDocumentLocalization::addResourceId resolveLocalization is pending`);
+      this._resourceIds.add(resourceId);
+    } else {
+      const l10n = await this._localization;
+      l10n.addResourceIds([resourceId]);
+      console.log(`mozDocumentLocalization::addResourceId has resources: ${this._resourceIds.size}`);
+      if (this._resourceIds.size === 0) {
+        console.log(`mozDocumentLocalization::addResourceId registeringObservers`);
+        l10n.registerObservers();
+        l10n.connectRoot(this._document.documentElement);
+        await l10n.translateRoots();
+      }
+      this._resourceIds.add(resourceId);
+    }
+  }
+
+  async removeResourceId(resourceId) {
+    const l10n = await this._localization;
+    l10n.removeResourceIds([resourceId]);
+    this._resourceIds.delete(resourceId);
+    if (this._resourceIds.size === 0) {
+      l10n.unregisterObservers();
+      l10n.disconnectRoot(this._document.documentElement);
+    }
+  }
+
+  setAttributes(element, id, args) {
+    element.setAttribute("data-l10n-id", id);
+    if (args) {
+      element.setAttribute("data-l10n-args", JSON.stringify(args));
+    } else {
+      element.removeAttribute("data-l10n-args");
+    }
+    return element;
+  }
+
+  getAttributes(element) {
+    return {
+      id: element.getAttribute("data-l10n-id"),
+      args: JSON.parse(element.getAttribute("data-l10n-args") || null)
+    };
+  }
+
+  async formatValues(keys, length) {
+    let l10n = await this._localization;
+    return l10n.formatValues(keys);
+  }
+
+  async formatValue(id, args) {
+    let l10n = await this._localization;
+    return l10n.formatValue(id, args);
+  }
+}
+
+mozDocumentLocalization.prototype.classID =
+  Components.ID("{29cc3895-8835-4c5b-b53a-0c0d1a458dee}");
+mozDocumentLocalization.prototype.QueryInterface =
+  ChromeUtils.generateQI([Ci.mozIDocumentLocalization]);
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([mozDocumentLocalization]);
new file mode 100644
--- /dev/null
+++ b/intl/l10n/mozDocumentLocalization.manifest
@@ -0,0 +1,2 @@
+component {29cc3895-8835-4c5b-b53a-0c0d1a458dee} mozDocumentLocalization.js
+contract @mozilla.org/intl/documentlocalization;1 {29cc3895-8835-4c5b-b53a-0c0d1a458dee}
new file mode 100644
--- /dev/null
+++ b/intl/l10n/mozIDocumentLocalization.idl
@@ -0,0 +1,74 @@
+/* -*- 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;
+
+/**
+ * An API for managing localization state of a single document using Fluent.
+ *
+ * The API is called by the DocumentL10n API and internally uses
+ * DOMLocalization class to handle localization state of the document.
+ *
+ * The DocumentLocalization object has two states:
+ *   * L10nContainerParsed
+ *         This state should be resolved after the document's l10n resources
+ *         container has been consumed by the parser.
+ *         For HTML this will mean that the `<head/>` element has been parsed,
+ *         while for XUL it will be after the first `<linkset/>` has been
+ *         parsed or after the end of the document has been reached.
+ *
+ *         This state is used to inform the DocumentLocalization on when to
+ *         kick off initial localization context I/O.
+ *
+ *   * DOMParsed
+ *         This state should be resolved after the document's DOM has been parsed.
+ *         If the document has l10n resources registered, the DocumentLocalization
+ *         will initialize even and Mutation Observer on the document and start
+ *         maintaining its translation state.
+ *
+ * Both states will be passed to the `init` method and if either of them is
+ * initialized as `false`, then the APIs methods will be pending the resolution
+ * of the corresponding `onL10nResourceContainerParsed` and `onDOMParsed` methods.
+ */
+[scriptable, uuid(7c468500-541f-4fe0-98c9-92a53b63ec8d)]
+interface mozIDocumentLocalization : nsISupports
+{
+  /**
+   * Initialization should provide the initial state for the `L10NContainerParsed` and
+   * `DOMParsed`. It's either resolved at the point of initialization, or in progress.
+   * If the API is initialized while parsing is in progress, its methods will pend
+   * the `onL10nResourceContainerParsed` and `onDOMParsed` methods to be called.
+   */
+  void init(in Document document, in boolean isL10nContainerParsed, in boolean isDOMParsed);
+  void onL10nResourceContainerParsed();
+  void onDOMParsed();
+
+  /**
+   * A promise to be resolved when the DocumentLocalization is fully initialized.
+   * This means that either l10n resources were registered, loaded and the
+   * initial localization was performed, or no localization resources were
+   * registered.
+   */
+  readonly attribute Promise ready;
+
+  /**
+   * Below methods are exposing `DOMLocalization` API for the document.
+   */
+  void setAttributes(in Element aElement, in DOMString aId, [optional] in jsval aArgs);
+  jsval getAttributes(in Element aElement);
+
+  Promise translateFragment(in Element aElement);
+  Promise translateElements([array, size_is(aLength)] in Element aElements, in unsigned long aLength);
+
+  void addResourceId(in DOMString aResource);
+  void removeResourceId(in DOMString aResource);
+
+  Promise formatMessages([array, size_is(aLength)] in jsval aKeys, in unsigned long aLength);
+  Promise formatValues([array, size_is(aLength)] in jsval aKeys, in unsigned long aLength);
+  Promise formatValue(in DOMString aId, [optional] in jsval aArgs);
+};
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/test_documentlocalization.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const documentLocalization =
+  Cc["@mozilla.org/intl/documentlocalization;1"].createInstance(
+    Ci.mozIDocumentLocalization);
+
+add_task(function test_methods_presence() {
+  equal(typeof documentLocalization.setDocument, "function");
+  equal(typeof documentLocalization.init, "function");
+  equal(typeof documentLocalization.onDOMReady, "function");
+  equal(typeof documentLocalization.setAttributes, "function");
+  equal(typeof documentLocalization.getAttributes, "function");
+  equal(typeof documentLocalization.translateElements, "function");
+  equal(typeof documentLocalization.translateFragment, "function");
+  equal(typeof documentLocalization.addResourceId, "function");
+  equal(typeof documentLocalization.removeResourceId, "function");
+  equal(typeof documentLocalization.formatMessages, "function");
+  equal(typeof documentLocalization.formatValues, "function");
+  equal(typeof documentLocalization.formatValue, "function");
+  equal(typeof documentLocalization.getReady, "function");
+});
--- a/intl/l10n/test/xpcshell.ini
+++ b/intl/l10n/test/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head =
 
+[test_documentlocalization.js]
 [test_domlocalization.js]
 [test_l10nregistry.js]
 [test_localization.js]
 [test_messagecontext.js]