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
--- 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.