diff --git a/intl/l10n/DOMLocalization.jsm b/intl/l10n/DOMLocalization.jsm index 3f2b676fab76..d3fb9b803833 100644 --- a/intl/l10n/DOMLocalization.jsm +++ b/intl/l10n/DOMLocalization.jsm @@ -16,7 +16,7 @@ */ -/* fluent-dom@aa95b1f (July 10, 2018) */ +/* fluent-dom@cab517f (July 31, 2018) */ const { Localization } = ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); @@ -60,10 +60,10 @@ const LOCALIZABLE_ATTRIBUTES = { th: ["abbr"] }, "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": { - description: ["value"], global: [ "accesskey", "aria-label", "aria-valuetext", "aria-moz-hint", "label" ], + description: ["value"], key: ["key", "keycode"], label: ["value"], textbox: ["placeholder"], @@ -523,7 +523,7 @@ class DOMLocalization extends Localization { if (this.windowElement) { if (this.windowElement !== newRoot.ownerGlobal) { throw new Error(`Cannot connect a root: - DOMLocalization already has a root from a different window`); + DOMLocalization already has a root from a different window.`); } } else { this.windowElement = newRoot.ownerGlobal; diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm index 2a0c0c4ae27e..8aa0722f20d8 100644 --- a/intl/l10n/Localization.jsm +++ b/intl/l10n/Localization.jsm @@ -16,7 +16,7 @@ */ -/* fluent-dom@aa95b1f (July 10, 2018) */ +/* fluent-dom@cab517f (July 31, 2018) */ /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ /* global console */ @@ -25,13 +25,25 @@ const { L10nRegistry } = ChromeUtils.import("resource://gre/modules/L10nRegistry const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {}); const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {}); -/* - * CachedAsyncIterable caches the elements yielded by an iterable. - * - * It can be used to iterate over an iterable many times without depleting the - * iterable. - */ -class CachedAsyncIterable { +class CachedIterable extends Array { + /** + * Create a `CachedIterable` instance from an iterable or, if another + * instance of `CachedIterable` is passed, return it without any + * modifications. + * + * @param {Iterable} iterable + * @returns {CachedIterable} + */ + static from(iterable) { + if (iterable instanceof this) { + return iterable; + } + + return new this(iterable); + } +} + +class CachedAsyncIterable extends CachedIterable { /** * Create an `CachedAsyncIterable` instance. * @@ -39,6 +51,8 @@ class CachedAsyncIterable { * @returns {CachedAsyncIterable} */ constructor(iterable) { + super(); + if (Symbol.asyncIterator in Object(iterable)) { this.iterator = iterable[Symbol.asyncIterator](); } else if (Symbol.iterator in Object(iterable)) { @@ -46,20 +60,46 @@ class CachedAsyncIterable { } else { throw new TypeError("Argument must implement the iteration protocol."); } - - this.seen = []; } + /** + * Synchronous iterator over the cached elements. + * + * Return a generator object implementing the iterator protocol over the + * cached elements of the original (async or sync) iterable. + */ + [Symbol.iterator]() { + const cached = this; + let cur = 0; + + return { + next() { + if (cached.length === cur) { + return {value: undefined, done: true}; + } + return cached[cur++]; + } + }; + } + + /** + * Asynchronous iterator caching the yielded elements. + * + * Elements yielded by the original iterable will be cached and available + * synchronously. Returns an async generator object implementing the + * iterator protocol over the elements of the original (async or sync) + * iterable. + */ [Symbol.asyncIterator]() { - const { seen, iterator } = this; + const cached = this; let cur = 0; return { async next() { - if (seen.length <= cur) { - seen.push(await iterator.next()); + if (cached.length <= cur) { + cached.push(await cached.iterator.next()); } - return seen[cur++]; + return cached[cur++]; } }; } @@ -71,13 +111,17 @@ class CachedAsyncIterable { * @param {number} count - number of elements to consume */ async touchNext(count = 1) { - const { seen, iterator } = this; let idx = 0; while (idx++ < count) { - if (seen.length === 0 || seen[seen.length - 1].done === false) { - seen.push(await iterator.next()); + const last = this[this.length - 1]; + if (last && last.done) { + break; } + this.push(await this.iterator.next()); } + // Return the last cached {value, done} object to allow the calling + // code to decide if it needs to call touchNext again. + return this[this.length - 1]; } } @@ -112,8 +156,8 @@ class Localization { constructor(resourceIds = [], generateMessages = defaultGenerateMessages) { this.resourceIds = resourceIds; this.generateMessages = generateMessages; - this.ctxs = - new CachedAsyncIterable(this.generateMessages(this.resourceIds)); + this.ctxs = CachedAsyncIterable.from( + this.generateMessages(this.resourceIds)); } addResourceIds(resourceIds) { @@ -276,8 +320,8 @@ class Localization { * that language negotiation or available resources changed. */ onChange() { - this.ctxs = - new CachedAsyncIterable(this.generateMessages(this.resourceIds)); + this.ctxs = CachedAsyncIterable.from( + this.generateMessages(this.resourceIds)); this.ctxs.touchNext(2); } } diff --git a/intl/l10n/MessageContext.jsm b/intl/l10n/MessageContext.jsm index 270fe82adae1..71932040660b 100644 --- a/intl/l10n/MessageContext.jsm +++ b/intl/l10n/MessageContext.jsm @@ -16,7 +16,7 @@ */ -/* fluent@aa95b1f (July 10, 2018) */ +/* fluent@0.7.0 */ /* eslint no-magic-numbers: [0] */ @@ -25,6 +25,9 @@ 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_?-]*$/; +const unicodeEscapeRe = /^[a-fA-F0-9]{4}$/; +const trailingWSRe = /[ \t\n\r]+$/; + /** * The `Parser` class is responsible for parsing FTL resources. @@ -94,46 +97,15 @@ class RuntimeParser { const ch = this._source[this._index]; // We don't care about comments or sections at runtime - if (ch === "/" || - (ch === "#" && - [" ", "#", "\n"].includes(this._source[this._index + 1]))) { + if (ch === "#" && + [" ", "#", "\n"].includes(this._source[this._index + 1])) { this.skipComment(); return; } - if (ch === "[") { - this.skipSection(); - return; - } - this.getMessage(); } - /** - * Skip the section entry from the current index. - * - * @private - */ - skipSection() { - this._index += 1; - if (this._source[this._index] !== "[") { - throw this.error('Expected "[[" to open a section'); - } - - this._index += 1; - - this.skipInlineWS(); - this.getVariantName(); - this.skipInlineWS(); - - if (this._source[this._index] !== "]" || - this._source[this._index + 1] !== "]") { - throw this.error('Expected "]]" to close a section'); - } - - this._index += 2; - } - /** * Parse the source string from the current index as an FTL message * and add it to the entries property on the Parser. @@ -147,6 +119,8 @@ class RuntimeParser { if (this._source[this._index] === "=") { this._index++; + } else { + throw this.error("Expected \"=\" after the identifier"); } this.skipInlineWS(); @@ -236,7 +210,7 @@ class RuntimeParser { * Get identifier using the provided regex. * * By default this will get identifiers of public messages, attributes and - * external arguments (without the $). + * variables (without the $). * * @returns {String} * @private @@ -311,21 +285,30 @@ class RuntimeParser { * @private */ getString() { - const start = this._index + 1; + let value = ""; + this._index++; - while (++this._index < this._length) { + while (this._index < this._length) { const ch = this._source[this._index]; if (ch === '"') { + this._index++; break; } if (ch === "\n") { throw this.error("Unterminated string expression"); } + + if (ch === "\\") { + value += this.getEscapedCharacter(["{", "\\", "\""]); + } else { + this._index++; + value += ch; + } } - return this._source.substring(start, this._index++); + return value; } /** @@ -349,10 +332,17 @@ class RuntimeParser { eol = this._length; } - const firstLineContent = start !== eol ? - this._source.slice(start, eol) : null; + // If there's any text between the = and the EOL, store it for now. The next + // non-empty line will decide what to do with it. + const firstLineContent = start !== eol + // Trim the trailing whitespace in case this is a single-line pattern. + // Multiline patterns are parsed anew by getComplexPattern. + ? this._source.slice(start, eol).replace(trailingWSRe, "") + : null; - if (firstLineContent && firstLineContent.includes("{")) { + if (firstLineContent + && (firstLineContent.includes("{") + || firstLineContent.includes("\\"))) { return this.getComplexPattern(); } @@ -439,13 +429,19 @@ class RuntimeParser { } ch = this._source[this._index]; continue; - } else if (ch === "\\") { - const ch2 = this._source[this._index + 1]; - if (ch2 === '"' || ch2 === "{" || ch2 === "\\") { - ch = ch2; - this._index++; - } - } else if (ch === "{") { + } + + if (ch === undefined) { + break; + } + + if (ch === "\\") { + buffer += this.getEscapedCharacter(); + ch = this._source[this._index]; + continue; + } + + if (ch === "{") { // Push the buffer to content array right before placeable if (buffer.length) { content.push(buffer); @@ -457,18 +453,13 @@ class RuntimeParser { buffer = ""; content.push(this.getPlaceable()); - this._index++; - - ch = this._source[this._index]; + ch = this._source[++this._index]; placeables++; continue; } - if (ch) { - buffer += ch; - } - this._index++; - ch = this._source[this._index]; + buffer += ch; + ch = this._source[++this._index]; } if (content.length === 0) { @@ -476,13 +467,42 @@ class RuntimeParser { } if (buffer.length) { - content.push(buffer); + // Trim trailing whitespace, too. + content.push(buffer.replace(trailingWSRe, "")); } return content; } /* eslint-enable complexity */ + /** + * Parse an escape sequence and return the unescaped character. + * + * @returns {string} + * @private + */ + getEscapedCharacter(specials = ["{", "\\"]) { + this._index++; + const next = this._source[this._index]; + + if (specials.includes(next)) { + this._index++; + return next; + } + + if (next === "u") { + const sequence = this._source.slice(this._index + 1, this._index + 5); + if (unicodeEscapeRe.test(sequence)) { + this._index += 5; + return String.fromCodePoint(parseInt(sequence, 16)); + } + + throw this.error(`Invalid Unicode escape sequence: \\u${sequence}`); + } + + throw this.error(`Unknown escape sequence: \\${next}`); + } + /** * Parses a single placeable in a Message pattern and returns its * expression. @@ -519,7 +539,7 @@ class RuntimeParser { const ch = this._source[this._index]; if (ch === "}") { - if (selector.type === "attr" && selector.id.name.startsWith("-")) { + if (selector.type === "getattr" && selector.id.name.startsWith("-")) { throw this.error( "Attributes of private messages cannot be interpolated." ); @@ -536,11 +556,11 @@ class RuntimeParser { throw this.error("Message references cannot be used as selectors."); } - if (selector.type === "var") { + if (selector.type === "getvar") { throw this.error("Variants cannot be used as selectors."); } - if (selector.type === "attr" && !selector.id.name.startsWith("-")) { + if (selector.type === "getattr" && !selector.id.name.startsWith("-")) { throw this.error( "Attributes of public messages cannot be used as selectors." ); @@ -578,6 +598,10 @@ class RuntimeParser { * @private */ getSelectorExpression() { + if (this._source[this._index] === "{") { + return this.getPlaceable(); + } + const literal = this.getLiteral(); if (literal.type !== "ref") { @@ -590,7 +614,7 @@ class RuntimeParser { const name = this.getIdentifier(); this._index++; return { - type: "attr", + type: "getattr", id: literal, name }; @@ -602,7 +626,7 @@ class RuntimeParser { const key = this.getVariantKey(); this._index++; return { - type: "var", + type: "getvar", id: literal, key }; @@ -640,7 +664,7 @@ class RuntimeParser { const args = []; while (this._index < this._length) { - this.skipInlineWS(); + this.skipWS(); if (this._source[this._index] === ")") { return args; @@ -657,7 +681,7 @@ class RuntimeParser { if (this._source[this._index] === ":") { this._index++; - this.skipInlineWS(); + this.skipWS(); const val = this.getSelectorExpression(); @@ -685,7 +709,7 @@ class RuntimeParser { } } - this.skipInlineWS(); + this.skipWS(); if (this._source[this._index] === ")") { break; @@ -885,7 +909,7 @@ class RuntimeParser { if (cc0 === 36) { // $ this._index++; return { - type: "ext", + type: "var", name: this.getIdentifier() }; } @@ -925,12 +949,11 @@ class RuntimeParser { // to parse them properly and skip their content. let eol = this._source.indexOf("\n", this._index); - while (eol !== -1 && - ((this._source[eol + 1] === "/" && this._source[eol + 2] === "/") || - (this._source[eol + 1] === "#" && - [" ", "#"].includes(this._source[eol + 2])))) { - this._index = eol + 3; + while (eol !== -1 + && this._source[eol + 1] === "#" + && [" ", "#"].includes(this._source[eol + 2])) { + this._index = eol + 3; eol = this._source.indexOf("\n", this._index); if (eol === -1) { @@ -972,7 +995,7 @@ class RuntimeParser { if ((cc >= 97 && cc <= 122) || // a-z (cc >= 65 && cc <= 90) || // A-Z - cc === 47 || cc === 91) { // /[ + cc === 45) { // - this._index = start; return; } @@ -1169,11 +1192,11 @@ function values(opts) { * The role of the Fluent resolver is to format a translation object to an * instance of `FluentType` or an array of instances. * - * Translations can contain references to other messages or external arguments, + * Translations can contain references to other messages or variables, * conditional logic in form of select expressions, traits which describe their * grammatical features, and can use Fluent builtins which make use of the * `Intl` formatters to format numbers, dates, lists and more into the - * context's language. See the documentation of the Fluent syntax for more + * context's language. See the documentation of the Fluent syntax for more * information. * * In case of errors the resolver will try to salvage as much of the @@ -1436,8 +1459,8 @@ function Type(env, expr) { return new FluentSymbol(expr.name); case "num": return new FluentNumber(expr.val); - case "ext": - return ExternalArgument(env, expr); + case "var": + return VariableReference(env, expr); case "fun": return FunctionReference(env, expr); case "call": @@ -1446,11 +1469,11 @@ function Type(env, expr) { const message = MessageReference(env, expr); return Type(env, message); } - case "attr": { + case "getattr": { const attr = AttributeExpression(env, expr); return Type(env, attr); } - case "var": { + case "getvar": { const variant = VariantExpression(env, expr); return Type(env, variant); } @@ -1474,7 +1497,7 @@ function Type(env, expr) { } /** - * Resolve a reference to an external argument. + * Resolve a reference to a variable. * * @param {Object} env * Resolver environment object. @@ -1485,11 +1508,11 @@ function Type(env, expr) { * @returns {FluentType} * @private */ -function ExternalArgument(env, {name}) { +function VariableReference(env, {name}) { const { args, errors } = env; if (!args || !args.hasOwnProperty(name)) { - errors.push(new ReferenceError(`Unknown external: ${name}`)); + errors.push(new ReferenceError(`Unknown variable: ${name}`)); return new FluentNone(name); } @@ -1512,7 +1535,7 @@ function ExternalArgument(env, {name}) { } default: errors.push( - new TypeError(`Unsupported external type: ${name}, ${typeof arg}`) + new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`) ); return new FluentNone(name); } @@ -1691,13 +1714,13 @@ class FluentResource extends Map { * 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 - * make use of the `Intl` formatters to format numbers, dates, lists and more - * into the context's language. See the documentation of the Fluent syntax for - * more information. + * Always use `MessageContext.format` to retrieve translation units from a + * context. Translations can contain references to other entities or variables, + * conditional logic in form of select expressions, traits which describe their + * grammatical features, and can use Fluent builtins which make use of the + * `Intl` formatters to format numbers, dates, lists and more into the + * context's language. See the documentation of the Fluent syntax for more + * information. */ class MessageContext { @@ -1849,8 +1872,8 @@ class MessageContext { * Format a message to a string or null. * * Format a raw `message` from the context into a string (or a null if it has - * a null value). `args` will be used to resolve references to external - * arguments inside of the translation. + * a null value). `args` will be used to resolve references to variables + * passed as arguments to the translation. * * In case of errors `format` will try to salvage as much of the translation * as possible and will still return a string. For performance reasons, the @@ -1868,7 +1891,7 @@ class MessageContext { * * // Returns 'Hello, name!' and `errors` is now: * - * [] + * [] * * @param {Object | string} message * @param {Object | undefined} args diff --git a/intl/l10n/fluent.js.patch b/intl/l10n/fluent.js.patch index f4dbe98ea136..f6072c69e831 100644 --- a/intl/l10n/fluent.js.patch +++ b/intl/l10n/fluent.js.patch @@ -1,481 +1,360 @@ -diff -uNr ./dist/DOMLocalization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm ---- ./dist/DOMLocalization.jsm 2018-04-13 08:25:21.143138950 -0700 -+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/DOMLocalization.jsm 2018-04-13 08:27:11.658083766 -0700 -@@ -18,10 +18,8 @@ +diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/DOMLocalization.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/DOMLocalization.jsm +--- ./intl/l10n/DOMLocalization.jsm 2018-08-03 13:25:20.275840905 -0700 ++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/DOMLocalization.jsm 2018-08-01 09:15:58.916763182 -0700 +@@ -16,10 +16,12 @@ + */ - /* fluent-dom@0.2.0 */ --import Localization from '../../fluent-dom/src/localization.js'; -- --/* eslint no-console: ["error", {allow: ["warn"]}] */ --/* global console */ -+const { Localization } = -+ ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); +-/* fluent-dom@cab517f (July 31, 2018) */ ++/* fluent-dom@0.3.0 */ + +-const { Localization } = +- ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); ++import Localization from '../../fluent-dom/src/localization.js'; ++ ++/* eslint no-console: ["error", {allow: ["warn"]}] */ ++/* global console */ // Match the opening angle bracket (<) in HTML tags, and HTML entities like // &, &, &. -@@ -96,6 +94,7 @@ +@@ -61,9 +63,7 @@ + global: [ + "accesskey", "aria-label", "aria-valuetext", "aria-moz-hint", "label" + ], +- description: ["value"], + key: ["key", "keycode"], +- label: ["value"], + textbox: ["placeholder"], + toolbarbutton: ["tooltiptext"], + } +@@ -96,7 +96,6 @@ const templateElement = element.ownerDocument.createElementNS( "http://www.w3.org/1999/xhtml", "template" ); -+ // eslint-disable-next-line no-unsanitized/property +- // eslint-disable-next-line no-unsanitized/property templateElement.innerHTML = value; overlayChildNodes(templateElement.content, element); } -@@ -323,6 +322,46 @@ +@@ -350,46 +349,6 @@ return toElement; } -+/** -+ * Sanitizes a translation before passing them to Node.localize API. -+ * -+ * It returns `false` if the translation contains DOM Overlays and should -+ * not go into Node.localize. -+ * -+ * Note: There's a third item of work that JS DOM Overlays do - removal -+ * of attributes from the previous translation. -+ * This is not trivial to implement for Node.localize scenario, so -+ * at the moment it is not supported. -+ * -+ * @param {{ -+ * localName: string, -+ * namespaceURI: string, -+ * type: string || null -+ * l10nId: string, -+ * l10nArgs: Array || null, -+ * l10nAttrs: string ||null, -+ * }} l10nItems -+ * @param {{value: string, attrs: Object}} translations -+ * @returns boolean -+ * @private -+ */ -+function sanitizeTranslationForNodeLocalize(l10nItem, translation) { -+ if (reOverlay.test(translation.value)) { -+ return false; -+ } -+ -+ if (translation.attributes) { -+ const explicitlyAllowed = l10nItem.l10nAttrs === null ? null : -+ l10nItem.l10nAttrs.split(",").map(i => i.trim()); -+ for (const [j, {name}] of translation.attributes.entries()) { -+ if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) { -+ translation.attributes.splice(j, 1); -+ } -+ } -+ } -+ return true; -+} -+ +-/** +- * Sanitizes a translation before passing them to Node.localize API. +- * +- * It returns `false` if the translation contains DOM Overlays and should +- * not go into Node.localize. +- * +- * Note: There's a third item of work that JS DOM Overlays do - removal +- * of attributes from the previous translation. +- * This is not trivial to implement for Node.localize scenario, so +- * at the moment it is not supported. +- * +- * @param {{ +- * localName: string, +- * namespaceURI: string, +- * type: string || null +- * l10nId: string, +- * l10nArgs: Array || null, +- * l10nAttrs: string ||null, +- * }} l10nItems +- * @param {{value: string, attrs: Object}} translations +- * @returns boolean +- * @private +- */ +-function sanitizeTranslationForNodeLocalize(l10nItem, translation) { +- if (reOverlay.test(translation.value)) { +- return false; +- } +- +- if (translation.attributes) { +- const explicitlyAllowed = l10nItem.l10nAttrs === null ? null : +- l10nItem.l10nAttrs.split(",").map(i => i.trim()); +- for (const [j, {name}] of translation.attributes.entries()) { +- if (!isAttrNameLocalizable(name, l10nItem, explicitlyAllowed)) { +- translation.attributes.splice(j, 1); +- } +- } +- } +- return true; +-} +- const L10NID_ATTR_NAME = "data-l10n-id"; const L10NARGS_ATTR_NAME = "data-l10n-args"; -@@ -568,6 +607,59 @@ +@@ -530,6 +489,7 @@ + ); + } + ++ + this.roots.add(newRoot); + this.mutationObserver.observe(newRoot, this.observerConfig); + } +@@ -639,10 +599,7 @@ + if (this.pendingElements.size > 0) { + if (this.pendingrAF === null) { + this.pendingrAF = this.windowElement.requestAnimationFrame(() => { +- // We need to filter for elements that lost their l10n-id while +- // waiting for the animation frame. +- this.translateElements(Array.from(this.pendingElements) +- .filter(elem => elem.hasAttribute("data-l10n-id"))); ++ this.translateElements(Array.from(this.pendingElements)); + this.pendingElements.clear(); + this.pendingrAF = null; + }); +@@ -664,63 +621,6 @@ * @returns {Promise} */ translateFragment(frag) { -+ if (frag.localize) { -+ // This is a temporary fast-path offered by Gecko to workaround performance -+ // issues coming from Fluent and XBL+Stylo performing unnecesary -+ // operations during startup. -+ // For details see bug 1441037, bug 1442262, and bug 1363862. -+ -+ // A sparse array which will store translations separated out from -+ // all translations that is needed for DOM Overlay. -+ const overlayTranslations = []; -+ -+ const getTranslationsForItems = async l10nItems => { -+ const keys = l10nItems.map(l10nItem => [l10nItem.l10nId, l10nItem.l10nArgs]); -+ const translations = await this.formatMessages(keys); -+ -+ // Here we want to separate out elements that require DOM Overlays. -+ // Those elements will have to be translated using our JS -+ // implementation, while everything else is going to use the fast-path. -+ for (const [i, translation] of translations.entries()) { -+ if (translation === undefined) { -+ continue; -+ } -+ -+ const hasOnlyText = -+ sanitizeTranslationForNodeLocalize(l10nItems[i], translation); -+ if (!hasOnlyText) { -+ // Removing from translations to make Node.localize skip it. -+ // We will translate it below using JS DOM Overlays. -+ overlayTranslations[i] = translations[i]; -+ translations[i] = undefined; -+ } -+ } -+ -+ // We pause translation observing here because Node.localize -+ // will translate the whole DOM next, using the `translations`. -+ // -+ // The observer will be resumed after DOM Overlays are localized -+ // in the next microtask. -+ this.pauseObserving(); -+ return translations; -+ }; -+ -+ return frag.localize(getTranslationsForItems.bind(this)) -+ .then(untranslatedElements => { -+ for (let i = 0; i < overlayTranslations.length; i++) { -+ if (overlayTranslations[i] !== undefined && -+ untranslatedElements[i] !== undefined) { -+ translateElement(untranslatedElements[i], overlayTranslations[i]); -+ } -+ } -+ this.resumeObserving(); -+ }) -+ .catch(() => this.resumeObserving()); -+ } +- if (frag.localize) { +- // This is a temporary fast-path offered by Gecko to workaround performance +- // issues coming from Fluent and XBL+Stylo performing unnecesary +- // operations during startup. +- // For details see bug 1441037, bug 1442262, and bug 1363862. +- +- // A sparse array which will store translations separated out from +- // all translations that is needed for DOM Overlay. +- const overlayTranslations = []; +- +- const getTranslationsForItems = async l10nItems => { +- const keys = l10nItems.map( +- l10nItem => ({id: l10nItem.l10nId, args: l10nItem.l10nArgs})); +- const translations = await this.formatMessages(keys); +- +- // Here we want to separate out elements that require DOM Overlays. +- // Those elements will have to be translated using our JS +- // implementation, while everything else is going to use the fast-path. +- for (const [i, translation] of translations.entries()) { +- if (translation === undefined) { +- continue; +- } +- +- const hasOnlyText = +- sanitizeTranslationForNodeLocalize(l10nItems[i], translation); +- if (!hasOnlyText) { +- // Removing from translations to make Node.localize skip it. +- // We will translate it below using JS DOM Overlays. +- overlayTranslations[i] = translations[i]; +- translations[i] = undefined; +- } +- } +- +- // We pause translation observing here because Node.localize +- // will translate the whole DOM next, using the `translations`. +- // +- // The observer will be resumed after DOM Overlays are localized +- // in the next microtask. +- this.pauseObserving(); +- return translations; +- }; +- +- return frag.localize(getTranslationsForItems.bind(this)) +- .then(untranslatedElements => { +- for (let i = 0; i < overlayTranslations.length; i++) { +- if (overlayTranslations[i] !== undefined && +- untranslatedElements[i] !== undefined) { +- translateElement(untranslatedElements[i], overlayTranslations[i]); +- } +- } +- this.resumeObserving(); +- }) +- .catch(e => { +- this.resumeObserving(); +- throw e; +- }); +- } return this.translateElements(this.getTranslatables(frag)); } -@@ -647,37 +739,5 @@ +@@ -800,5 +700,37 @@ } } --/* global L10nRegistry, Services */ -- --/** -- * The default localization strategy for Gecko. It comabines locales -- * available in L10nRegistry, with locales requested by the user to -- * generate the iterator over MessageContexts. -- * -- * In the future, we may want to allow certain modules to override this -- * with a different negotitation strategy to allow for the module to -- * be localized into a different language - for example DevTools. -- */ --function defaultGenerateMessages(resourceIds) { -- const requestedLocales = Services.locale.getRequestedLocales(); -- const availableLocales = L10nRegistry.getAvailableLocales(); -- const defaultLocale = Services.locale.defaultLocale; -- const locales = Services.locale.negotiateLanguages( -- requestedLocales, availableLocales, defaultLocale, -- ); -- return L10nRegistry.generateContexts(locales, resourceIds); --} -- -- --class GeckoDOMLocalization extends DOMLocalization { -- constructor( -- windowElement, -- resourceIds, -- generateMessages = defaultGenerateMessages -- ) { -- super(windowElement, resourceIds, generateMessages); -- } --} -- --this.DOMLocalization = GeckoDOMLocalization; --this.EXPORTED_SYMBOLS = ["DOMLocalization"]; -+this.DOMLocalization = DOMLocalization; -+var EXPORTED_SYMBOLS = ["DOMLocalization"]; -diff -uNr ./dist/l10n.js /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js ---- ./dist/l10n.js 2018-04-13 08:25:21.307139138 -0700 -+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/l10n.js 2018-04-13 08:27:25.230296529 -0700 -@@ -1,20 +1,26 @@ --/* global Components, document, window */ - { - const { DOMLocalization } = -- Components.utils.import("resource://gre/modules/DOMLocalization.jsm"); -+ ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {}); - - /** - * Polyfill for document.ready polyfill. - * See: https://github.com/whatwg/html/issues/127 for details. - * -+ * XXX: The callback is a temporary workaround for bug 1193394. Once Promises in Gecko -+ * start beeing a microtask and stop pushing translation post-layout, we can -+ * remove it and start using the returned Promise again. -+ * -+ * @param {Function} callback - function to be called when the document is ready. - * @returns {Promise} - */ -- function documentReady() { -+ function documentReady(callback) { - if (document.contentType === "application/vnd.mozilla.xul+xml") { - // XUL - return new Promise( - resolve => document.addEventListener( -- "MozBeforeInitialXULLayout", resolve, { once: true } -+ "MozBeforeInitialXULLayout", () => { -+ resolve(callback()); -+ }, { once: true } - ) - ); - } -@@ -22,11 +28,13 @@ - // HTML - const rs = document.readyState; - if (rs === "interactive" || rs === "completed") { -- return Promise.resolve(); -+ return Promise.resolve(callback); - } - return new Promise( - resolve => document.addEventListener( -- "readystatechange", resolve, { once: true } -+ "readystatechange", () => { -+ resolve(callback()); -+ }, { once: true } - ) - ); - } -@@ -50,11 +58,8 @@ - // trigger first context to be fetched eagerly - document.l10n.ctxs.touchNext(); - -- document.l10n.ready = documentReady().then(() => { -+ document.l10n.ready = documentReady(() => { - document.l10n.registerObservers(); -- window.addEventListener("unload", () => { -- document.l10n.unregisterObservers(); -- }); - document.l10n.connectRoot(document.documentElement); - return document.l10n.translateRoots(); - }); -diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm ---- ./dist/Localization.jsm 2018-04-13 08:25:20.946138732 -0700 -+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/Localization.jsm 2018-04-13 08:27:16.396155987 -0700 -@@ -18,70 +18,13 @@ - - /* fluent-dom@0.2.0 */ - --/* eslint no-magic-numbers: [0] */ -- --/* global Intl */ -- --/** -- * @overview -- * -- * The FTL resolver ships with a number of functions built-in. -- * -- * Each function take two arguments: -- * - args - an array of positional args -- * - opts - an object of key-value args -- * -- * Arguments to functions are guaranteed to already be instances of -- * `FluentType`. Functions must return `FluentType` objects as well. -- */ -+/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ -+/* global console */ - --/** -- * @overview -- * -- * The role of the Fluent resolver is to format a translation object to an -- * instance of `FluentType` or an array of instances. -- * -- * Translations can contain references to other messages or external arguments, -- * conditional logic in form of select expressions, traits which describe their -- * grammatical features, and can use Fluent builtins which make use of the -- * `Intl` formatters to format numbers, dates, lists and more into the -- * context's language. See the documentation of the Fluent syntax for more -- * information. -- * -- * In case of errors the resolver will try to salvage as much of the -- * translation as possible. In rare situations where the resolver didn't know -- * how to recover from an error it will return an instance of `FluentNone`. -- * -- * `MessageReference`, `VariantExpression`, `AttributeExpression` and -- * `SelectExpression` resolve to raw Runtime Entries objects and the result of -- * the resolution needs to be passed into `Type` to get their real value. -- * This is useful for composing expressions. Consider: -- * -- * brand-name[nominative] -- * -- * which is a `VariantExpression` with properties `id: MessageReference` and -- * `key: Keyword`. If `MessageReference` was resolved eagerly, it would -- * instantly resolve to the value of the `brand-name` message. Instead, we -- * want to get the message object and look for its `nominative` variant. -- * -- * All other expressions (except for `FunctionReference` which is only used in -- * `CallExpression`) resolve to an instance of `FluentType`. The caller should -- * use the `toString` method to convert the instance to a native value. -- * -- * -- * All functions in this file pass around a special object called `env`. -- * This object stores a set of elements used by all resolve functions: -- * -- * * {MessageContext} ctx -- * context for which the given resolution is happening -- * * {Object} args -- * list of developer provided arguments that can be used -- * * {Array} errors -- * list of errors collected while resolving -- * * {WeakSet} dirty -- * Set of patterns already encountered during this resolution. -- * This is used to prevent cyclic resolutions. -- */ -+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", {}); - - /* - * CachedIterable caches the elements yielded by an iterable. -@@ -148,58 +91,19 @@ - } - } - --/* -- * @overview -- * -- * Functions for managing ordered sequences of MessageContexts. -- * -- * An ordered iterable of MessageContext instances can represent the current -- * negotiated fallback chain of languages. This iterable can be used to find -- * the best existing translation for a given identifier. -- * -- * The mapContext* methods can be used to find the first MessageContext in the -- * given iterable which contains the translation with the given identifier. If -- * the iterable is ordered according to the result of a language negotiation -- * the returned MessageContext contains the best available translation. -- * -- * A simple function which formats translations based on the identifier might -- * be implemented as follows: -- * -- * formatString(id, args) { -- * const ctx = mapContextSync(contexts, id); -- * -- * if (ctx === null) { -- * return id; -- * } -- * -- * const msg = ctx.getMessage(id); -- * return ctx.format(msg, args); -- * } -- * -- * In order to pass an iterator to mapContext*, wrap it in CachedIterable. -- * This allows multiple calls to mapContext* without advancing and eventually -- * depleting the iterator. -- * -- * function *generateMessages() { -- * // Some lazy logic for yielding MessageContexts. -- * yield *[ctx1, ctx2]; -- * } -- * -- * const contexts = new CachedIterable(generateMessages()); -- * const ctx = mapContextSync(contexts, id); -- * -- */ -- --/* -- * @module fluent -- * @overview -- * -- * `fluent` is a JavaScript implementation of Project Fluent, a localization -- * framework designed to unleash the expressive power of the natural language. +-this.DOMLocalization = DOMLocalization; +-var EXPORTED_SYMBOLS = ["DOMLocalization"]; ++/* global L10nRegistry, Services */ ++ +/** + * The default localization strategy for Gecko. It comabines locales + * available in L10nRegistry, with locales requested by the user to + * generate the iterator over MessageContexts. - * ++ * + * In the future, we may want to allow certain modules to override this + * with a different negotitation strategy to allow for the module to + * be localized into a different language - for example DevTools. - */ -- --/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ ++ */ +function defaultGenerateMessages(resourceIds) { -+ const appLocales = Services.locale.getAppLocalesAsLangTags(); -+ return L10nRegistry.generateContexts(appLocales, resourceIds); ++ const requestedLocales = Services.locale.getRequestedLocales(); ++ const availableLocales = L10nRegistry.getAvailableLocales(); ++ const defaultLocale = Services.locale.defaultLocale; ++ const locales = Services.locale.negotiateLanguages( ++ requestedLocales, availableLocales, defaultLocale, ++ ); ++ return L10nRegistry.generateContexts(locales, resourceIds); +} - - /** - * The `Localization` class is a central high-level API for vanilla -@@ -215,7 +119,7 @@ - * - * @returns {Localization} - */ -- constructor(resourceIds, generateMessages) { -+ constructor(resourceIds, generateMessages = defaultGenerateMessages) { - this.resourceIds = resourceIds; - this.generateMessages = generateMessages; - this.ctxs = new CachedIterable(this.generateMessages(this.resourceIds)); -@@ -236,7 +140,7 @@ - async formatWithFallback(keys, method) { - const translations = []; - -- for (let ctx of this.ctxs) { -+ for await (let ctx of this.ctxs) { - // This can operate on synchronous and asynchronous - // contexts coming from the iterator. - if (typeof ctx.then === "function") { -@@ -248,7 +152,7 @@ - break; - } - -- if (typeof console !== "undefined") { -+ if (AppConstants.NIGHTLY_BUILD) { - const locale = ctx.locales[0]; - const ids = Array.from(missingIds).join(", "); - console.warn(`Missing translations in ${locale}: ${ids}`); -@@ -335,8 +239,28 @@ - return val; - } - -- handleEvent() { -- this.onLanguageChange(); -+ /** -+ * Register weak observers on events that will trigger cache invalidation -+ */ -+ registerObservers() { -+ Services.obs.addObserver(this, "intl:app-locales-changed", true); -+ } + -+ /** -+ * Default observer handler method. -+ * -+ * @param {String} subject -+ * @param {String} topic -+ * @param {Object} data -+ */ -+ observe(subject, topic, data) { -+ switch (topic) { -+ case "intl:app-locales-changed": -+ this.onLanguageChange(); -+ break; -+ default: -+ break; -+ } - } ++ ++class GeckoDOMLocalization extends DOMLocalization { ++ constructor( ++ windowElement, ++ resourceIds, ++ generateMessages = defaultGenerateMessages ++ ) { ++ super(windowElement, resourceIds, generateMessages); ++ } ++} ++ ++this.DOMLocalization = GeckoDOMLocalization; ++this.EXPORTED_SYMBOLS = ["DOMLocalization"]; +diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/l10n.js /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/l10n.js +--- ./intl/l10n/l10n.js 2018-08-03 13:26:42.691527746 -0700 ++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/l10n.js 2018-08-01 09:15:59.253432348 -0700 +@@ -1,6 +1,7 @@ ++/* global Components, document, window */ + { + const { DOMLocalization } = +- ChromeUtils.import("resource://gre/modules/DOMLocalization.jsm", {}); ++ Components.utils.import("resource://gre/modules/DOMLocalization.jsm"); /** -@@ -348,6 +272,10 @@ - } - } + * Polyfill for document.ready polyfill. +@@ -44,13 +45,16 @@ -+Localization.prototype.QueryInterface = XPCOMUtils.generateQI([ -+ Ci.nsISupportsWeakReference -+]); -+ - /** - * Format the value of a message into a string. - * -@@ -368,6 +296,7 @@ + const resourceIds = getResourceLinks(document.head || document); + +- document.l10n = new DOMLocalization(resourceIds); ++ document.l10n = new DOMLocalization(window, resourceIds); + +- // Trigger the first two contexts to be loaded eagerly. +- document.l10n.ctxs.touchNext(2); ++ // trigger first context to be fetched eagerly ++ document.l10n.ctxs.touchNext(); + + document.l10n.ready = documentReady().then(() => { + document.l10n.registerObservers(); ++ window.addEventListener("unload", () => { ++ document.l10n.unregisterObservers(); ++ }); + document.l10n.connectRoot(document.documentElement); + return document.l10n.translateRoots(); + }); +diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/Localization.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/Localization.jsm +--- ./intl/l10n/Localization.jsm 2018-08-03 13:20:57.417703171 -0700 ++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/Localization.jsm 2018-08-01 09:15:58.546760435 -0700 +@@ -16,128 +16,11 @@ */ - function valueFromContext(ctx, errors, id, args) { - const msg = ctx.getMessage(id); -+ - return ctx.format(msg, args, errors); - } -@@ -467,44 +396,5 @@ - return missingIds; - } --/* global Components */ --/* eslint no-unused-vars: 0 */ +-/* fluent-dom@cab517f (July 31, 2018) */ ++/* fluent-dom@0.3.0 */ + +-/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ +-/* global console */ - --const Cu = Components.utils; --const Cc = Components.classes; --const Ci = Components.interfaces; +-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", {}); ++import { CachedAsyncIterable } from 'cached-iterable'; + +-class CachedIterable extends Array { +- /** +- * Create a `CachedIterable` instance from an iterable or, if another +- * instance of `CachedIterable` is passed, return it without any +- * modifications. +- * +- * @param {Iterable} iterable +- * @returns {CachedIterable} +- */ +- static from(iterable) { +- if (iterable instanceof this) { +- return iterable; +- } - --const { L10nRegistry } = -- Cu.import("resource://gre/modules/L10nRegistry.jsm", {}); --const ObserverService = -- Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); --const { Services } = -- Cu.import("resource://gre/modules/Services.jsm", {}); +- return new this(iterable); +- } +-} +- +-class CachedAsyncIterable extends CachedIterable { +- /** +- * Create an `CachedAsyncIterable` instance. +- * +- * @param {Iterable} iterable +- * @returns {CachedAsyncIterable} +- */ +- constructor(iterable) { +- super(); +- +- if (Symbol.asyncIterator in Object(iterable)) { +- this.iterator = iterable[Symbol.asyncIterator](); +- } else if (Symbol.iterator in Object(iterable)) { +- this.iterator = iterable[Symbol.iterator](); +- } else { +- throw new TypeError("Argument must implement the iteration protocol."); +- } +- } +- +- /** +- * Synchronous iterator over the cached elements. +- * +- * Return a generator object implementing the iterator protocol over the +- * cached elements of the original (async or sync) iterable. +- */ +- [Symbol.iterator]() { +- const cached = this; +- let cur = 0; +- +- return { +- next() { +- if (cached.length === cur) { +- return {value: undefined, done: true}; +- } +- return cached[cur++]; +- } +- }; +- } +- +- /** +- * Asynchronous iterator caching the yielded elements. +- * +- * Elements yielded by the original iterable will be cached and available +- * synchronously. Returns an async generator object implementing the +- * iterator protocol over the elements of the original (async or sync) +- * iterable. +- */ +- [Symbol.asyncIterator]() { +- const cached = this; +- let cur = 0; +- +- return { +- async next() { +- if (cached.length <= cur) { +- cached.push(await cached.iterator.next()); +- } +- return cached[cur++]; +- } +- }; +- } +- +- /** +- * This method allows user to consume the next element from the iterator +- * into the cache. +- * +- * @param {number} count - number of elements to consume +- */ +- async touchNext(count = 1) { +- let idx = 0; +- while (idx++ < count) { +- const last = this[this.length - 1]; +- if (last && last.done) { +- break; +- } +- this.push(await this.iterator.next()); +- } +- // Return the last cached {value, done} object to allow the calling +- // code to decide if it needs to call touchNext again. +- return this[this.length - 1]; +- } +-} - -/** - * The default localization strategy for Gecko. It comabines locales @@ -487,99 +366,201 @@ diff -uNr ./dist/Localization.jsm /home/zbraniecki/projects/mozilla-unified/intl - * be localized into a different language - for example DevTools. - */ -function defaultGenerateMessages(resourceIds) { -- const requestedLocales = Services.locale.getRequestedLocales(); -- const availableLocales = L10nRegistry.getAvailableLocales(); -- const defaultLocale = Services.locale.defaultLocale; -- const locales = Services.locale.negotiateLanguages( -- requestedLocales, availableLocales, defaultLocale, -- ); -- return L10nRegistry.generateContexts(locales, resourceIds); +- const appLocales = Services.locale.getAppLocalesAsBCP47(); +- return L10nRegistry.generateContexts(appLocales, resourceIds); -} -- --class GeckoLocalization extends Localization { -- constructor(resourceIds, generateMessages = defaultGenerateMessages) { -- super(resourceIds, generateMessages); ++/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ + + /** + * The `Localization` class is a central high-level API for vanilla +@@ -153,7 +36,7 @@ + * + * @returns {Localization} + */ +- constructor(resourceIds = [], generateMessages = defaultGenerateMessages) { ++ constructor(resourceIds = [], generateMessages) { + this.resourceIds = resourceIds; + this.generateMessages = generateMessages; + this.ctxs = CachedAsyncIterable.from( +@@ -194,12 +77,9 @@ + break; + } + +- if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) { ++ if (typeof console !== "undefined") { + const locale = ctx.locales[0]; + const ids = Array.from(missingIds).join(", "); +- if (Cu.isInAutomation) { +- throw new Error(`Missing translations in ${locale}: ${ids}`); +- } + console.warn(`Missing translations in ${locale}: ${ids}`); + } + } +@@ -284,35 +164,8 @@ + 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); - } --} - --this.Localization = GeckoLocalization; --this.EXPORTED_SYMBOLS = ["Localization"]; -+this.Localization = Localization; -+var EXPORTED_SYMBOLS = ["Localization"]; -diff -uNr ./dist/MessageContext.jsm /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm ---- ./dist/MessageContext.jsm 2018-04-13 08:25:20.698138486 -0700 -+++ /home/zbraniecki/projects/mozilla-unified/intl/l10n/MessageContext.jsm 2018-04-13 08:27:20.944227388 -0700 +- /** +- * Default observer handler method. +- * +- * @param {String} subject +- * @param {String} topic +- * @param {Object} data +- */ +- observe(subject, topic, data) { +- switch (topic) { +- case "intl:app-locales-changed": +- this.onChange(); +- break; +- case "nsPref:changed": +- switch (data) { +- case "intl.l10n.pseudo": +- this.onChange(); +- } +- break; +- default: +- break; +- } ++ handleEvent() { ++ this.onChange(); + } + + /** +@@ -326,10 +179,6 @@ + } + } + +-Localization.prototype.QueryInterface = ChromeUtils.generateQI([ +- Ci.nsISupportsWeakReference +-]); +- + /** + * Format the value of a message into a string. + * +@@ -449,5 +298,44 @@ + return missingIds; + } + +-this.Localization = Localization; +-var EXPORTED_SYMBOLS = ["Localization"]; ++/* global Components */ ++/* eslint no-unused-vars: 0 */ ++ ++const Cu = Components.utils; ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++ ++const { L10nRegistry } = ++ Cu.import("resource://gre/modules/L10nRegistry.jsm", {}); ++const ObserverService = ++ Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); ++const { Services } = ++ Cu.import("resource://gre/modules/Services.jsm", {}); ++ ++/** ++ * The default localization strategy for Gecko. It comabines locales ++ * available in L10nRegistry, with locales requested by the user to ++ * generate the iterator over MessageContexts. ++ * ++ * In the future, we may want to allow certain modules to override this ++ * with a different negotitation strategy to allow for the module to ++ * be localized into a different language - for example DevTools. ++ */ ++function defaultGenerateMessages(resourceIds) { ++ const requestedLocales = Services.locale.getRequestedLocales(); ++ const availableLocales = L10nRegistry.getAvailableLocales(); ++ const defaultLocale = Services.locale.defaultLocale; ++ const locales = Services.locale.negotiateLanguages( ++ requestedLocales, availableLocales, defaultLocale, ++ ); ++ return L10nRegistry.generateContexts(locales, resourceIds); ++} ++ ++class GeckoLocalization extends Localization { ++ constructor(resourceIds, generateMessages = defaultGenerateMessages) { ++ super(resourceIds, generateMessages); ++ } ++} ++ ++this.Localization = GeckoLocalization; ++this.EXPORTED_SYMBOLS = ["Localization"]; +diff -uN -x README -x moz.build -x L10nRegistry.jsm -x jar.mn -x fluent.js.patch ./intl/l10n/MessageContext.jsm /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/MessageContext.jsm +--- ./intl/l10n/MessageContext.jsm 2018-08-03 13:11:36.949029757 -0700 ++++ /home/zbraniecki/projects/fluent/fluent.js/fluent-gecko/dist/MessageContext.jsm 2018-08-01 09:15:58.176757688 -0700 @@ -16,7 +16,7 @@ */ --/* fluent-dom@0.2.0 */ -+/* fluent@0.6.3 */ +-/* fluent@0.6.0 */ ++/* fluent-dom@0.3.0 */ /* eslint no-magic-numbers: [0] */ -@@ -1858,63 +1858,5 @@ +@@ -1930,6 +1930,57 @@ } } --/* -- * CachedIterable caches the elements yielded by an iterable. -- * -- * It can be used to iterate over an iterable many times without depleting the -- * iterable. -- */ -- --/* -- * @overview -- * -- * Functions for managing ordered sequences of MessageContexts. -- * -- * An ordered iterable of MessageContext instances can represent the current -- * negotiated fallback chain of languages. This iterable can be used to find -- * the best existing translation for a given identifier. -- * -- * The mapContext* methods can be used to find the first MessageContext in the -- * given iterable which contains the translation with the given identifier. If -- * the iterable is ordered according to the result of a language negotiation -- * the returned MessageContext contains the best available translation. -- * -- * A simple function which formats translations based on the identifier might -- * be implemented as follows: -- * -- * formatString(id, args) { -- * const ctx = mapContextSync(contexts, id); -- * -- * if (ctx === null) { -- * return id; -- * } -- * -- * const msg = ctx.getMessage(id); -- * return ctx.format(msg, args); -- * } -- * -- * In order to pass an iterator to mapContext*, wrap it in CachedIterable. -- * This allows multiple calls to mapContext* without advancing and eventually -- * depleting the iterator. -- * -- * function *generateMessages() { -- * // Some lazy logic for yielding MessageContexts. -- * yield *[ctx1, ctx2]; -- * } -- * -- * const contexts = new CachedIterable(generateMessages()); -- * const ctx = mapContextSync(contexts, id); -- * -- */ -- --/* -- * @module fluent -- * @overview -- * -- * `fluent` is a JavaScript implementation of Project Fluent, a localization -- * framework designed to unleash the expressive power of the natural language. -- * -- */ -- ++/* ++ * @overview ++ * ++ * Functions for managing ordered sequences of MessageContexts. ++ * ++ * An ordered iterable of MessageContext instances can represent the current ++ * negotiated fallback chain of languages. This iterable can be used to find ++ * the best existing translation for a given identifier. ++ * ++ * The mapContext* methods can be used to find the first MessageContext in the ++ * given iterable which contains the translation with the given identifier. If ++ * the iterable is ordered according to the result of a language negotiation ++ * the returned MessageContext contains the best available translation. ++ * ++ * A simple function which formats translations based on the identifier might ++ * be implemented as follows: ++ * ++ * formatString(id, args) { ++ * const ctx = mapContextSync(contexts, id); ++ * ++ * if (ctx === null) { ++ * return id; ++ * } ++ * ++ * const msg = ctx.getMessage(id); ++ * return ctx.format(msg, args); ++ * } ++ * ++ * In order to pass an iterator to mapContext*, wrap it in ++ * Cached{Sync|Async}Iterable. ++ * This allows multiple calls to mapContext* without advancing and eventually ++ * depleting the iterator. ++ * ++ * function *generateMessages() { ++ * // Some lazy logic for yielding MessageContexts. ++ * yield *[ctx1, ctx2]; ++ * } ++ * ++ * const contexts = new CachedSyncIterable(generateMessages()); ++ * const ctx = mapContextSync(contexts, id); ++ * ++ */ ++ ++/* ++ * @module fluent ++ * @overview ++ * ++ * `fluent` is a JavaScript implementation of Project Fluent, a localization ++ * framework designed to unleash the expressive power of the natural language. ++ * ++ */ ++ this.MessageContext = MessageContext; --this.EXPORTED_SYMBOLS = ["MessageContext"]; -+var EXPORTED_SYMBOLS = ["MessageContext"]; +-this.FluentResource = FluentResource; +-var EXPORTED_SYMBOLS = ["MessageContext", "FluentResource"]; ++this.EXPORTED_SYMBOLS = ["MessageContext"]; diff --git a/intl/l10n/test/dom/test_domloc.xul b/intl/l10n/test/dom/test_domloc.xul index fcdbefc7254f..215e608754ff 100644 --- a/intl/l10n/test/dom/test_domloc.xul +++ b/intl/l10n/test/dom/test_domloc.xul @@ -19,10 +19,10 @@ async function * generateMessages(locales, resourceIds) { const mc = new MessageContext(locales); mc.addMessages(` -file-menu +file-menu = .label = File .accesskey = F -new-tab +new-tab = .label = New Tab .accesskey = N `); diff --git a/intl/l10n/test/dom/test_domloc_translateElements.html b/intl/l10n/test/dom/test_domloc_translateElements.html index 5e76c1a50beb..e82f03cf40dc 100644 --- a/intl/l10n/test/dom/test_domloc_translateElements.html +++ b/intl/l10n/test/dom/test_domloc_translateElements.html @@ -15,7 +15,7 @@ async function* mockGenerateMessages(locales, resourceIds) { const mc = new MessageContext(locales); mc.addMessages("title = Hello World"); - mc.addMessages("link\n .title = Click me"); + mc.addMessages("link =\n .title = Click me"); yield mc; }