Bug 1455649 - Implement DocumentLocalization XPIDL API.
MozReview-Commit-ID: A4Mf5nY5MKb
--- 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]