From e3c2f73edd00278ebd26075ed2de7018f7e20629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Tue, 12 Dec 2017 16:42:54 -0600 Subject: [PATCH] Add mapContextAsync This is the async counterpart to mapContextSync. Given an async iterable of MessageContext instance and an array of ids (or a single id), it maps each identifier to the first MessageContext which contains the message for it. An ordered interable 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 iterable of MessageContexts can now be async, allowing code like this: async formatString(id, args) { const ctx = await mapContextAsync(contexts, id); if (ctx === null) { return id; } const msg = ctx.getMessage(id); return ctx.format(msg, args); } The iterable of MessageContexts should always be wrapped in CachedIterable to optimize subsequent calls to mapContextSync and mapContextAsync. --- compat_config.js | 2 +- fluent/src/fallback.js | 34 +++++++- fluent/src/index.js | 2 +- fluent/test/fallback_async_test.js | 87 +++++++++++++++++++ ...fallback_test.js => fallback_sync_test.js} | 4 +- 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 fluent/test/fallback_async_test.js rename fluent/test/{fallback_test.js => fallback_sync_test.js} (96%) diff --git a/compat_config.js b/compat_config.js index 2e2116817..0e84224c4 100644 --- a/compat_config.js +++ b/compat_config.js @@ -22,7 +22,7 @@ export default { 'plugins': [ // Cf. https://github.com/rollup/rollup-plugin-babel#helpers 'external-helpers', - ['babel-plugin-transform-builtin-extend', { + ['transform-builtin-extend', { globals: ['Error'] }] ] diff --git a/fluent/src/fallback.js b/fluent/src/fallback.js index 7793739c7..5924490c3 100644 --- a/fluent/src/fallback.js +++ b/fluent/src/fallback.js @@ -48,7 +48,6 @@ * @param {string|Array} ids * @returns {MessageContext|Array} */ - export function mapContextSync(iterable, ids) { if (!Array.isArray(ids)) { return getContextForId(iterable, ids); @@ -71,3 +70,36 @@ function getContextForId(iterable, id) { return null; } + +/* + * Asynchronously map an identifier or an array of identifiers to the best + * `MessageContext` instance(s). + * + * @param {AsyncIterable} iterable + * @param {string|Array} ids + * @returns {MessageContext|Array} + */ +export async function mapContextAsync(iterable, ids) { + if (!Array.isArray(ids)) { + for await (const context of iterable) { + if (context.hasMessage(ids)) { + return context; + } + } + } + + const foundContexts = new Array(ids.length).fill(null); + + for await (const context of iterable) { + // XXX Switch to const [index, id] of id.entries() when we move to Babel 7. + // See https://github.com/babel/babel/issues/5880. + for (let index = 0; index < ids.length; index++) { + const id = ids[index]; + if (!foundContexts[index] && context.hasMessage(id)) { + foundContexts[index] = context; + } + } + } + + return foundContexts; +} diff --git a/fluent/src/index.js b/fluent/src/index.js index 4e61c23dc..0bfbfc68b 100644 --- a/fluent/src/index.js +++ b/fluent/src/index.js @@ -17,4 +17,4 @@ export { } from './types'; export { default as CachedIterable } from './cached_iterable'; -export { mapContextSync } from './fallback'; +export { mapContextSync, mapContextAsync } from './fallback'; diff --git a/fluent/test/fallback_async_test.js b/fluent/test/fallback_async_test.js new file mode 100644 index 000000000..16ef0e69b --- /dev/null +++ b/fluent/test/fallback_async_test.js @@ -0,0 +1,87 @@ +import assert from 'assert'; + +import CachedIterable from '../src/cached_iterable'; +import MessageContext from './message_context_stub'; +import { mapContextAsync } from '../src/index'; + +suite('Async Fallback — single id', function() { + let ctx1, ctx2; + + suiteSetup(function() { + ctx1 = new MessageContext(); + ctx1._setMessages(['bar']); + ctx2 = new MessageContext(); + ctx2._setMessages(['foo', 'bar']); + }); + + test('eager iterable', async function() { + const contexts = new CachedIterable([ctx1, ctx2]); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + }); + + test('eager iterable works more than once', async function() { + const contexts = new CachedIterable([ctx1, ctx2]); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + }); + + test('lazy iterable', async function() { + async function *generateMessages() { + yield *[ctx1, ctx2]; + } + + const contexts = new CachedIterable(generateMessages()); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + }); + + test('lazy iterable works more than once', async function() { + async function *generateMessages() { + yield *[ctx1, ctx2]; + } + + const contexts = new CachedIterable(generateMessages()); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + assert.equal(await mapContextAsync(contexts, 'foo'), ctx2); + assert.equal(await mapContextAsync(contexts, 'bar'), ctx1); + }); +}); + +suite('Async Fallback — multiple ids', async function() { + let ctx1, ctx2; + + suiteSetup(function() { + ctx1 = new MessageContext(); + ctx1._setMessages(['foo', 'bar']); + ctx2 = new MessageContext(); + ctx2._setMessages(['foo', 'bar', 'baz']); + }); + + test('existing translations', async function() { + const contexts = new CachedIterable([ctx1, ctx2]); + assert.deepEqual( + await mapContextAsync(contexts, ['foo', 'bar']), + [ctx1, ctx1] + ); + }); + + test('fallback translations', async function() { + const contexts = new CachedIterable([ctx1, ctx2]); + assert.deepEqual( + await mapContextAsync(contexts, ['foo', 'bar', 'baz']), + [ctx1, ctx1, ctx2] + ); + }); + + test('missing translations', async function() { + const contexts = new CachedIterable([ctx1, ctx2]); + assert.deepEqual( + await mapContextAsync(contexts, ['foo', 'bar', 'baz', 'qux']), + [ctx1, ctx1, ctx2, null] + ); + }); +}); diff --git a/fluent/test/fallback_test.js b/fluent/test/fallback_sync_test.js similarity index 96% rename from fluent/test/fallback_test.js rename to fluent/test/fallback_sync_test.js index 4e9520364..7438d2e9d 100644 --- a/fluent/test/fallback_test.js +++ b/fluent/test/fallback_sync_test.js @@ -4,7 +4,7 @@ import CachedIterable from '../src/cached_iterable'; import MessageContext from './message_context_stub'; import { mapContextSync } from '../src/index'; -suite('Fallback — single id', function() { +suite('Sync Fallback — single id', function() { let ctx1, ctx2; suiteSetup(function() { @@ -51,7 +51,7 @@ suite('Fallback — single id', function() { }); }); -suite('Fallback — multiple ids', function() { +suite('Sync Fallback — multiple ids', function() { let ctx1, ctx2; suiteSetup(function() {