Bug 1067937 Fix translation of Loop's link-clicker UI in Google Chrome. r=dmose
authorMark Banner <standard8@mozilla.com>
Wed, 17 Sep 2014 19:23:31 +0100
changeset 218070 ff15047902268ddc1d9cfb069b5f7cf8c8b820e6
parent 218069 1c47b01eea8532dc2ec624a23b5d1bb209917509
child 218071 76dbf699bd7b311e6b6071e7ce48b5e7d3bf0a0a
push idunknown
push userunknown
push dateunknown
reviewersdmose
bugs1067937
milestone34.0a2
Bug 1067937 Fix translation of Loop's link-clicker UI in Google Chrome. r=dmose
browser/components/loop/standalone/content/index.html
browser/components/loop/standalone/content/l10n/data.ini
browser/components/loop/standalone/content/libs/l10n-gaia-02ca67948fe8.js
browser/components/loop/standalone/content/libs/l10n-gaia-4e35bf8f0569.js
browser/components/loop/test/shared/index.html
browser/components/loop/test/standalone/index.html
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -5,33 +5,36 @@
 <html>
   <head>
     <meta charset="utf-8">
     <title>Loop</title>
     <link rel="stylesheet" type="text/css" href="shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
     <link rel="stylesheet" type="text/css" href="css/webapp.css">
-    <link type="application/l10n" href="l10n/data.ini">
+    <link rel="localization" href="l10n/loop.{locale}.properties">
+
+    <meta name="locales" content="en-US" />
+    <meta name="default_locale" content="en-US" />
   </head>
   <body class="standalone">
 
     <div id="main"></div>
 
     <!-- libs -->
     <script>
       window.OTProperties = {
         cdnURL: 'shared/libs/',
       };
       window.OTProperties.assetURL = window.OTProperties.cdnURL + 'sdk-content/';
       window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
       window.OTProperties.cssURL = window.OTProperties.assetURL + 'css/ot.css';
     </script>
     <script type="text/javascript" src="shared/libs/sdk.js"></script>
-    <script type="text/javascript" src="libs/l10n-gaia-4e35bf8f0569.js"></script>
+    <script type="text/javascript" src="libs/l10n-gaia-02ca67948fe8.js"></script>
     <script type="text/javascript" src="shared/libs/react-0.11.1.js"></script>
     <script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
     <script type="text/javascript" src="shared/js/utils.js"></script>
