# HG changeset patch # User Zibi Braniecki # Date 1531270142 25200 # Node ID 03a018872c480ac6a43675a88c58439fdaf808e6 # Parent 8415c758679e3ccf26038cec94b55e24eefa250c Bug 1474786 - Update Fluent to master. r=stas MozReview-Commit-ID: 5PMKF46TPVM diff --git a/intl/l10n/DOMLocalization.jsm b/intl/l10n/DOMLocalization.jsm --- a/intl/l10n/DOMLocalization.jsm +++ b/intl/l10n/DOMLocalization.jsm @@ -11,17 +11,17 @@ * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -/* fluent-dom@0.2.0 */ +/* fluent-dom@aa95b1f (July 10, 2018) */ const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); // Match the opening angle bracket (<) in HTML tags, and HTML entities like // &, &, &. const reOverlay = /<|&#?\w+;/; @@ -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} 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,28 @@ 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 +541,30 @@ 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; + this.pendingrAF = null; + this.pendingElements.clear(); + return true; + } + + // Resume observing all other roots. this.resumeObserving(); - - return this.roots.size === 0; + return false; } /** * Translate all roots associated with this `DOMLocalization`. * * @returns {Promise} */ translateRoots() { @@ -557,26 +575,34 @@ 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`. * diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm --- a/intl/l10n/Localization.jsm +++ b/intl/l10n/Localization.jsm @@ -11,17 +11,17 @@ * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -/* fluent-dom@0.2.0 */ +/* fluent-dom@aa95b1f (July 10, 2018) */ /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ /* global console */ const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {}); const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry.jsm", {}); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {}); const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {}); @@ -111,31 +111,33 @@ function defaultGenerateMessages(resourc class Localization { /** * @param {Array} 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)); } 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. @@ -289,16 +291,17 @@ 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 = XPCOMUtils.generateQI([ Ci.nsISupportsWeakReference ]); /** diff --git a/intl/l10n/MessageContext.jsm b/intl/l10n/MessageContext.jsm --- a/intl/l10n/MessageContext.jsm +++ b/intl/l10n/MessageContext.jsm @@ -11,17 +11,17 @@ * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -/* fluent@0.6.3 */ +/* fluent@aa95b1f (July 10, 2018) */ /* eslint no-magic-numbers: [0] */ const MAX_PLACEABLES = 100; const entryIdentifierRe = /-?[a-zA-Z][a-zA-Z0-9_-]*/y; const identifierRe = /[a-zA-Z][a-zA-Z0-9_-]*/y; const functionIdentifierRe = /^[A-Z][A-Z_?-]*$/; @@ -1666,16 +1666,32 @@ function Pattern(env, ptn) { function resolve(ctx, args, message, errors = []) { const env = { ctx, args, errors, dirty: new WeakSet() }; return Type(env, message).toString(ctx); } /** + * Fluent Resource is a structure storing a map + * of localization entries. + */ +class FluentResource extends Map { + constructor(entries, errors = []) { + super(entries); + this.errors = errors; + } + + static fromString(source) { + const [entries, errors] = parse(source); + return new FluentResource(Object.entries(entries), errors); + } +} + +/** * Message contexts are single-language stores of translations. They are * responsible for parsing translation resources in the Fluent syntax and can * format translation units (entities) to strings. * * Always use `MessageContext.format` to retrieve translation units from * a context. Translations can contain references to other entities or * external arguments, conditional logic in form of select expressions, traits * which describe their grammatical features, and can use Fluent builtins which @@ -1707,21 +1723,27 @@ class MessageContext { * Available options: * * - `functions` - an object of additional functions available to * translations as builtins. * * - `useIsolating` - boolean specifying whether to use Unicode isolation * marks (FSI, PDI) for bidi interpolations. * + * - `transform` - a function used to transform string parts of patterns. + * * @param {string|Array} locales - Locale or locales of the context * @param {Object} [options] * @returns {MessageContext} */ - constructor(locales, { functions = {}, useIsolating = true, transform = v => v } = {}) { + constructor(locales, { + functions = {}, + useIsolating = true, + transform = v => v + } = {}) { this.locales = Array.isArray(locales) ? locales : [locales]; this._terms = new Map(); this._messages = new Map(); this._functions = functions; this._useIsolating = useIsolating; this._transform = transform; this._intls = new WeakMap(); @@ -1773,32 +1795,55 @@ class MessageContext { * * Parsed entities should be formatted with the `format` method in case they * contain logic (references, select expressions etc.). * * @param {string} source - Text resource with translations. * @returns {Array} */ addMessages(source) { - const [entries, errors] = parse(source); - for (const id in entries) { + const res = FluentResource.fromString(source); + return this.addResource(res); + } + + /** + * Add a translation resource to the context. + * + * The translation resource must be a proper FluentResource + * parsed by `MessageContext.parseResource`. + * + * let res = MessageContext.parseResource("foo = Foo"); + * ctx.addResource(res); + * ctx.getMessage('foo'); + * + * // Returns a raw representation of the 'foo' message. + * + * Parsed entities should be formatted with the `format` method in case they + * contain logic (references, select expressions etc.). + * + * @param {FluentResource} res - FluentResource object. + * @returns {Array} + */ + addResource(res) { + const errors = res.errors.slice(); + for (const [id, value] of res) { if (id.startsWith("-")) { // Identifiers starting with a dash (-) define terms. Terms are private // and cannot be retrieved from MessageContext. if (this._terms.has(id)) { errors.push(`Attempt to override an existing term: "${id}"`); continue; } - this._terms.set(id, entries[id]); + this._terms.set(id, value); } else { if (this._messages.has(id)) { errors.push(`Attempt to override an existing message: "${id}"`); continue; } - this._messages.set(id, entries[id]); + this._messages.set(id, value); } } return errors; } /** * Format a message to a string or null. diff --git a/intl/l10n/l10n.js b/intl/l10n/l10n.js --- a/intl/l10n/l10n.js +++ b/intl/l10n/l10n.js @@ -48,17 +48,17 @@ function getResourceLinks(elem) { return Array.from(elem.querySelectorAll('link[rel="localization"]')).map( el => el.getAttribute("href") ); } const resourceIds = getResourceLinks(document.head || document); - document.l10n = new DOMLocalization(window, resourceIds); + document.l10n = new DOMLocalization(resourceIds); // Trigger the first two contexts to be loaded eagerly. document.l10n.ctxs.touchNext(2); document.l10n.ready = documentReady(() => { document.l10n.registerObservers(); window.addEventListener("unload", () => { document.l10n.unregisterObservers(); diff --git a/intl/l10n/test/dom/test_domloc.xul b/intl/l10n/test/dom/test_domloc.xul --- a/intl/l10n/test/dom/test_domloc.xul +++ b/intl/l10n/test/dom/test_domloc.xul @@ -28,17 +28,16 @@ new-tab `); yield mc; } SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], generateMessages ); async function foo() { domLoc.connectRoot(document); await domLoc.translateRoots(); diff --git a/intl/l10n/test/dom/test_domloc_attr_sanitized.html b/intl/l10n/test/dom/test_domloc_attr_sanitized.html --- a/intl/l10n/test/dom/test_domloc_attr_sanitized.html +++ b/intl/l10n/test/dom/test_domloc_attr_sanitized.html @@ -20,17 +20,16 @@ key1 = Value for Key 1 key2 = Value for Key 2. `); yield mc; } SimpleTest.waitForExplicitFinish(); addLoadEvent(async () => { const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body); const elem1 = document.querySelector("#elem1"); const elem2 = document.querySelector("#elem2"); diff --git a/intl/l10n/test/dom/test_domloc_connectRoot.html b/intl/l10n/test/dom/test_domloc_connectRoot.html --- a/intl/l10n/test/dom/test_domloc_connectRoot.html +++ b/intl/l10n/test/dom/test_domloc_connectRoot.html @@ -12,17 +12,16 @@ async function* mockGenerateMessages(locales, resourceIds) { } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const frag = document.querySelectorAll("div")[0]; domLoc.connectRoot(frag); diff --git a/intl/l10n/test/dom/test_domloc_disconnectRoot.html b/intl/l10n/test/dom/test_domloc_disconnectRoot.html --- a/intl/l10n/test/dom/test_domloc_disconnectRoot.html +++ b/intl/l10n/test/dom/test_domloc_disconnectRoot.html @@ -12,17 +12,16 @@ async function* mockGenerateMessages(locales, resourceIds) { } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const frag = document.querySelectorAll("div")[0]; domLoc.connectRoot(frag); is(domLoc.roots.has(frag), true); diff --git a/intl/l10n/test/dom/test_domloc_getAttributes.html b/intl/l10n/test/dom/test_domloc_getAttributes.html --- a/intl/l10n/test/dom/test_domloc_getAttributes.html +++ b/intl/l10n/test/dom/test_domloc_getAttributes.html @@ -11,17 +11,16 @@ ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {}); async function* mockGenerateMessages(locales, resourceIds) {} window.onload = function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const p1 = document.querySelectorAll("p")[0]; const p2 = document.querySelectorAll("p")[1]; const p3 = document.querySelectorAll("p")[2]; const attrs1 = domLoc.getAttributes(p1); diff --git a/intl/l10n/test/dom/test_domloc_mutations.html b/intl/l10n/test/dom/test_domloc_mutations.html --- a/intl/l10n/test/dom/test_domloc_mutations.html +++ b/intl/l10n/test/dom/test_domloc_mutations.html @@ -18,17 +18,16 @@ mc.addMessages("title2 = Hello Another World"); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const h1 = document.querySelectorAll("h1")[0]; domLoc.connectRoot(document.body); diff --git a/intl/l10n/test/dom/test_domloc_overlay.html b/intl/l10n/test/dom/test_domloc_overlay.html --- a/intl/l10n/test/dom/test_domloc_overlay.html +++ b/intl/l10n/test/dom/test_domloc_overlay.html @@ -18,17 +18,16 @@ mc.addMessages(`title2 = This is a link!`); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const p1 = document.querySelectorAll("p")[0]; const p2 = document.querySelectorAll("p")[1]; const a = p2.querySelector("a"); a.addEventListener("click", function() { diff --git a/intl/l10n/test/dom/test_domloc_overlay_missing_all.html b/intl/l10n/test/dom/test_domloc_overlay_missing_all.html --- a/intl/l10n/test/dom/test_domloc_overlay_missing_all.html +++ b/intl/l10n/test/dom/test_domloc_overlay_missing_all.html @@ -17,17 +17,16 @@ // No translations! yield mc; } SimpleTest.waitForExplicitFinish(); addLoadEvent(async () => { const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body).then(() => { ok(false, "Expected translateFragment to throw on missing l10n-id"); }, () => { ok(true, "Expected translateFragment to throw on missing l10n-id"); diff --git a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html --- a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html +++ b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html @@ -17,17 +17,16 @@ mc.addMessages(`title = Visit Mozilla or Firefox website!`); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body); const p1 = document.querySelectorAll("p")[0]; const linkList = p1.querySelectorAll("a"); diff --git a/intl/l10n/test/dom/test_domloc_overlay_repeated.html b/intl/l10n/test/dom/test_domloc_overlay_repeated.html --- a/intl/l10n/test/dom/test_domloc_overlay_repeated.html +++ b/intl/l10n/test/dom/test_domloc_overlay_repeated.html @@ -17,17 +17,16 @@ mc.addMessages(`title = Visit Mozilla or Firefox website!`); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body); const p1 = document.querySelectorAll("p")[0]; const linkList = p1.querySelectorAll("a"); diff --git a/intl/l10n/test/dom/test_domloc_overlay_sanitized.html b/intl/l10n/test/dom/test_domloc_overlay_sanitized.html --- a/intl/l10n/test/dom/test_domloc_overlay_sanitized.html +++ b/intl/l10n/test/dom/test_domloc_overlay_sanitized.html @@ -21,17 +21,16 @@ key1 = key2 = .href = https://pl.wikipedia.org `); yield mc; } async function test() { const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body); const key1Elem = document.querySelector("[data-l10n-id=key1]"); const key2Elem = document.querySelector("[data-l10n-id=key2]"); diff --git a/intl/l10n/test/dom/test_domloc_repeated_l10nid.html b/intl/l10n/test/dom/test_domloc_repeated_l10nid.html --- a/intl/l10n/test/dom/test_domloc_repeated_l10nid.html +++ b/intl/l10n/test/dom/test_domloc_repeated_l10nid.html @@ -20,17 +20,16 @@ key1 = Translation For Key 1 key2 = Visit this link. `); yield mc; } SimpleTest.waitForExplicitFinish(); addLoadEvent(async () => { const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); await domLoc.translateFragment(document.body); ok(document.querySelector("#elem1").textContent.includes("Key 1")); ok(document.querySelector("#elem2").textContent.includes("Key 1")); diff --git a/intl/l10n/test/dom/test_domloc_setAttributes.html b/intl/l10n/test/dom/test_domloc_setAttributes.html --- a/intl/l10n/test/dom/test_domloc_setAttributes.html +++ b/intl/l10n/test/dom/test_domloc_setAttributes.html @@ -11,17 +11,16 @@ ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {}); async function* mockGenerateMessages(locales, resourceIds) {} window.onload = function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const p1 = document.querySelectorAll("p")[0]; domLoc.setAttributes(p1, "title"); is(p1.getAttribute("data-l10n-id"), "title"); diff --git a/intl/l10n/test/dom/test_domloc_translateElements.html b/intl/l10n/test/dom/test_domloc_translateElements.html --- a/intl/l10n/test/dom/test_domloc_translateElements.html +++ b/intl/l10n/test/dom/test_domloc_translateElements.html @@ -18,17 +18,16 @@ mc.addMessages("link\n .title = Click me"); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const p1 = document.querySelectorAll("p")[0]; const link1 = document.querySelectorAll("a")[0]; await domLoc.translateElements([p1, link1]); diff --git a/intl/l10n/test/dom/test_domloc_translateFragment.html b/intl/l10n/test/dom/test_domloc_translateFragment.html --- a/intl/l10n/test/dom/test_domloc_translateFragment.html +++ b/intl/l10n/test/dom/test_domloc_translateFragment.html @@ -18,17 +18,16 @@ mc.addMessages("subtitle = Welcome to Fluent"); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const frag = document.querySelectorAll("div")[0]; const h1 = document.querySelectorAll("h1")[0]; const p1 = document.querySelectorAll("p")[0]; diff --git a/intl/l10n/test/dom/test_domloc_translateRoots.html b/intl/l10n/test/dom/test_domloc_translateRoots.html --- a/intl/l10n/test/dom/test_domloc_translateRoots.html +++ b/intl/l10n/test/dom/test_domloc_translateRoots.html @@ -18,17 +18,16 @@ mc.addMessages("title2 = Hello Another World"); yield mc; } window.onload = async function() { SimpleTest.waitForExplicitFinish(); const domLoc = new DOMLocalization( - window, [], mockGenerateMessages ); const frag1 = document.querySelectorAll("div")[0]; const frag2 = document.querySelectorAll("div")[1]; const h1 = document.querySelectorAll("h1")[0]; const h2 = document.querySelectorAll("h2")[0];