Bug 1455649 - DocumentL10n, part 2 - Update FluentDOM to work with DocumentL10n. draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Fri, 22 Jun 2018 11:53:53 -0700
changeset 815116 93448ee31634580275cfcd744219f0805fcebc44
parent 815115 8ca755489d961db52e103fc3bc5119e1eae981b1
child 815117 5246ee946c259a116dfb097b6a3dda110a0432f3
push id115447
push userbmo:gandalf@aviary.pl
push dateFri, 06 Jul 2018 20:45:09 +0000
bugs1455649
milestone63.0a1
Bug 1455649 - DocumentL10n, part 2 - Update FluentDOM to work with DocumentL10n. In order to allow for Localization/DOMLocalization to be used as XPIDL classes, we need to make the constructor work without taking arguments. We also want to initialize the observers when we construct them, and return the remaining resourceIds to allow for unregistering of `document.l10n` when resourceIds reaches 0. For DOMLocalization, we'll now take the windowElement and construct the mutationObserver, only when the first root is connected. MozReview-Commit-ID: 6z6yJKmHTIH
intl/l10n/DOMLocalization.jsm
intl/l10n/Localization.jsm
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -398,35 +398,32 @@ const L10N_ELEMENT_QUERY = `[${L10NID_AT
  * formatting translations.
  *
  * It implements the fallback strategy in case of errors encountered during the
  * formatting of translations and methods for observing DOM
  * trees with a `MutationObserver`.
  */
 class DOMLocalization extends Localization {
   /**
-   * @param {Window}           windowElement
    * @param {Array<String>}    resourceIds      - List of resource IDs
    * @param {Function}         generateMessages - Function that returns a
    *                                              generator over MessageContexts
    * @returns {DOMLocalization}
    */
-  constructor(windowElement, resourceIds, generateMessages) {
+  constructor(resourceIds, generateMessages) {
     super(resourceIds, generateMessages);
 
     // A Set of DOM trees observed by the `MutationObserver`.
     this.roots = new Set();
     // requestAnimationFrame handler.
     this.pendingrAF = null;
     // list of elements pending for translation.
     this.pendingElements = new Set();
-    this.windowElement = windowElement;
-    this.mutationObserver = new windowElement.MutationObserver(
-      mutations => this.translateMutations(mutations)
-    );
+    this.windowElement = null;
+    this.mutationObserver = null;
 
     this.observerConfig = {
       attribute: true,
       characterData: false,
       childList: true,
       subtree: true,
       attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME]
     };
@@ -514,16 +511,27 @@ class DOMLocalization extends Localizati
     for (const root of this.roots) {
       if (root === newRoot ||
           root.contains(newRoot) ||
           newRoot.contains(root)) {
         throw new Error("Cannot add a root that overlaps with existing root.");
       }
     }
 
+    if (this.windowElement) {
+      if (this.windowElement !== newRoot.ownerGlobal) {
+        throw new Error("Cannot connect a root: DOMLocalization already has a root from a different window.");
+      }
+    } else {
+      this.windowElement = newRoot.ownerGlobal;
+      this.mutationObserver = new this.windowElement.MutationObserver(
+        mutations => this.translateMutations(mutations)
+      );
+    }
+
     this.roots.add(newRoot);
     this.mutationObserver.observe(newRoot, this.observerConfig);
   }
 
   /**
    * Remove `root` from the list of roots managed by this `DOMLocalization`.
    *
    * Additionally, if this `DOMLocalization` has an observer, stop observing
@@ -532,21 +540,28 @@ class DOMLocalization extends Localizati
    * Returns `true` if the root was the last one managed by this
    * `DOMLocalization`.
    *
    * @param   {Element} root - Root to disconnect.
    * @returns {boolean}
    */
   disconnectRoot(root) {
     this.roots.delete(root);
-    // Pause and resume the mutation observer to stop observing `root`.
+
+    // Pause the mutation observer to stop observing `root`.
     this.pauseObserving();
+
+    if (this.roots.size === 0) {
+      this.mutationObserver = null;
+      this.windowElement = null;
+      return true;
+    }
+
     this.resumeObserving();
-
-    return this.roots.size === 0;
+    return false;
   }
 
   /**
    * Translate all roots associated with this `DOMLocalization`.
    *
    * @returns {Promise}
    */
   translateRoots() {
@@ -557,26 +572,33 @@ class DOMLocalization extends Localizati
   }
 
   /**
    * Pauses the `MutationObserver`.
    *
    * @private
    */
   pauseObserving() {
+    if (!this.mutationObserver) {
+      return;
+    }
+
     this.translateMutations(this.mutationObserver.takeRecords());
     this.mutationObserver.disconnect();
   }
 
   /**
    * Resumes the `MutationObserver`.
    *
    * @private
    */
   resumeObserving() {
+    if (!this.mutationObserver) {
+      return;
+    }
     for (const root of this.roots) {
       this.mutationObserver.observe(root, this.observerConfig);
     }
   }
 
   /**
    * Translate mutations detected by the `MutationObserver`.
    *
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -104,31 +104,36 @@ function defaultGenerateMessages(resourc
 class Localization {
   /**
    * @param {Array<String>} resourceIds      - List of resource IDs
    * @param {Function}      generateMessages - Function that returns a
    *                                           generator over MessageContexts
    *
    * @returns {Localization}
    */
-  constructor(resourceIds, generateMessages = defaultGenerateMessages) {
+  constructor(resourceIds = [], generateMessages = defaultGenerateMessages) {
     this.resourceIds = resourceIds;
     this.generateMessages = generateMessages;
-    this.ctxs =
-      new CachedAsyncIterable(this.generateMessages(this.resourceIds));
+    if (resourceIds) {
+      this.ctxs =
+        new CachedAsyncIterable(this.generateMessages(this.resourceIds));
+    }
+    this.registerObservers();
   }
 
   addResourceIds(resourceIds) {
     this.resourceIds.push(...resourceIds);
     this.onChange();
+    return this.resourceIds.length;
   }
 
   removeResourceIds(resourceIds) {
     this.resourceIds = this.resourceIds.filter(r => !resourceIds.includes(r));
     this.onChange();
+    return this.resourceIds.length;
   }
 
   /**
    * Format translations and handle fallback if needed.
    *
    * Format translations for `keys` from `MessageContext` instances on this
    * DOMLocalization. In case of errors, fetch the next context in the
    * fallback chain.
@@ -237,18 +242,23 @@ class Localization {
     const [val] = await this.formatValues([{id, args}]);
     return val;
   }
 
   /**
    * Register weak observers on events that will trigger cache invalidation
    */
   registerObservers() {
-    Services.obs.addObserver(this, "intl:app-locales-changed", true);
-    Services.prefs.addObserver("intl.l10n.pseudo", this, true);
+    Services.obs.addObserver(this, "intl:app-locales-changed", false);
+    Services.prefs.addObserver("intl.l10n.pseudo", this, false);
+  }
+
+  unregisterObservers() {
+    Services.obs.removeObserver(this, "intl:app-locales-changed");
+    Services.prefs.removeObserver("intl.l10n.pseudo", this);
   }
 
   /**
    * Default observer handler method.
    *
    * @param {String} subject
    * @param {String} topic
    * @param {Object} data
@@ -272,23 +282,20 @@ class Localization {
 
   /**
    * This method should be called when there's a reason to believe
    * that language negotiation or available resources changed.
    */
   onChange() {
     this.ctxs =
       new CachedAsyncIterable(this.generateMessages(this.resourceIds));
+    this.ctxs.touchNext(2);
   }
 }
 
-Localization.prototype.QueryInterface = ChromeUtils.generateQI([
-  Ci.nsISupportsWeakReference
-]);
-
 /**
  * Format the value of a message into a string.
  *
  * This function is passed as a method to `keysFromContext` and resolve
  * a value of a single L10n Entity using provided `MessageContext`.
  *
  * If the function fails to retrieve the entity, it will return an ID of it.
  * If formatting fails, it will return a partially resolved entity.