deleted file mode 100644
--- a/browser/components/loop/standalone/content/l10n/data.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-@import url(loop.en-US.properties)
-
rename from browser/components/loop/standalone/content/libs/l10n-gaia-4e35bf8f0569.js
rename to browser/components/loop/standalone/content/libs/l10n-gaia-02ca67948fe8.js
--- a/browser/components/loop/standalone/content/libs/l10n-gaia-4e35bf8f0569.js
+++ b/browser/components/loop/standalone/content/libs/l10n-gaia-02ca67948fe8.js
@@ -570,17 +570,17 @@
 
 
 
   function PropertiesParser() {
     var parsePatterns = {
       comment: /^\s*#|^\s*$/,
       entity: /^([^=\s]+)\s*=\s*(.+)$/,
       multiline: /[^\\]\\$/,
-      macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i,
+      index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
       unicode: /\\u([0-9a-fA-F]{1,4})/g,
       entries: /[\r\n]+/,
       controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g
     };
 
     this.parse = function (ctx, source) {
       var ast = Object.create(null);
 
@@ -630,17 +630,17 @@
       if (!key) {
         obj[prop] = value;
         return;
       }
 
       if (!(prop in obj)) {
         obj[prop] = {'_': {}};
       } else if (typeof(obj[prop]) === 'string') {
-        obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}};
+        obj[prop] = {'_index': parseIndex(obj[prop]), '_': {}};
       }
       obj[prop]._[key] = value;
     }
 
     function parseEntity(id, value, ast) {
       var name, key;
 
       var pos = id.indexOf('[');
@@ -682,27 +682,32 @@
 
     function unescapeString(str) {
       if (str.lastIndexOf('\\') !== -1) {
         str = unescapeControlCharacters(str);
       }
       return unescapeUnicode(str);
     }
 
-    function parseMacro(str) {
-      var match = str.match(parsePatterns.macro);
+    function parseIndex(str) {
+      var match = str.match(parsePatterns.index);
       if (!match) {
-        throw new L10nError('Malformed macro');
+        throw new L10nError('Malformed index');
       }
-      return [match[1], match[2]];
+      var parts = Array.prototype.slice.call(match, 1);
+      return parts.filter(function(part) {
+        return !!part;
+      });
     }
   }
 
 
 
+  var KNOWN_MACROS = ['plural'];
+
   var MAX_PLACEABLE_LENGTH = 2500;
   var MAX_PLACEABLES = 100;
   var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g;
 
   function Entity(id, node, env) {
     this.id = id;
     this.env = env;
     // the dirty guard prevents cyclic or recursive references from other
@@ -734,17 +739,17 @@
       return undefined;
     }
 
     this.dirty = true;
     var val;
     // if resolve fails, we want the exception to bubble up and stop the whole
     // resolving process;  however, we still need to clean up the dirty flag
     try {
-      val = resolve(ctxdata, this.env, this.value, this.index);
+      val = resolveValue(ctxdata, this.env, this.value, this.index);
     } finally {
       this.dirty = false;
     }
     return val;
   };
 
   Entity.prototype.toString = function E_toString(ctxdata) {
     try {
@@ -767,40 +772,56 @@
     for (var key in this.attributes) {
       /* jshint -W089 */
       entity.attributes[key] = this.attributes[key].toString(ctxdata);
     }
 
     return entity;
   };
 
-  function subPlaceable(ctxdata, env, match, id) {
+  function resolveIdentifier(ctxdata, env, id) {
+    if (KNOWN_MACROS.indexOf(id) > -1) {
+      return env['__' + id];
+    }
+
     if (ctxdata && ctxdata.hasOwnProperty(id) &&
         (typeof ctxdata[id] === 'string' ||
          (typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) {
       return ctxdata[id];
     }
 
     // XXX: special case for Node.js where still:
     // '__proto__' in Object.create(null) => true
     if (id in env && id !== '__proto__') {
       if (!(env[id] instanceof Entity)) {
         env[id] = new Entity(id, env[id], env);
       }
-      var value = env[id].resolve(ctxdata);
-      if (typeof value === 'string') {
-        // prevent Billion Laughs attacks
-        if (value.length >= MAX_PLACEABLE_LENGTH) {
-          throw new L10nError('Too many characters in placeable (' +
-                              value.length + ', max allowed is ' +
-                              MAX_PLACEABLE_LENGTH + ')');
-        }
-        return value;
+      return env[id].resolve(ctxdata);
+    }
+
+    return undefined;
+  }
+
+  function subPlaceable(ctxdata, env, match, id) {
+    var value = resolveIdentifier(ctxdata, env, id);
+
+    if (typeof value === 'number') {
+      return value;
+    }
+
+    if (typeof value === 'string') {
+      // prevent Billion Laughs attacks
+      if (value.length >= MAX_PLACEABLE_LENGTH) {
+        throw new L10nError('Too many characters in placeable (' +
+                            value.length + ', max allowed is ' +
+                            MAX_PLACEABLE_LENGTH + ')');
       }
+      return value;
     }
+
     return match;
   }
 
   function interpolate(ctxdata, env, str) {
     var placeablesCount = 0;
     var value = str.replace(rePlaceables, function(match, id) {
       // prevent Quadratic Blowup attacks
       if (placeablesCount++ >= MAX_PLACEABLES) {
@@ -808,52 +829,75 @@
                             ', max allowed is ' + MAX_PLACEABLES + ')');
       }
       return subPlaceable(ctxdata, env, match, id);
     });
     placeablesCount = 0;
     return value;
   }
 
-  function resolve(ctxdata, env, expr, index) {
+  function resolveSelector(ctxdata, env, expr, index) {
+      var selector = resolveIdentifier(ctxdata, env, index[0]);
+      if (selector === undefined) {
+        throw new L10nError('Unknown selector: ' + index[0]);
+      }
+
+      if (typeof selector !== 'function') {
+        // selector is a simple reference to an entity or ctxdata
+        return selector;
+      }
+
+      var argLength = index.length - 1;
+      if (selector.length !== argLength) {
+        throw new L10nError('Macro ' + index[0] + ' expects ' +
+                            selector.length + ' argument(s), yet ' + argLength +
+                            ' given');
+      }
+
+      var argValue = resolveIdentifier(ctxdata, env, index[1]);
+
+      if (selector === env.__plural) {
+        // special cases for zero, one, two if they are defined on the hash
+        if (argValue === 0 && 'zero' in expr) {
+          return 'zero';
+        }
+        if (argValue === 1 && 'one' in expr) {
+          return 'one';
+        }
+        if (argValue === 2 && 'two' in expr) {
+          return 'two';
+        }
+      }
+
+      return selector(argValue);
+  }
+
+  function resolveValue(ctxdata, env, expr, index) {
     if (typeof expr === 'string') {
       return interpolate(ctxdata, env, expr);
     }
 
     if (typeof expr === 'boolean' ||
         typeof expr === 'number' ||
         !expr) {
       return expr;
     }
 
     // otherwise, it's a dict
-
-    if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) {
-      var argValue = ctxdata[index[1]];
-
-      // special cases for zero, one, two if they are defined on the hash
-      if (argValue === 0 && 'zero' in expr) {
-        return resolve(ctxdata, env, expr.zero);
-      }
-      if (argValue === 1 && 'one' in expr) {
-        return resolve(ctxdata, env, expr.one);
-      }
-      if (argValue === 2 && 'two' in expr) {
-        return resolve(ctxdata, env, expr.two);
-      }
-
-      var selector = env.__plural(argValue);
+    if (index) {
+      // try to use the index in order to select the right dict member
+      var selector = resolveSelector(ctxdata, env, expr, index);
       if (expr.hasOwnProperty(selector)) {
-        return resolve(ctxdata, env, expr[selector]);
+        return resolveValue(ctxdata, env, expr[selector]);
       }
     }
 
     // if there was no index or no selector was found, try 'other'
     if ('other' in expr) {
-      return resolve(ctxdata, env, expr.other);
+      return resolveValue(ctxdata, env, expr.other);
     }
 
     return undefined;
   }
 
   function compile(env, ast) {
     /* jshint -W089 */
     env = env || Object.create(null);
@@ -1066,17 +1110,17 @@
         var ast = propertiesParser.parse(ctx, source);
         self.addAST(ast);
       }
       onL10nLoaded(err);
     }
 
     var idToFetch = this.isPseudo ? ctx.defaultLocale : this.id;
     for (var i = 0; i < ctx.resLinks.length; i++) {
-      var path = ctx.resLinks[i].replace('{{locale}}', idToFetch);
+      var path = ctx.resLinks[i].replace('{locale}', idToFetch);
       var type = path.substr(path.lastIndexOf('.') + 1);
 
       switch (type) {
         case 'json':
           io.loadJSON(path, onJSONLoaded, sync);
           break;
         case 'properties':
           io.load(path, onPropLoaded, sync);
@@ -1115,17 +1159,19 @@
 
   function Context(id) {
 
     this.id = id;
     this.isReady = false;
     this.isLoading = false;
 
     this.defaultLocale = 'en-US';
+    this.availableLocales = [];
     this.supportedLocales = [];
+
     this.resLinks = [];
     this.locales = {};
 
     this._emitter = new EventEmitter();
 
 
     // Getting translations
 
@@ -1213,25 +1259,45 @@
     }
 
     function setReady(supported) {
       this.supportedLocales = supported;
       this.isReady = true;
       this._emitter.emit('ready');
     }
 
+    this.registerLocales = function (defLocale, available) {
+      /* jshint boss:true */
+      this.availableLocales = [this.defaultLocale = defLocale];
+
+      if (available) {
+        for (var i = 0, loc; loc = available[i]; i++) {
+          if (this.availableLocales.indexOf(loc) === -1) {
+            this.availableLocales.push(loc);
+          }
+        }
+      }
+    };
+
     this.requestLocales = function requestLocales() {
       if (this.isLoading && !this.isReady) {
         throw new L10nError('Context not ready');
       }
 
       this.isLoading = true;
       var requested = Array.prototype.slice.call(arguments);
+      if (requested.length === 0) {
+        throw new L10nError('No locales requested');
+      }
 
-      var supported = negotiate(requested.concat(this.defaultLocale),
+      var reqPseudo = requested.filter(function(loc) {
+        return loc in PSEUDO_STRATEGIES;
+      });
+
+      var supported = negotiate(this.availableLocales.concat(reqPseudo),
                                 requested,
                                 this.defaultLocale);
       freeze.call(this, supported);
     };
 
 
     // Events
 
@@ -1275,21 +1341,22 @@
     function error(e) {
       this._emitter.emit('error', e);
       return e;
     }
   }
 
 
 
-  var DEBUG = true;
+  var DEBUG = false;
   var isPretranslated = false;
   var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur'];
   var nodeObserver = null;
   var pendingElements = null;
+  var manifest = {};
 
   var moConfig = {
     attributes: true,
     characterData: false,
     childList: true,
     subtree: true,
     attributeFilter: ['data-l10n-id', 'data-l10n-args']
   };
@@ -1338,30 +1405,31 @@
         Error: L10nError,
         Context: Context,
         Locale: Locale,
         Entity: Entity,
         getPluralRule: getPluralRule,
         rePlaceables: rePlaceables,
         getTranslatableChildren:  getTranslatableChildren,
         translateDocument: translateDocument,
-        loadINI: loadINI,
+        onManifestInjected: onManifestInjected,
+        onMetaInjected: onMetaInjected,
         fireLocalizedEvent: fireLocalizedEvent,
         PropertiesParser: PropertiesParser,
         compile: compile,
         walkContent: walkContent
       };
     }
   };
 
   navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));
 
   if (DEBUG) {
-    navigator.mozL10n.ctx.addEventListener('error', console);
-    navigator.mozL10n.ctx.addEventListener('warning', console);
+    navigator.mozL10n.ctx.addEventListener('error', console.error);
+    navigator.mozL10n.ctx.addEventListener('warning', console.warn);
   }
 
   function getDirection(lang) {
     return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr';
   }
 
   var readyStates = {
     'loading': 0,
@@ -1439,67 +1507,140 @@
     };
     translateDocument.call(l10n);
 
     // the visible DOM is now pretranslated
     isPretranslated = true;
   }
 
   function initResources() {
-    var nodes =
-      document.head.querySelectorAll('link[type="application/l10n"],' +
-                                     'script[type="application/l10n"]');
-    var iniLinks = [];
-
-    for (var i = 0; i < nodes.length; i++) {
-      var node = nodes[i];
-      var nodeName = node.nodeName.toLowerCase();
+    /* jshint boss:true */
+    var manifestFound = false;
 
-      switch (nodeName) {
-        case 'link':
-          var url = node.getAttribute('href');
-          var type = url.substr(url.lastIndexOf('.') + 1);
-          if (type === 'ini') {
-            iniLinks.push(url);
-          }
-          this.ctx.resLinks.push(url);
+    var nodes = document.head
+                        .querySelectorAll('link[rel="localization"],' +
+                                          'link[rel="manifest"],' +
+                                          'meta[name="locales"],' +
+                                          'meta[name="default_locale"],' +
+                                          'script[type="application/l10n"]');
+    for (var i = 0, node; node = nodes[i]; i++) {
+      var type = node.getAttribute('rel') || node.nodeName.toLowerCase();
+      switch (type) {
+        case 'manifest':
+          manifestFound = true;
+          onManifestInjected.call(this, node.getAttribute('href'), initLocale);
+          break;
+        case 'localization':
+          this.ctx.resLinks.push(node.getAttribute('href'));
+          break;
+        case 'meta':
+          onMetaInjected.call(this, node);
           break;
         case 'script':
-          var lang = node.getAttribute('lang');
-          var locale = this.ctx.getLocale(lang);
-          locale.addAST(JSON.parse(node.textContent));
+          onScriptInjected.call(this, node);
           break;
       }
     }
 
-    var iniLoads = iniLinks.length;
-    if (iniLoads === 0) {
-      initLocale.call(this);
+    // if after scanning the head any locales have been registered in the ctx
+    // it's safe to initLocale without waiting for manifest.webapp
+    if (this.ctx.availableLocales.length) {
+      return initLocale.call(this);
+    }
+
+    // if no locales were registered so far and no manifest.webapp link was
+    // found we still call initLocale with just the default language available
+    if (!manifestFound) {
+      this.ctx.registerLocales(this.ctx.defaultLocale);
+      return initLocale.call(this);
+    }
+  }
+
+  function onMetaInjected(node) {
+    if (this.ctx.availableLocales.length) {
+      return;
+    }
+
+    switch (node.getAttribute('name')) {
+      case 'locales':
+        manifest.locales = node.getAttribute('content').split(',').map(
+          Function.prototype.call, String.prototype.trim);
+        break;
+      case 'default_locale':
+        manifest.defaultLocale = node.getAttribute('content');
+        break;
+    }
+
+    if (Object.keys(manifest).length === 2) {
+      this.ctx.registerLocales(manifest.defaultLocale, manifest.locales);
+      manifest = {};
+    }
+  }
+
+  function onScriptInjected(node) {
+    var lang = node.getAttribute('lang');
+    var locale = this.ctx.getLocale(lang);
+    locale.addAST(JSON.parse(node.textContent));
+  }
+
+  function onManifestInjected(url, callback) {
+    if (this.ctx.availableLocales.length) {
       return;
     }
 
-    function onIniLoaded(err) {
+    io.loadJSON(url, function parseManifest(err, json) {
+      if (this.ctx.availableLocales.length) {
+        return;
+      }
+
       if (err) {
         this.ctx._emitter.emit('error', err);
-      }
-      if (--iniLoads === 0) {
-        initLocale.call(this);
+        this.ctx.registerLocales(this.ctx.defaultLocale);
+        if (callback) {
+          callback.call(this);
+        }
+        return;
       }
-    }
 
-    for (i = 0; i < iniLinks.length; i++) {
-      loadINI.call(this, iniLinks[i], onIniLoaded.bind(this));
-    }
+      // default_locale and locales might have been already provided by meta
+      // elements which take precedence;  check if we already have them
+      if (!('defaultLocale' in manifest)) {
+        if (json.default_locale) {
+          manifest.defaultLocale = json.default_locale;
+        } else {
+          manifest.defaultLocale = this.ctx.defaultLocale;
+          this.ctx._emitter.emit(
+            'warning', new L10nError('default_locale missing from manifest'));
+        }
+      }
+      if (!('locales' in manifest)) {
+        if (json.locales) {
+          manifest.locales = Object.keys(json.locales);
+        } else {
+          this.ctx._emitter.emit(
+            'warning', new L10nError('locales missing from manifest'));
+        }
+      }
+
+      this.ctx.registerLocales(manifest.defaultLocale, manifest.locales);
+      manifest = {};
+
+      if (callback) {
+        callback.call(this);
+      }
+    }.bind(this));
   }
 
   function initLocale() {
-    this.ctx.requestLocales(navigator.language);
+    this.ctx.requestLocales.apply(
+      this.ctx, navigator.languages || [navigator.language]);
     window.addEventListener('languagechange', function l10n_langchange() {
-      navigator.mozL10n.language.code = navigator.language;
-    });
+      this.ctx.requestLocales.apply(
+        this.ctx, navigator.languages || [navigator.language]);
+    }.bind(this));
   }
 
   function localizeMutations(mutations) {
     var mutation;
 
     for (var i = 0; i < mutations.length; i++) {
       mutation = mutations[i];
       if (mutation.type === 'childList') {
@@ -1560,93 +1701,16 @@
         'language': this.ctx.supportedLocales[0]
       }
     });
     window.dispatchEvent(event);
   }
 
   /* jshint -W104 */
 
