Bug 1509583 - Introduce LocalizationSync. r=stas
authorZibi Braniecki <zbraniecki@mozilla.com>
Fri, 11 Jan 2019 00:25:49 +0000
changeset 453424 f8e2d2010c07418ea0916bb6a5679e872958d6e9
parent 453423 7d8101dcf03a2bfb19b5511716100d6014c093f8
child 453425 9d7f77b05ae7ed2a809e2ecf3d7c76974c9e62f7
push id35357
push usernerli@mozilla.com
push dateFri, 11 Jan 2019 21:54:07 +0000
treeherdermozilla-central@0ce024c91511 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersstas
bugs1509583
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1509583 - Introduce LocalizationSync. r=stas Differential Revision: https://phabricator.services.mozilla.com/D13340
intl/l10n/Localization.jsm
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -109,30 +109,93 @@ class CachedAsyncIterable extends Cached
       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.generateBundles(appLocales, resourceIds);
 }
 
+function defaultGenerateBundlesSync(resourceIds) {
+  const appLocales = Services.locale.appLocalesAsBCP47;
+  return L10nRegistry.generateBundlesSync(appLocales, resourceIds);
+}
+
 /**
  * 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.
  */
 class Localization {
   /**
@@ -140,20 +203,24 @@ class Localization {
    * @param {Function}      generateBundles - Function that returns a
    *                                          generator over FluentBundles
    *
    * @returns {Localization}
    */
   constructor(resourceIds = [], generateBundles = defaultGenerateBundles) {
     this.resourceIds = resourceIds;
     this.generateBundles = generateBundles;
-    this.bundles = CachedAsyncIterable.from(
+    this.bundles = this.cached(
       this.generateBundles(this.resourceIds));
   }
 
+  cached(iterable) {
+    return CachedAsyncIterable.from(iterable);
+  }
+
   /**
    * @param {Array<String>} resourceIds - List of resource IDs
    * @param {bool}                eager - whether the I/O for new context should
    *                                      begin eagerly
    */
   addResourceIds(resourceIds, eager = false) {
     this.resourceIds.push(...resourceIds);
     this.onChange(eager);
@@ -314,17 +381,17 @@ class Localization {
 
   /**
    * This method should be called when there's a reason to believe
    * that language negotiation or available resources changed.
    *
    * @param {bool} eager - whether the I/O for new context should begin eagerly
    */
   onChange(eager = false) {
-    this.bundles = CachedAsyncIterable.from(
+    this.bundles = this.cached(
       this.generateBundles(this.resourceIds));
     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;
@@ -334,16 +401,54 @@ class Localization {
     }
   }
 }
 
 Localization.prototype.QueryInterface = ChromeUtils.generateQI([
   Ci.nsISupportsWeakReference,
 ]);
 
+class LocalizationSync extends Localization {
+  constructor(resourceIds = [], generateBundles = defaultGenerateBundlesSync) {
+    super(resourceIds, generateBundles);
+  }
+
+  cached(iterable) {
+    return CachedSyncIterable.from(iterable);
+  }
+
+  formatWithFallback(keys, method) {
+    const translations = [];
+
+    for (const bundle of this.bundles) {
+      const missingIds = keysFromBundle(method, bundle, keys, translations);
+
+      if (missingIds.size === 0) {
+        break;
+      }
+
+      if (AppConstants.NIGHTLY_BUILD || Cu.isInAutomation) {
+        const locale = bundle.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}`);
+      }
+    }
+
+    return translations;
+  }
+
+  formatValue(id, args) {
+    const [val] = this.formatValues([{id, args}]);
+    return val;
+  }
+}
+
 /**
  * Format the value of a message into a string.
  *
  * This function is passed as a method to `keysFromBundle` and resolve
  * a value of a single L10n Entity using provided `FluentBundle`.
  *
  * If the function fails to retrieve the entity, it will return an ID of it.
  * If formatting fails, it will return a partially resolved entity.
@@ -454,9 +559,10 @@ function keysFromBundle(method, bundle, 
       missingIds.add(id);
     }
   });
 
   return missingIds;
 }
 
 this.Localization = Localization;
-var EXPORTED_SYMBOLS = ["Localization"];
+this.LocalizationSync = LocalizationSync;
+var EXPORTED_SYMBOLS = ["Localization", "LocalizationSync"];