--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -31,17 +31,17 @@ Components.utils.importGlobalProperties(
* ]
*
* If the user will request:
* L10nRegistry.generateContexts(['de', 'en-US'], [
* '/browser/menu.ftl',
* '/platform/toolkit.ftl'
* ]);
*
- * the generator will return an iterator over the following contexts:
+ * the generator will return an async iterator over the following contexts:
*
* {
* locale: 'de',
* resources: [
* ['app', '/browser/menu.ftl'],
* ['app', '/platform/toolkit.ftl'],
* ]
* },
@@ -80,19 +80,19 @@ const L10nRegistry = {
ctxCache: new Map(),
/**
* Based on the list of requested languages and resource Ids,
* this function returns an lazy iterator over message context permutations.
*
* @param {Array} requestedLangs
* @param {Array} resourceIds
- * @returns {Iterator<MessageContext>}
+ * @returns {AsyncIterator<MessageContext>}
*/
- * generateContexts(requestedLangs, resourceIds) {
+ async * generateContexts(requestedLangs, resourceIds) {
const sourcesOrder = Array.from(this.sources.keys()).reverse();
for (const locale of requestedLangs) {
yield * generateContextsForLocale(locale, sourcesOrder, resourceIds);
}
},
/**
* Adds a new resource source to the L10nRegistry.
@@ -174,19 +174,19 @@ function generateContextID(locale, sourc
* This function is called recursively to generate all possible permutations
* and uses the last, optional parameter, to pass the already resolved
* sources order.
*
* @param {String} locale
* @param {Array} sourcesOrder
* @param {Array} resourceIds
* @param {Array} [resolvedOrder]
- * @returns {Iterator<MessageContext>}
+ * @returns {AsyncIterator<MessageContext>}
*/
-function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedOrder = []) {
+async function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedOrder = []) {
const resolvedLength = resolvedOrder.length;
const resourcesLength = resourceIds.length;
// Inside that loop we have a list of resources and the sources for them, like this:
// ['test.ftl', 'menu.ftl', 'foo.ftl']
// ['app', 'platform', 'app']
for (const sourceName of sourcesOrder) {
const order = resolvedOrder.concat(sourceName);
@@ -197,150 +197,202 @@ function* generateContextsForLocale(loca
// have to perform the I/O to learn.
if (L10nRegistry.sources.get(sourceName).hasFile(locale, resourceIds[resolvedOrder.length]) === false) {
continue;
}
// If the number of resolved sources equals the number of resources,
// create the right context and return it if it loads.
if (resolvedLength + 1 === resourcesLength) {
- yield generateContext(locale, order, resourceIds);
+ const ctx = await generateContext(locale, order, resourceIds);
+ if (ctx !== null) {
+ yield ctx;
+ }
} else {
// otherwise recursively load another generator that walks over the
// partially resolved list of sources.
yield * generateContextsForLocale(locale, sourcesOrder, resourceIds, order);
}
}
}
/**
* Generates a single MessageContext by loading all resources
* from the listed sources for a given locale.
*
+ * The function casts all error cases into a Promise that resolves with
+ * value `null`.
+ * This allows the caller to be an async generator without using
+ * try/catch clauses.
+ *
* @param {String} locale
* @param {Array} sourcesOrder
* @param {Array} resourceIds
* @returns {Promise<MessageContext>}
*/
-async function generateContext(locale, sourcesOrder, resourceIds) {
+function generateContext(locale, sourcesOrder, resourceIds) {
const ctxId = generateContextID(locale, sourcesOrder, resourceIds);
- if (!L10nRegistry.ctxCache.has(ctxId)) {
- const ctx = new MessageContext(locale);
- for (let i = 0; i < resourceIds.length; i++) {
- const data = await L10nRegistry.sources.get(sourcesOrder[i]).fetchFile(locale, resourceIds[i]);
- if (data === null) {
- return false;
+ if (L10nRegistry.ctxCache.has(ctxId)) {
+ return L10nRegistry.ctxCache.get(ctxId);
+ }
+
+ const fetchPromises = resourceIds.map((resourceId, i) => {
+ return L10nRegistry.sources.get(sourcesOrder[i]).fetchFile(locale, resourceId);
+ });
+
+ const ctxPromise = Promise.all(fetchPromises).then(
+ dataSets => {
+ const ctx = new MessageContext(locale);
+ for (const data of dataSets) {
+ if (data === null) {
+ return null;
+ }
+ ctx.addMessages(data);
}
- ctx.addMessages(data);
- }
- L10nRegistry.ctxCache.set(ctxId, ctx);
- }
- return L10nRegistry.ctxCache.get(ctxId);
+ return ctx;
+ },
+ () => null
+ );
+ L10nRegistry.ctxCache.set(ctxId, ctxPromise);
+ return ctxPromise;
}
/**
* This is a basic Source for L10nRegistry.
* It registers its own locales and a pre-path, and when asked for a file
* it attempts to download and cache it.
*
* The Source caches the downloaded files so any consecutive loads will
* come from the cache.
**/
class FileSource {
+ /**
+ * @param {string} name
+ * @param {Array<string>} locales
+ * @param {string} prePath
+ *
+ * @returns {IndexedFileSource}
+ */
constructor(name, locales, prePath) {
this.name = name;
this.locales = locales;
this.prePath = prePath;
+ this.indexed = false;
+
+ // The cache object stores information about the resources available
+ // in the Source.
+ //
+ // It can take one of three states:
+ // * true - the resource is available but not fetched yet
+ // * false - the resource is not available
+ // * Promise - the resource has been fetched
+ //
+ // If the cache has no entry for a given path, that means that there
+ // is no information available about whether the resource is available.
+ //
+ // If the `indexed` property is set to `true` it will be treated as the
+ // resource not being available. Otherwise, the resource may be
+ // available and we do not have any information about it yet.
this.cache = {};
}
getPath(locale, path) {
return (this.prePath + path).replace(/\{locale\}/g, locale);
}
hasFile(locale, path) {
if (!this.locales.includes(locale)) {
return false;
}
const fullPath = this.getPath(locale, path);
if (!this.cache.hasOwnProperty(fullPath)) {
- return undefined;
+ return this.indexed ? false : undefined;
}
-
- if (this.cache[fullPath] === null) {
+ if (this.cache[fullPath] === false) {
return false;
}
+ if (this.cache[fullPath].then) {
+ return undefined;
+ }
return true;
}
- async fetchFile(locale, path) {
+ fetchFile(locale, path) {
if (!this.locales.includes(locale)) {
- return null;
+ return Promise.reject(`The source has no resources for locale "${locale}"`);
}
const fullPath = this.getPath(locale, path);
- if (this.hasFile(locale, path) === undefined) {
- let file = await L10nRegistry.load(fullPath);
- if (file === undefined) {
- this.cache[fullPath] = null;
- } else {
- this.cache[fullPath] = file;
+ if (this.cache.hasOwnProperty(fullPath)) {
+ if (this.cache[fullPath] === false) {
+ return Promise.reject(`The source has no resources for path "${fullPath}"`);
+ }
+ if (this.cache[fullPath].then) {
+ return this.cache[fullPath];
+ }
+ } else {
+ if (this.indexed) {
+ return Promise.reject(`The source has no resources for path "${fullPath}"`);
}
}
- return this.cache[fullPath];
+ return this.cache[fullPath] = L10nRegistry.load(fullPath).then(
+ data => {
+ return this.cache[fullPath] = data;
+ },
+ err => {
+ this.cache[fullPath] = false;
+ return Promise.reject(err);
+ }
+ );
}
}
/**
* This is an extension of the FileSource which should be used
* for sources that can provide the list of files available in the source.
*
* This allows for a faster lookup in cases where the source does not
* contain most of the files that the app will request for (e.g. an addon).
**/
class IndexedFileSource extends FileSource {
+ /**
+ * @param {string} name
+ * @param {Array<string>} locales
+ * @param {string} prePath
+ * @param {Array<string>} paths
+ *
+ * @returns {IndexedFileSource}
+ */
constructor(name, locales, prePath, paths) {
super(name, locales, prePath);
- this.paths = paths;
- }
-
- hasFile(locale, path) {
- if (!this.locales.includes(locale)) {
- return false;
- }
- const fullPath = this.getPath(locale, path);
- return this.paths.includes(fullPath);
- }
-
- async fetchFile(locale, path) {
- if (!this.locales.includes(locale)) {
- return null;
- }
-
- const fullPath = this.getPath(locale, path);
- if (this.paths.includes(fullPath)) {
- let file = await L10nRegistry.load(fullPath);
-
- if (file === undefined) {
- return null;
- } else {
- return file;
- }
- } else {
- return null;
+ this.indexed = true;
+ for (const path of paths) {
+ this.cache[path] = true;
}
}
}
/**
+ * The low level wrapper around Fetch API. It unifies the error scenarios to
+ * always produce a promise rejection.
+ *
* We keep it as a method to make it easier to override for testing purposes.
- **/
+ *
+ * @param {string} url
+ *
+ * @returns {Promise<string>}
+ */
L10nRegistry.load = function(url) {
- return fetch(url).then(data => data.text()).catch(() => undefined);
+ return fetch(url).then(response => {
+ if (!response.ok) {
+ return Promise.reject(response.statusText);
+ }
+ return response.text()
+ });
};
this.L10nRegistry = L10nRegistry;
this.FileSource = FileSource;
this.IndexedFileSource = IndexedFileSource;
this.EXPORTED_SYMBOLS = ['L10nRegistry', 'FileSource', 'IndexedFileSource'];
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -31,32 +31,32 @@ const ObserverService = Cc["@mozilla.org
/**
* CachedIterable caches the elements yielded by an iterable.
*
* It can be used to iterate over an iterable many times without depleting the
* iterable.
*/
class CachedIterable {
constructor(iterable) {
- if (!(Symbol.iterator in Object(iterable))) {
- throw new TypeError('Argument must implement the iteration protocol.');
+ if (!(Symbol.asyncIterator in Object(iterable))) {
+ throw new TypeError('Argument must implement the async iteration protocol.');
}
- this.iterator = iterable[Symbol.iterator]();
+ this.iterator = iterable[Symbol.asyncIterator]();
this.seen = [];
}
- [Symbol.iterator]() {
+ [Symbol.asyncIterator]() {
const { seen, iterator } = this;
let cur = 0;
return {
- next() {
+ async next() {
if (seen.length <= cur) {
- seen.push(iterator.next());
+ seen.push(await iterator.next());
}
return seen[cur++];
}
};
}
}
/**
@@ -126,17 +126,17 @@ class Localization {
*
* @param {Array<Array>} keys - Translation keys to format.
* @param {Function} method - Formatting function.
* @returns {Promise<Array<string|Object>>}
* @private
*/
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') {
ctx = await ctx;
}
const errors = keysFromContext(method, ctx, keys, translations);
if (!errors) {
break;
--- a/intl/l10n/test/dom/test_domloc.xul
+++ b/intl/l10n/test/dom/test_domloc.xul
@@ -11,17 +11,17 @@
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script type="application/javascript">
<![CDATA[
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * generateMessages(locales, resourceIds) {
+ async function * generateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages(`
file-menu
.label = File
.accesskey = F
new-tab
.label = New Tab
.accesskey = N
--- a/intl/l10n/test/dom/test_domloc_connectRoot.html
+++ b/intl/l10n/test/dom/test_domloc_connectRoot.html
@@ -5,17 +5,17 @@
<title>Test DOMLocalization.prototype.connectRoot</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
[],
--- a/intl/l10n/test/dom/test_domloc_disconnectRoot.html
+++ b/intl/l10n/test/dom/test_domloc_disconnectRoot.html
@@ -5,17 +5,17 @@
<title>Test DOMLocalization.prototype.disconnectRoot</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
[],
--- a/intl/l10n/test/dom/test_domloc_getAttributes.html
+++ b/intl/l10n/test/dom/test_domloc_getAttributes.html
@@ -5,17 +5,17 @@
<title>Test DOMLocalization.prototype.getAttributes</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {}
+ async function * mockGenerateMessages(locales, resourceIds) {}
window.onload = function () {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
[],
mockGenerateMessages
--- a/intl/l10n/test/dom/test_domloc_mutations.html
+++ b/intl/l10n/test/dom/test_domloc_mutations.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Hello World');
mc.addMessages('title2 = Hello Another World');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_overlay.html
+++ b/intl/l10n/test/dom/test_domloc_overlay.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = <strong>Hello</strong> World');
mc.addMessages('title2 = This is <a>a link</a>!');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Visit <a>Mozilla</a> or <a>Firefox</a> website!');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_overlay_repeated.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_repeated.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Visit <a>Mozilla</a> or <a>Firefox</a> website!');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_setAttributes.html
+++ b/intl/l10n/test/dom/test_domloc_setAttributes.html
@@ -5,17 +5,17 @@
<title>Test DOMLocalization.prototype.setAttributes</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {}
+ async function * mockGenerateMessages(locales, resourceIds) {}
window.onload = function () {
SimpleTest.waitForExplicitFinish();
const domLoc = new DOMLocalization(
window,
[],
mockGenerateMessages
--- a/intl/l10n/test/dom/test_domloc_translateElement.html
+++ b/intl/l10n/test/dom/test_domloc_translateElement.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Hello World');
mc.addMessages('link\n .title = Click me');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_translateFragment.html
+++ b/intl/l10n/test/dom/test_domloc_translateFragment.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Hello World');
mc.addMessages('subtitle = Welcome to Fluent');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_translateRoots.html
+++ b/intl/l10n/test/dom/test_domloc_translateRoots.html
@@ -7,17 +7,17 @@
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
"use strict";
const { DOMLocalization } =
Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
const { MessageContext } =
Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
- function * mockGenerateMessages(locales, resourceIds) {
+ async function * mockGenerateMessages(locales, resourceIds) {
const mc = new MessageContext(locales);
mc.addMessages('title = Hello World');
mc.addMessages('title2 = Hello Another World');
yield mc;
}
window.onload = async function () {
SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/test_l10nregistry.js
+++ b/intl/l10n/test/test_l10nregistry.js
@@ -1,19 +1,23 @@
/* Any copyrighequal dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const {
L10nRegistry,
FileSource,
IndexedFileSource
} = Components.utils.import("resource://gre/modules/L10nRegistry.jsm", {});
+Components.utils.import("resource://gre/modules/Timer.jsm");
let fs;
L10nRegistry.load = async function(url) {
+ if (!fs.hasOwnProperty(url)) {
+ return Promise.reject('Resource unavailable');
+ }
return fs[url];
}
add_task(function test_methods_presence() {
equal(typeof L10nRegistry.generateContexts, "function");
equal(typeof L10nRegistry.getAvailableLocales, "function");
equal(typeof L10nRegistry.registerSource, "function");
equal(typeof L10nRegistry.updateSource, "function");
@@ -22,24 +26,23 @@ add_task(function test_methods_presence(
/**
* This test tests generation of a proper context for a single
* source scenario
*/
add_task(async function test_methods_calling() {
fs = {
'/localization/en-US/browser/menu.ftl': 'key = Value',
};
- const originalLoad = L10nRegistry.load;
const source = new FileSource('test', ['en-US'], '/localization/{locale}');
L10nRegistry.registerSource(source);
const ctxs = L10nRegistry.generateContexts(['en-US'], ['/browser/menu.ftl']);
- const ctx = await ctxs.next().value;
+ const ctx = (await ctxs.next()).value;
equal(ctx.hasMessage('key'), true);
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
@@ -59,27 +62,27 @@ add_task(async function test_has_one_sou
equal(L10nRegistry.sources.size, 1);
equal(L10nRegistry.sources.has('app'), true);
// returns a single context
let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl']);
- let ctx0 = await ctxs.next().value;
+ let ctx0 = (await ctxs.next()).value;
equal(ctx0.hasMessage('key'), true);
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// returns no contexts for missing locale
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
/**
* This test verifies that public methods return expected values
@@ -102,41 +105,41 @@ add_task(async function test_has_two_sou
equal(L10nRegistry.sources.size, 2);
equal(L10nRegistry.sources.has('app'), true);
equal(L10nRegistry.sources.has('platform'), true);
// returns correct contexts for en-US
let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl']);
- let ctx0 = await ctxs.next().value;
+ let ctx0 = (await ctxs.next()).value;
equal(ctx0.hasMessage('key'), true);
let msg = ctx0.getMessage('key');
equal(ctx0.format(msg), 'platform value');
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// returns correct contexts for [pl, en-US]
ctxs = L10nRegistry.generateContexts(['pl', 'en-US'], ['test.ftl']);
- ctx0 = await ctxs.next().value;
+ ctx0 = (await ctxs.next()).value;
equal(ctx0.locales[0], 'pl');
equal(ctx0.hasMessage('key'), true);
let msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'app value');
- let ctx1 = await ctxs.next().value;
+ let ctx1 = (await ctxs.next()).value;
equal(ctx1.locales[0], 'en-US');
equal(ctx1.hasMessage('key'), true);
let msg1 = ctx1.getMessage('key');
equal(ctx1.format(msg1), 'platform value');
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
/**
* This test verifies that behavior specific to the IndexedFileSource
@@ -183,29 +186,29 @@ add_task(async function test_override()
'/app/data/locales/pl/test.ftl': 'key = value',
'/data/locales/pl/test.ftl': 'key = addon value'
};
equal(L10nRegistry.sources.size, 2);
equal(L10nRegistry.sources.has('langpack-pl'), true);
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- let ctx0 = await ctxs.next().value;
+ let ctx0 = (await ctxs.next()).value;
equal(ctx0.locales[0], 'pl');
equal(ctx0.hasMessage('key'), true);
let msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'addon value');
- let ctx1 = await ctxs.next().value;
+ let ctx1 = (await ctxs.next()).value;
equal(ctx1.locales[0], 'pl');
equal(ctx1.hasMessage('key'), true);
let msg1 = ctx1.getMessage('key');
equal(ctx1.format(msg1), 'value');
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
/**
* This test verifies that new contexts are returned
@@ -216,32 +219,32 @@ add_task(async function test_updating()
'/data/locales/pl/test.ftl',
]);
L10nRegistry.registerSource(oneSource);
fs = {
'/data/locales/pl/test.ftl': 'key = value'
};
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- let ctx0 = await ctxs.next().value;
+ let ctx0 = (await ctxs.next()).value;
equal(ctx0.locales[0], 'pl');
equal(ctx0.hasMessage('key'), true);
let msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'value');
const newSource = new IndexedFileSource('langpack-pl', ['pl'], '/data/locales/{locale}/', [
'/data/locales/pl/test.ftl'
]);
fs['/data/locales/pl/test.ftl'] = 'key = new value';
L10nRegistry.updateSource(newSource);
equal(L10nRegistry.sources.size, 1);
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- ctx0 = await ctxs.next().value;
+ ctx0 = (await ctxs.next()).value;
msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'new value');
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
@@ -262,51 +265,151 @@ add_task(async function test_removing()
'/app/data/locales/pl/test.ftl': 'key = value',
'/data/locales/pl/test.ftl': 'key = addon value'
};
equal(L10nRegistry.sources.size, 2);
equal(L10nRegistry.sources.has('langpack-pl'), true);
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- let ctx0 = await ctxs.next().value;
+ let ctx0 = (await ctxs.next()).value;
equal(ctx0.locales[0], 'pl');
equal(ctx0.hasMessage('key'), true);
let msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'addon value');
- let ctx1 = await ctxs.next().value;
+ let ctx1 = (await ctxs.next()).value;
equal(ctx1.locales[0], 'pl');
equal(ctx1.hasMessage('key'), true);
let msg1 = ctx1.getMessage('key');
equal(ctx1.format(msg1), 'value');
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// Remove langpack
L10nRegistry.removeSource('langpack-pl');
equal(L10nRegistry.sources.size, 1);
equal(L10nRegistry.sources.has('langpack-pl'), false);
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- ctx0 = await ctxs.next().value;
+ ctx0 = (await ctxs.next()).value;
equal(ctx0.locales[0], 'pl');
equal(ctx0.hasMessage('key'), true);
msg0 = ctx0.getMessage('key');
equal(ctx0.format(msg0), 'value');
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
// Remove app source
L10nRegistry.removeSource('app');
equal(L10nRegistry.sources.size, 0);
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
- equal(ctxs.next().done, true);
+ equal((await ctxs.next()).done, true);
+
+ // cleanup
+ L10nRegistry.sources.clear();
+ L10nRegistry.ctxCache.clear();
+});
+
+/**
+ * This test verifies that the logic works correctly when there's a missing
+ * file in the FileSource scenario.
+ */
+add_task(async function test_missing_file() {
+ let oneSource = new FileSource('app', ['en-US'], './app/data/locales/{locale}/');
+ L10nRegistry.registerSource(oneSource);
+ let twoSource = new FileSource('platform', ['en-US'], './platform/data/locales/{locale}/');
+ L10nRegistry.registerSource(twoSource);
+
+ fs = {
+ './app/data/locales/en-US/test.ftl': 'key = value en-US',
+ './platform/data/locales/en-US/test.ftl': 'key = value en-US',
+ './platform/data/locales/en-US/test2.ftl': 'key2 = value2 en-US'
+ };
+
+
+ // has two sources
+
+ equal(L10nRegistry.sources.size, 2);
+ equal(L10nRegistry.sources.has('app'), true);
+ equal(L10nRegistry.sources.has('platform'), true);
+
+
+ // returns a single context
+
+ let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl', 'test2.ftl']);
+ let ctx0 = (await ctxs.next()).value;
+ let ctx1 = (await ctxs.next()).value;
+
+ equal((await ctxs.next()).done, true);
+
// cleanup
L10nRegistry.sources.clear();
L10nRegistry.ctxCache.clear();
});
+
+/**
+ * This test verifies that each file is that all files requested
+ * by a single context are fetched at the same time, even
+ * if one I/O is slow.
+ */
+add_task(async function test_parallel_io() {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ let originalLoad = L10nRegistry.load;
+ let fetchIndex = new Map();
+
+ L10nRegistry.load = function(url) {
+ if (!fetchIndex.has(url)) {
+ fetchIndex.set(url, 0);
+ }
+ fetchIndex.set(url, fetchIndex.get(url) + 1);
+
+ if (url === '/en-US/slow-file.ftl') {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Despite slow-file being the first on the list,
+ // by the time the it finishes loading, the other
+ // two files are already fetched.
+ equal(fetchIndex.get('/en-US/test.ftl'), 1);
+ equal(fetchIndex.get('/en-US/test2.ftl'), 1);
+
+ resolve('');
+ }, 10);
+ });
+ };
+ return Promise.resolve('');
+ }
+ let oneSource = new FileSource('app', ['en-US'], '/{locale}/');
+ L10nRegistry.registerSource(oneSource);
+
+ fs = {
+ '/en-US/test.ftl': 'key = value en-US',
+ '/en-US/test2.ftl': 'key2 = value2 en-US',
+ '/en-US/slow-file.ftl': 'key-slow = value slow en-US',
+ };
+
+ // returns a single context
+
+ let ctxs = L10nRegistry.generateContexts(['en-US'], ['slow-file.ftl', 'test.ftl', 'test2.ftl']);
+
+ equal(fetchIndex.size, 0);
+
+ let ctx0 = await ctxs.next();
+
+ equal(ctx0.done, false);
+
+ equal((await ctxs.next()).done, true);
+
+ // When requested again, the cache should make the load operation not
+ // increase the fetchedIndex count
+ let ctxs2= L10nRegistry.generateContexts(['en-US'], ['test.ftl', 'test2.ftl', 'slow-file.ftl']);
+
+ // cleanup
+ L10nRegistry.sources.clear();
+ L10nRegistry.ctxCache.clear();
+ L10nRegistry.load = originalLoad;
+});
--- a/intl/l10n/test/test_localization.js
+++ b/intl/l10n/test/test_localization.js
@@ -18,25 +18,25 @@ add_task(async function test_methods_cal
const fs = {
'/localization/de/browser/menu.ftl': 'key = [de] Value2',
'/localization/en-US/browser/menu.ftl': 'key = [en] Value2\nkey2 = [en] Value3',
};
const originalLoad = L10nRegistry.load;
const originalRequested = LocaleService.getRequestedLocales();
- L10nRegistry.load = function(url) {
+ L10nRegistry.load = async function(url) {
return fs[url];
}
const source = new FileSource('test', ['de', 'en-US'], '/localization/{locale}');
L10nRegistry.registerSource(source);
- function * generateMessages(resIds) {
- yield * L10nRegistry.generateContexts(['de', 'en-US'], resIds);
+ async function * generateMessages(resIds) {
+ yield * await L10nRegistry.generateContexts(['de', 'en-US'], resIds);
}
const l10n = new Localization([
'/browser/menu.ftl'
], generateMessages);
let values = await l10n.formatValues([['key'], ['key2']]);