-  function loadINI(url, callback) {
-    var ctx = this.ctx;
-    io.load(url, function(err, source) {
-      var pos = ctx.resLinks.indexOf(url);
-
-      if (err) {
-        // remove the ini link from resLinks
-        ctx.resLinks.splice(pos, 1);
-        return callback(err);
-      }
-
-      if (!source) {
-        ctx.resLinks.splice(pos, 1);
-        return callback(new Error('Empty file: ' + url));
-      }
-
-      var patterns = parseINI(source, url).resources.map(function(x) {
-        return x.replace('en-US', '{{locale}}');
-      });
-      ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns));
-      callback();
-    });
-  }
-
-  function relativePath(baseUrl, url) {
-    if (url[0] === '/') {
-      return url;
-    }
-
-    var dirs = baseUrl.split('/')
-      .slice(0, -1)
-      .concat(url.split('/'))
-      .filter(function(path) {
-        return path !== '.';
-      });
-
-    return dirs.join('/');
-  }
-
-  var iniPatterns = {
-    'section': /^\s*\[(.*)\]\s*$/,
-    'import': /^\s*@import\s+url\((.*)\)\s*$/i,
-    'entry': /[\r\n]+/
-  };
-
-  function parseINI(source, iniPath) {
-    var entries = source.split(iniPatterns.entry);
-    var locales = ['en-US'];
-    var genericSection = true;
-    var uris = [];
-    var match;
-
-    for (var i = 0; i < entries.length; i++) {
-      var line = entries[i];
-      // we only care about en-US resources
-      if (genericSection && iniPatterns['import'].test(line)) {
-        match = iniPatterns['import'].exec(line);
-        var uri = relativePath(iniPath, match[1]);
-        uris.push(uri);
-        continue;
-      }
-
-      // but we need the list of all locales in the ini, too
-      if (iniPatterns.section.test(line)) {
-        genericSection = false;
-        match = iniPatterns.section.exec(line);
-        locales.push(match[1]);
-      }
-    }
-    return {
-      locales: locales,
-      resources: uris
-    };
-  }
-
-  /* jshint -W104 */
-
   function translateDocument() {
     document.documentElement.lang = this.language.code;
     document.documentElement.dir = this.language.direction;
     translateFragment.call(this, document.documentElement);
   }
 
   function translateFragment(element) {
     if (element.hasAttribute('data-l10n-id')) {
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -15,17 +15,17 @@
   <div id="messages"></div>
   <div id="fixtures"></div>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.11.1.js"></script>
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
-  <script src="../../standalone/content/libs/l10n-gaia-4e35bf8f0569.js"></script>
+  <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
 
   <!-- test dependencies -->
   <script src="vendor/mocha-1.17.1.js"></script>
   <script src="vendor/chai-1.9.0.js"></script>
   <script src="vendor/sinon-1.9.0.js"></script>
   <script>
     /*global chai, mocha */
     chai.Assertion.includeStack = true;
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -15,17 +15,17 @@
  </div>
   <div id="messages"></div>
   <div id="fixtures"></div>
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.11.1.js"></script>
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
-  <script src="../../standalone/content/libs/l10n-gaia-4e35bf8f0569.js"></script>
+  <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
   <!-- test dependencies -->
   <script src="../shared/vendor/mocha-1.17.1.js"></script>
   <script src="../shared/vendor/chai-1.9.0.js"></script>
   <script src="../shared/vendor/sinon-1.9.0.js"></script>
   <script src="../shared/sdk_mock.js"></script>
   <script>
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');