diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html index a6567eda4ea3..0c96ad25f142 100644 --- a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html @@ -38,7 +38,7 @@ document.l10n.setAttributes(inputElem, "about-telemetry-filter-all-placeholder"); // Due to the async iteractions between nsINode.localize - // and DOMLocalization.jsm, we'll need to wait two frames + // and DOMLocalization, we'll need to wait two frames // to verify that no mutations happened. requestAnimationFrame(() => { requestAnimationFrame(() => { diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html index af2a108e4605..3d8e0a77f997 100644 --- a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html @@ -38,7 +38,7 @@ document.l10n.setAttributes(inputElem, "about-telemetry-filter-placeholder", {selectedTitle: "Test"}); // Due to the async iteractions between nsINode.localize - // and DOMLocalization.jsm, we'll need to wait two frames + // and DOMLocalization, we'll need to wait two frames // to verify that no mutations happened. requestAnimationFrame(() => { requestAnimationFrame(() => { diff --git a/intl/l10n/Localization.jsm b/intl/l10n/Localization.jsm deleted file mode 100644 index 7d3e786fa74f..000000000000 --- a/intl/l10n/Localization.jsm +++ /dev/null @@ -1,607 +0,0 @@ -/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ - -/* Copyright 2017 Mozilla Foundation and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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@fa25466f (October 12, 2018) */ - -/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ -/* global console */ - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); - -/* - * Base CachedIterable class. - */ -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); - } -} - -/* - * CachedAsyncIterable caches the elements yielded by an async iterable. - * - * It can be used to iterate over an iterable many times without depleting the - * 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."); - } - } - - /** - * 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(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 && (await last).done) { - break; - } - this.push(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]; - } -} - -/* - * CachedSyncIterable caches the elements yielded by an iterable. - * - * It can be used to iterate over an iterable many times without depleting the - * iterable. - */ -class CachedSyncIterable extends CachedIterable { - /** - * Create an `CachedSyncIterable` instance. - * - * @param {Iterable} iterable - * @returns {CachedSyncIterable} - */ - constructor(iterable) { - super(); - - if (Symbol.iterator in Object(iterable)) { - this.iterator = iterable[Symbol.iterator](); - } else { - throw new TypeError("Argument must implement the iteration protocol."); - } - } - - [Symbol.iterator]() { - const cached = this; - let cur = 0; - - return { - next() { - if (cached.length <= cur) { - cached.push(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 - */ - touchNext(count = 1) { - let idx = 0; - while (idx++ < count) { - const last = this[this.length - 1]; - if (last && last.done) { - break; - } - this.push(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 - * available in L10nRegistry, with locales requested by the user to - * generate the iterator over FluentBundles. - * - * 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 defaultGenerateBundles(resourceIds) { - const appLocales = Services.locale.appLocalesAsBCP47; - return L10nRegistry.getInstance().generateBundles(appLocales, resourceIds); -} - -function defaultGenerateBundlesSync(resourceIds) { - const appLocales = Services.locale.appLocalesAsBCP47; - return L10nRegistry.getInstance().generateBundlesSync(appLocales, resourceIds); -} - -function maybeReportErrorToGecko(error) { - if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) { - if (Cu.isInAutomation) { - // We throw a string, rather than Error - // to allow the C++ Promise handler - // to clone it - throw error; - } - console.warn(error); - } -} - -/** - * The `Localization` class is a central high-level API for vanilla - * JavaScript use of Fluent. - * It combines language negotiation, FluentBundle and I/O to - * provide a scriptable API to format translations. - */ -const Localization = { - cached(iterable, isSync) { - if (isSync) { - return CachedSyncIterable.from(iterable); - } else { - return CachedAsyncIterable.from(iterable); - } - }, - - /** - * Format translations and handle fallback if needed. - * - * Format translations for `keys` from `FluentBundle` instances on this - * Localization. In case of errors, fetch the next context in the - * fallback chain. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @param {Function} method - Formatting function. - * @returns {Promise>} - * @private - */ - async formatWithFallback(resourceIds, bundles, keys, method) { - if (!bundles) { - throw new Error("Attempt to format on an uninitialized instance."); - } - - const translations = new Array(keys.length).fill(null); - let hasAtLeastOneBundle = false; - - for await (const bundle of bundles) { - hasAtLeastOneBundle = true; - const missingIds = keysFromBundle(method, bundle, keys, translations); - - if (missingIds.size === 0) { - break; - } - - const locale = bundle.locales[0]; - const ids = Array.from(missingIds).join(", "); - maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`); - } - - if (!hasAtLeastOneBundle) { - maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(resourceIds)}.`); - } - - return translations; - }, - - /** - * Format translations and handle fallback if needed. - * - * Format translations for `keys` from `FluentBundle` instances on this - * Localization. In case of errors, fetch the next context in the - * fallback chain. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @param {Function} method - Formatting function. - * @returns {Array} - * @private - */ - formatWithFallbackSync(resourceIds, bundles, keys, method) { - if (!bundles) { - throw new Error("Attempt to format on an uninitialized instance."); - } - - const translations = new Array(keys.length).fill(null); - let hasAtLeastOneBundle = false; - - for (const bundle of bundles) { - hasAtLeastOneBundle = true; - const missingIds = keysFromBundle(method, bundle, keys, translations); - - if (missingIds.size === 0) { - break; - } - - const locale = bundle.locales[0]; - const ids = Array.from(missingIds).join(", "); - maybeReportErrorToGecko(`[fluent] Missing translations in ${locale}: ${ids}.`); - } - - if (!hasAtLeastOneBundle) { - maybeReportErrorToGecko(`[fluent] Request for keys failed because no resource bundles got generated.\n keys: ${JSON.stringify(keys)}.\n resourceIds: ${JSON.stringify(resourceIds)}.`); - } - - return translations; - }, - - - /** - * Format translations into {value, attributes} objects. - * - * The fallback logic is the same as in `formatValues` but it returns {value, - * attributes} objects which are suitable for the translation of DOM - * elements. - * - * docL10n.formatMessages([ - * {id: 'hello', args: { who: 'Mary' }}, - * {id: 'welcome'} - * ]).then(console.log); - * - * // [ - * // { value: 'Hello, Mary!', attributes: null }, - * // { - * // value: 'Welcome!', - * // attributes: [ { name: "title", value: 'Hello' } ] - * // } - * // ] - * - * Returns a Promise resolving to an array of the translation messages. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @returns {Promise>} - * @private - */ - formatMessages(resourceIds, bundles, keys) { - return this.formatWithFallback(resourceIds, bundles, keys, messageFromBundle); - }, - - /** - * Sync version of `formatMessages`. - * - * Returns an array of the translation messages. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @returns {Array<{value: string, attributes: Object}?>} - * @private - */ - formatMessagesSync(resourceIds, bundles, keys) { - return this.formatWithFallbackSync(resourceIds, bundles, keys, messageFromBundle); - }, - - /** - * Retrieve translations corresponding to the passed keys. - * - * A generalized version of `Localization.formatValue`. Keys must - * be `{id, args}` objects. - * - * docL10n.formatValues([ - * {id: 'hello', args: { who: 'Mary' }}, - * {id: 'hello', args: { who: 'John' }}, - * {id: 'welcome'} - * ]).then(console.log); - * - * // ['Hello, Mary!', 'Hello, John!', 'Welcome!'] - * - * Returns a Promise resolving to an array of the translation strings. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @returns {Promise>} - */ - formatValues(resourceIds, bundles, keys) { - return this.formatWithFallback(resourceIds, bundles, keys, valueFromBundle); - }, - - /** - * Sync version of `formatValues`. - * - * Returns an array of the translation strings. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {Array} keys - Translation keys to format. - * @returns {Array} - * @private - */ - formatValuesSync(resourceIds, bundles, keys) { - return this.formatWithFallbackSync(resourceIds, bundles, keys, valueFromBundle); - }, - - /** - * Retrieve the translation corresponding to the `id` identifier. - * - * If passed, `args` is a simple hash object with a list of variables that - * will be interpolated in the value of the translation. - * - * docL10n.formatValue( - * 'hello', { who: 'world' } - * ).then(console.log); - * - * // 'Hello, world!' - * - * Returns a Promise resolving to a translation string. - * - * Use this sparingly for one-off messages which don't need to be - * retranslated when the user changes their language preferences, e.g. in - * notifications. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {string} id - Identifier of the translation to format - * @param {Object} [args] - Optional external arguments - * @returns {Promise} - */ - async formatValue(resourceIds, bundles, id, args) { - const [val] = await this.formatValues(resourceIds, bundles, [{id, args}]); - return val; - }, - - /** - * Sync version of `formatValue`. - * - * Returns a translation string. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {Iter} bundles - Iterator over bundles. - * @param {string} id - Identifier of the translation to format - * @param {Object} [args] - Optional external arguments - * @returns {string?} - * @private - */ - formatValueSync(resourceIds, bundles, id, args) { - const [val] = this.formatValuesSync(resourceIds, bundles, [{id, args}]); - return val; - }, - - /** - * This method should be called when there's a reason to believe - * that language negotiation or available resources changed. - * - * @param {Array} resourceIds - List of resource ids used by this - * localization. - * @param {bool} isSync - Whether the instance should be - * synchronous. - * @param {bool} eager - whether the I/O for new context should begin eagerly - * @param {Function} generateBundles - Custom FluentBundle asynchronous generator. - * @param {Function} generateBundlesSync - Custom FluentBundle generator. - * @returns {Iter} - */ - generateBundles(resourceIds, isSync, eager = false, generateBundles = defaultGenerateBundles, generateBundlesSync = defaultGenerateBundlesSync) { - // Store for error reporting from `formatWithFallback`. - let generateMessages = isSync ? generateBundlesSync : generateBundles; - let bundles = this.cached(generateMessages(resourceIds), isSync); - if (eager) { - // If the first app locale is the same as last fallback - // it means that we have all resources in this locale, and - // we want to eagerly fetch just that one. - // Otherwise, we're in a scenario where the first locale may - // be partial and we want to eagerly fetch a fallback as well. - const appLocale = Services.locale.appLocaleAsBCP47; - const lastFallback = Services.locale.lastFallbackLocale; - const prefetchCount = appLocale === lastFallback ? 1 : 2; - bundles.touchNext(prefetchCount); - } - return bundles; - }, -} - -/** - * Format the value of a message into a string or `null`. - * - * This function is passed as a method to `keysFromBundle` and resolve - * a value of a single L10n Entity using provided `FluentBundle`. - - * If the message doesn't have a value, return `null`. - * - * @param {FluentBundle} bundle - * @param {Array} errors - * @param {Object} message - * @param {Object} args - * @returns {string?} - * @private - */ -function valueFromBundle(bundle, errors, message, args) { - if (message.value) { - return bundle.formatPattern(message.value, args, errors); - } - - return null; -} - -/** - * Format all public values of a message into a {value, attributes} object. - * - * This function is passed as a method to `keysFromBundle` and resolve - * a single L10n Entity using provided `FluentBundle`. - * - * The function will return an object with a value and attributes of the - * entity. - * - * @param {FluentBundle} bundle - * @param {Array} errors - * @param {Object} message - * @param {Object} args - * @returns {Object} - * @private - */ -function messageFromBundle(bundle, errors, message, args) { - const formatted = { - value: null, - attributes: null, - }; - - if (message.value) { - formatted.value = bundle.formatPattern(message.value, args, errors); - } - - let attrNames = Object.keys(message.attributes); - if (attrNames.length > 0) { - formatted.attributes = new Array(attrNames.length); - for (let [i, name] of attrNames.entries()) { - let value = bundle.formatPattern(message.attributes[name], args, errors); - formatted.attributes[i] = {name, value}; - } - } - - return formatted; -} - -/** - * This function is an inner function for `Localization.formatWithFallback`. - * - * It takes a `FluentBundle`, list of l10n-ids and a method to be used for - * key resolution (either `valueFromBundle` or `messageFromBundle`) and - * optionally a value returned from `keysFromBundle` executed against - * another `FluentBundle`. - * - * The idea here is that if the previous `FluentBundle` did not resolve - * all keys, we're calling this function with the next context to resolve - * the remaining ones. - * - * In the function, we loop over `keys` and check if we have the `prev` - * passed and if it has an error entry for the position we're in. - * - * If it doesn't, it means that we have a good translation for this key and - * we return it. If it does, we'll try to resolve the key using the passed - * `FluentBundle`. - * - * In the end, we fill the translations array, and return the Set with - * missing ids. - * - * See `Localization.formatWithFallback` for more info on how this is used. - * - * @param {Function} method - * @param {FluentBundle} bundle - * @param {Array} keys - * @param {{Array<{value: string, attributes: Object}>}} translations - * - * @returns {Set} - * @private - */ -function keysFromBundle(method, bundle, keys, translations) { - const messageErrors = []; - const missingIds = new Set(); - - keys.forEach((key, i) => { - let id; - let args = undefined; - if (typeof key == "object" && "id" in key) { - id = String(key.id); - args = key.args; - } else { - id = String(key); - } - - if (translations[i] !== null) { - return; - } - - let message = bundle.getMessage(id); - if (message) { - messageErrors.length = 0; - translations[i] = method(bundle, messageErrors, message, args); - if (messageErrors.length > 0) { - const locale = bundle.locales[0]; - const errors = messageErrors.join(", "); - maybeReportErrorToGecko(`[fluent][resolver] errors in ${locale}/${id}: ${errors}.`); - } - } else { - missingIds.add(id); - } - }); - - return missingIds; -} - -this.Localization = Localization; -var EXPORTED_SYMBOLS = ["Localization"]; diff --git a/intl/l10n/README b/intl/l10n/README index ac157e004e29..d507db0522b4 100644 --- a/intl/l10n/README +++ b/intl/l10n/README @@ -1,7 +1,7 @@ The content of this directory is partially sourced from the fluent.js project. The following files are affected: - - Localization.jsm + - FluentSyntax.jsm At the moment, the tool used to produce those files in fluent.js repository, doesn't fully align with how the code is structured here, so we perform a manual adjustments diff --git a/intl/l10n/docs/fluent/tutorial.rst b/intl/l10n/docs/fluent/tutorial.rst index 6114b0e65d32..fe20e7e28714 100644 --- a/intl/l10n/docs/fluent/tutorial.rst +++ b/intl/l10n/docs/fluent/tutorial.rst @@ -495,10 +495,6 @@ Localization object manually using the `Localization` class: .. code-block:: javascript - const { Localization } = - ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); - - const myL10n = new Localization([ "branding/brand.ftl", "browser/preferences/preferences.ftl" @@ -528,10 +524,6 @@ on the class. .. code-block:: javascript - const { Localization } = - ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); - - const myL10n = new Localization([ "branding/brand.ftl", "browser/preferences/preferences.ftl" diff --git a/intl/l10n/moz.build b/intl/l10n/moz.build index 4583838ab7d7..ec31f70e0c61 100644 --- a/intl/l10n/moz.build +++ b/intl/l10n/moz.build @@ -23,10 +23,6 @@ UNIFIED_SOURCES += [ "Localization.cpp", ] -EXTRA_JS_MODULES += [ - "Localization.jsm", -] - TESTING_JS_MODULES += [ "FluentSyntax.jsm", ]