Bug 1294220 - part2: Devtools shared l10n helper: remove dependency on Services.string;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Sat, 20 Aug 2016 21:46:29 +0200
changeset 404084 f149b09e55105b44f8e36fd2da20efdfbdbd53f0
parent 404083 b513f69a00335c63c46e085b0101ca4cf884fb08
child 404085 5e45396f68623cbef49aafd8406747a2037d5f8a
child 404092 c6fe88c94206f30a83eea94619fc882100c7f2e4
push id27110
push userjdescottes@mozilla.com
push dateMon, 22 Aug 2016 22:07:41 +0000
reviewersbgrins
bugs1294220
milestone51.0a1
Bug 1294220 - part2: Devtools shared l10n helper: remove dependency on Services.string;r=bgrins MozReview-Commit-ID: DyPKbYtlbEz
devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
devtools/client/projecteditor/lib/helpers/l10n.js
devtools/client/shared/l10n.js
devtools/client/shared/vendor/NODE_PROPERTIES_UPGRADING
devtools/client/shared/vendor/SPRINTF_JS_UPGRADING
devtools/client/shared/vendor/moz.build
devtools/client/shared/vendor/node-properties.js
devtools/client/shared/vendor/sprintf.js
--- a/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
+++ b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -1,40 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 /**
  * Tests if the preferences and localization objects work correctly.
  */
 
 function test() {
   initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     ok(aMonitor.panelWin.L10N,
       "Should have a localization object available on the panel window.");
     ok(aMonitor.panelWin.Prefs,
       "Should have a preferences object available on the panel window.");
 
     function testL10N() {
       let { L10N } = aMonitor.panelWin;
-
-      ok(L10N.stringBundle,
-        "The localization object should have a string bundle available.");
-
-      let bundleName = "chrome://devtools/locale/netmonitor.properties";
-      let stringBundle = Services.strings.createBundle(bundleName);
-
-      is(L10N.getStr("netmonitor.label"),
-        stringBundle.GetStringFromName("netmonitor.label"),
-        "The getStr() method didn't return the expected string.");
-
-      is(L10N.getFormatStr("networkMenu.totalMS", "foo"),
-        stringBundle.formatStringFromName("networkMenu.totalMS", ["foo"], 1),
-        "The getFormatStr() method didn't return the expected string.");
+      is(typeof L10N.getStr("netmonitor.label"), "string",
+        "The getStr() method didn't return a valid string.");
+      is(typeof L10N.getFormatStr("networkMenu.totalMS", "foo"), "string",
+        "The getFormatStr() method didn't return a valid string.");
     }
 
     function testPrefs() {
       let { Prefs } = aMonitor.panelWin;
 
       is(Prefs.networkDetailsWidth,
         Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
         "Getting a pref should work correctly.");
--- a/devtools/client/projecteditor/lib/helpers/l10n.js
+++ b/devtools/client/projecteditor/lib/helpers/l10n.js
@@ -1,25 +1,26 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
 /**
  * This file contains helper functions for internationalizing projecteditor strings
  */
 
-const { Cu, Cc, Ci } = require("chrome");
 const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const ITCHPAD_STRINGS_URI = "chrome://devtools/locale/projecteditor.properties";
-const L10N = new LocalizationHelper(ITCHPAD_STRINGS_URI).stringBundle;
+const L10N = new LocalizationHelper(ITCHPAD_STRINGS_URI);
 
 function getLocalizedString(name) {
   try {
-    return L10N.GetStringFromName(name);
+    return L10N.getStr(name);
   } catch (ex) {
     console.log("Error reading '" + name + "'");
     throw new Error("l10n error with " + name);
   }
 }
 
 exports.getLocalizedString = getLocalizedString;
--- a/devtools/client/shared/l10n.js
+++ b/devtools/client/shared/l10n.js
@@ -1,64 +1,69 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const Services = require("Services");
+const parsePropertiesFile = require("devtools/client/shared/vendor/node-properties");
+const { sprintf } = require("devtools/client/shared/vendor/sprintf");
 
 /**
  * Localization convenience methods.
  *
  * @param string stringBundleName
  *        The desired string bundle's name.
  */
 function LocalizationHelper(stringBundleName) {
-  loader.lazyGetter(this, "stringBundle", () =>
-    Services.strings.createBundle(stringBundleName));
+  loader.lazyGetter(this, "properties", () => {
+    return parsePropertiesFile(require(`raw!${stringBundleName}`));
+  });
 }
 
 LocalizationHelper.prototype = {
   /**
    * L10N shortcut function.
    *
    * @param string name
    * @return string
    */
   getStr: function (name) {
-    return this.stringBundle.GetStringFromName(name);
+    if (name in this.properties) {
+      return this.properties[name];
+    }
+
+    throw new Error("No localization found for [" + name + "]");
   },
 
   /**
    * L10N shortcut function.
    *
    * @param string name
    * @param array args
    * @return string
    */
   getFormatStr: function (name, ...args) {
-    return this.stringBundle.formatStringFromName(name, args, args.length);
+    return sprintf(this.getStr(name), ...args);
   },
 
   /**
    * L10N shortcut function for numeric arguments that need to be formatted.
    * All numeric arguments will be fixed to 2 decimals and given a localized
    * decimal separator. Other arguments will be left alone.
    *
    * @param string name
    * @param array args
    * @return string
    */
   getFormatStrWithNumbers: function (name, ...args) {
     let newArgs = args.map(x => {
       return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
     });
-    return this.stringBundle.formatStringFromName(name,
-                                                  newArgs,
-                                                  newArgs.length);
+
+    return this.getFormatStr(name, ...newArgs);
   },
 
   /**
    * Converts a number to a locale-aware string format and keeps a certain
    * number of decimals.
    *
    * @param number number
    *        The number to convert.
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/NODE_PROPERTIES_UPGRADING
@@ -0,0 +1,12 @@
+NODE PROPERTIES UPGRADING
+
+Original library at https://github.com/gagle/node-properties
+The original library is intended for node and not for the browser. Most files are not
+needed here.
+
+To update
+- copy https://github.com/gagle/node-properties/blob/master/lib/parse.js
+- update the initial "module.exports" to "var parse" in parse.js
+- copy https://github.com/gagle/node-properties/blob/master/lib/read.js
+- remove the require statements at the beginning
+- merge the two files, parse.js first, read.js second
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/SPRINTF_JS_UPGRADING
@@ -0,0 +1,12 @@
+SPRINTF JS UPGRADING
+
+Original library at https://github.com/alexei/sprintf.js
+By default the library only supports string placeholders using %s (lowercase) while we use
+%S (uppercase). The library has to be manually patched in order to support it.
+
+- grab the unminified version at https://github.com/alexei/sprintf.js/blob/master/src/sprintf.js
+- update the re.placeholder regexp to allow "S" as well as "s"
+- update the switch statement in the format() method to make case "S" equivalent to case "s"
+
+The original changeset adding support for "%S" can be found on this fork:
+- https://github.com/juliandescottes/sprintf.js/commit/a60ea5d7c4cd9a006002ba9f0afc1e2689107eec
\ No newline at end of file
--- a/devtools/client/shared/vendor/moz.build
+++ b/devtools/client/shared/vendor/moz.build
@@ -1,25 +1,28 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 modules = []
 modules += [
     'immutable.js',
-    'jsol.js'
+    'jsol.js',
+    'node-properties.js',
 ]
 
 # react-dev is used if either debug mode is enabled,
 # so include it for both
 if CONFIG['DEBUG_JS_MODULES'] or CONFIG['MOZ_DEBUG']:
     modules += ['react-dev.js']
+
 modules += [
     'react-dom.js',
     'react-proxy.js',
     'react-redux.js',
     'react.js',
     'redux.js',
-    'seamless-immutable.js'
+    'seamless-immutable.js',
+    'sprintf.js',
 ]
 
 DevToolsModules(*modules)
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/node-properties.js
@@ -0,0 +1,776 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014 Gabriel Llamas
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+"use strict";
+
+var hex = function (c){
+  switch (c){
+    case "0": return 0;
+    case "1": return 1;
+    case "2": return 2;
+    case "3": return 3;
+    case "4": return 4;
+    case "5": return 5;
+    case "6": return 6;
+    case "7": return 7;
+    case "8": return 8;
+    case "9": return 9;
+    case "a": case "A": return 10;
+    case "b": case "B": return 11;
+    case "c": case "C": return 12;
+    case "d": case "D": return 13;
+    case "e": case "E": return 14;
+    case "f": case "F": return 15;
+  }
+};
+
+var parse = function (data, options, handlers, control){
+  var c;
+  var code;
+  var escape;
+  var skipSpace = true;
+  var isCommentLine;
+  var isSectionLine;
+  var newLine = true;
+  var multiLine;
+  var isKey = true;
+  var key = "";
+  var value = "";
+  var section;
+  var unicode;
+  var unicodeRemaining;
+  var escapingUnicode;
+  var keySpace;
+  var sep;
+  var ignoreLine;
+
+  var line = function (){
+    if (key || value || sep){
+      handlers.line (key, value);
+      key = "";
+      value = "";
+      sep = false;
+    }
+  };
+
+  var escapeString = function (key, c, code){
+    if (escapingUnicode && unicodeRemaining){
+      unicode = (unicode << 4) + hex (c);
+      if (--unicodeRemaining) return key;
+      escape = false;
+      escapingUnicode = false;
+      return key + String.fromCharCode (unicode);
+    }
+
+    //code 117: u
+    if (code === 117){
+      unicode = 0;
+      escapingUnicode = true;
+      unicodeRemaining = 4;
+      return key;
+    }
+
+    escape = false;
+
+    //code 116: t
+    //code 114: r
+    //code 110: n
+    //code 102: f
+    if (code === 116) return key + "\t";
+    else if (code === 114) return key + "\r";
+    else if (code === 110) return key + "\n";
+    else if (code === 102) return key + "\f";
+
+    return key + c;
+  };
+
+  var isComment;
+  var isSeparator;
+
+  if (options._strict){
+    isComment = function (c, code, options){
+      return options._comments[c];
+    };
+
+    isSeparator = function (c, code, options){
+      return options._separators[c];
+    };
+  }else{
+    isComment = function (c, code, options){
+      //code 35: #
+      //code 33: !
+      return code === 35 || code === 33 || options._comments[c];
+    };
+
+    isSeparator = function (c, code, options){
+      //code 61: =
+      //code 58: :
+      return code === 61 || code === 58 || options._separators[c];
+    };
+  }
+
+  for (var i=~~control.resume; i<data.length; i++){
+    if (control.abort) return;
+    if (control.pause){
+      //The next index is always the start of a new line, it's a like a fresh
+      //start, there's no need to save the current state
+      control.resume = i;
+      return;
+    }
+
+    c = data[i];
+    code = data.charCodeAt (i);
+
+    //code 13: \r
+    if (code === 13) continue;
+
+    if (isCommentLine){
+      //code 10: \n
+      if (code === 10){
+        isCommentLine = false;
+        newLine = true;
+        skipSpace = true;
+      }
+      continue;
+    }
+
+    //code 93: ]
+    if (isSectionLine && code === 93){
+      handlers.section (section);
+      //Ignore chars after the section in the same line
+      ignoreLine = true;
+      continue;
+    }
+
+    if (skipSpace){
+      //code 32: " " (space)
+      //code 9: \t
+      //code 12: \f
+      if (code === 32 || code === 9 || code === 12){
+        continue;
+      }
+      //code 10: \n
+      if (!multiLine && code === 10){
+        //Empty line or key w/ separator and w/o value
+        isKey = true;
+        keySpace = false;
+        newLine = true;
+        line ();
+        continue;
+      }
+      skipSpace = false;
+      multiLine = false;
+    }
+
+    if (newLine){
+      newLine = false;
+      if (isComment (c, code, options)){
+        isCommentLine = true;
+        continue;
+      }
+      //code 91: [
+      if (options.sections && code === 91){
+        section = "";
+        isSectionLine = true;
+        control.skipSection = false;
+        continue;
+      }
+    }
+
+    //code 10: \n
+    if (code !== 10){
+      if (control.skipSection || ignoreLine) continue;
+
+      if (!isSectionLine){
+        if (!escape && isKey && isSeparator (c, code, options)){
+          //sep is needed to detect empty key and empty value with a
+          //non-whitespace separator
+          sep = true;
+          isKey = false;
+          keySpace = false;
+          //Skip whitespace between separator and value
+          skipSpace = true;
+          continue;
+        }
+      }
+
+      //code 92: "\" (backslash)
+      if (code === 92){
+        if (escape){
+          if (escapingUnicode) continue;
+
+          if (keySpace){
+            //Line with whitespace separator
+            keySpace = false;
+            isKey = false;
+          }
+
+          if (isSectionLine) section += "\\";
+          else if (isKey) key += "\\";
+          else value += "\\";
+        }
+        escape = !escape;
+      }else{
+        if (keySpace){
+          //Line with whitespace separator
+          keySpace = false;
+          isKey = false;
+        }
+
+        if (isSectionLine){
+          if (escape) section = escapeString (section, c, code);
+          else section += c;
+        }else if (isKey){
+          if (escape){
+            key = escapeString (key, c, code);
+          }else{
+            //code 32: " " (space)
+            //code 9: \t
+            //code 12: \f
+            if (code === 32 || code === 9 || code === 12){
+              keySpace = true;
+              //Skip whitespace between key and separator
+              skipSpace = true;
+              continue;
+            }
+            key += c;
+          }
+        }else{
+          if (escape) value = escapeString (value, c, code);
+          else value += c;
+        }
+      }
+    }else{
+      if (escape){
+        if (!escapingUnicode){
+          escape = false;
+        }
+        skipSpace = true;
+        multiLine = true;
+      }else{
+        if (isSectionLine){
+          isSectionLine = false;
+          if (!ignoreLine){
+            //The section doesn't end with ], it's a key
+            control.error = new Error ("The section line \"" + section +
+                "\" must end with \"]\"");
+            return;
+          }
+          ignoreLine = false;
+        }
+        newLine = true;
+        skipSpace = true;
+        isKey = true;
+
+        line ();
+      }
+    }
+  }
+
+  control.parsed = true;
+
+  if (isSectionLine && !ignoreLine){
+    //The section doesn't end with ], it's a key
+    control.error = new Error ("The section line \"" + section + "\" must end" +
+        "with \"]\"");
+    return;
+  }
+  line ();
+};
+
+var INCLUDE_KEY = "include";
+var INDEX_FILE = "index.properties";
+
+var cast = function (value){
+  if (value === null || value === "null") return null;
+  if (value === "undefined") return undefined;
+  if (value === "true") return true;
+  if (value === "false") return false;
+  var v = Number (value);
+  return isNaN (v) ? value : v;
+};
+
+var expand = function  (o, str, options, cb){
+  if (!options.variables || !str) return cb (null, str);
+
+  var stack = [];
+  var c;
+  var cp;
+  var key = "";
+  var section = null;
+  var v;
+  var holder;
+  var t;
+  var n;
+
+  for (var i=0; i<str.length; i++){
+    c = str[i];
+
+    if (cp === "$" && c === "{"){
+      key = key.substring (0, key.length - 1);
+      stack.push ({
+        key: key,
+        section: section
+      });
+      key = "";
+      section = null;
+      continue;
+    }else if (stack.length){
+      if (options.sections && c === "|"){
+        section = key;
+        key = "";
+        continue;
+      }else if (c === "}"){
+        holder = section !== null ? searchValue (o, section, true) : o;
+        if (!holder){
+          return cb (new Error ("The section \"" + section + "\" does not " +
+              "exist"));
+        }
+
+        v = options.namespaces ? searchValue (holder, key) : holder[key];
+        if (v === undefined){
+          //Read the external vars
+          v = options.namespaces
+              ? searchValue (options._vars, key)
+              : options._vars[key]
+
+          if (v === undefined){
+            return cb (new Error ("The property \"" + key + "\" does not " +
+                "exist"));
+          }
+        }
+
+        t = stack.pop ();
+        section = t.section;
+        key = t.key + (v === null ? "" : v);
+        continue;
+      }
+    }
+
+    cp = c;
+    key += c;
+  }
+
+  if (stack.length !== 0){
+    return cb (new Error ("Malformed variable: " + str));
+  }
+
+  cb (null, key);
+};
+
+var searchValue = function (o, chain, section){
+  var n = chain.split (".");
+  var str;
+
+  for (var i=0; i<n.length-1; i++){
+    str = n[i];
+    if (o[str] === undefined) return;
+    o = o[str];
+  }
+
+  var v = o[n[n.length - 1]];
+  if (section){
+    if (typeof v !== "object") return;
+    return v;
+  }else{
+    if (typeof v === "object") return;
+    return v;
+  }
+};
+
+var namespaceKey = function (o, key, value){
+  var n = key.split (".");
+  var str;
+
+  for (var i=0; i<n.length-1; i++){
+    str = n[i];
+    if (o[str] === undefined){
+      o[str] = {};
+    }else if (typeof o[str] !== "object"){
+      throw new Error ("Invalid namespace chain in the property name '" +
+          key + "' ('" + str + "' has already a value)");
+    }
+    o = o[str];
+  }
+
+  o[n[n.length - 1]] = value;
+};
+
+var namespaceSection = function (o, section){
+  var n = section.split (".");
+  var str;
+
+  for (var i=0; i<n.length; i++){
+    str = n[i];
+    if (o[str] === undefined){
+      o[str] = {};
+    }else if (typeof o[str] !== "object"){
+      throw new Error ("Invalid namespace chain in the section name '" +
+          section + "' ('" + str + "' has already a value)");
+    }
+    o = o[str];
+  }
+
+  return o;
+};
+
+var merge = function (o1, o2){
+  for (var p in o2){
+    try{
+      if (o1[p].constructor === Object){
+        o1[p] = merge (o1[p], o2[p]);
+      }else{
+        o1[p] = o2[p];
+      }
+    }catch (e){
+      o1[p] = o2[p];
+    }
+  }
+  return o1;
+}
+
+var build = function (data, options, dirname, cb){
+  var o = {};
+
+  if (options.namespaces){
+    var n = {};
+  }
+
+  var control = {
+    abort: false,
+    skipSection: false
+  };
+
+  if (options.include){
+    var remainingIncluded = 0;
+
+    var include = function (value){
+      if (currentSection !== null){
+        return abort (new Error ("Cannot include files from inside a " +
+            "section: " + currentSection));
+      }
+
+      var p = path.resolve (dirname, value);
+      if (options._included[p]) return;
+
+      options._included[p] = true;
+      remainingIncluded++;
+      control.pause = true;
+
+      read (p, options, function (error, included){
+        if (error) return abort (error);
+
+        remainingIncluded--;
+        merge (options.namespaces ? n : o, included);
+        control.pause = false;
+
+        if (!control.parsed){
+          parse (data, options, handlers, control);
+          if (control.error) return abort (control.error);
+        }
+
+        if (!remainingIncluded) cb (null, options.namespaces ? n : o);
+      });
+    };
+  }
+
+  if (!data){
+    if (cb) return cb (null, o);
+    return o;
+  }
+
+  var currentSection = null;
+  var currentSectionStr = null;
+
+  var abort = function (error){
+    control.abort = true;
+    if (cb) return cb (error);
+    throw error;
+  };
+
+  var handlers = {};
+  var reviver = {
+    assert: function (){
+      return this.isProperty ? reviverLine.value : true;
+    }
+  };
+  var reviverLine = {};
+
+  //Line handler
+  //For speed reasons, if "namespaces" is enabled, the old object is still
+  //populated, e.g.: ${a.b} reads the "a.b" property from { "a.b": 1 }, instead
+  //of having a unique object { a: { b: 1 } } which is slower to search for
+  //the "a.b" value
+  //If "a.b" is not found, then the external vars are read. If "namespaces" is
+  //enabled, the var "a.b" is split and it searches the a.b value. If it is not
+  //enabled, then the var "a.b" searches the "a.b" value
+
+  var line;
+  var error;
+
+  if (options.reviver){
+    if (options.sections){
+      line = function (key, value){
+        if (options.include && key === INCLUDE_KEY) return include (value);
+
+        reviverLine.value = value;
+        reviver.isProperty = true;
+        reviver.isSection = false;
+
+        value = options.reviver.call (reviver, key, value, currentSectionStr);
+        if (value !== undefined){
+          if (options.namespaces){
+            try{
+              namespaceKey (currentSection === null ? n : currentSection,
+                  key, value);
+            }catch (error){
+              abort (error);
+            }
+          }else{
+            if (currentSection === null) o[key] = value;
+            else currentSection[key] = value;
+          }
+        }
+      };
+    }else{
+      line = function (key, value){
+        if (options.include && key === INCLUDE_KEY) return include (value);
+
+        reviverLine.value = value;
+        reviver.isProperty = true;
+        reviver.isSection = false;
+
+        value = options.reviver.call (reviver, key, value);
+        if (value !== undefined){
+          if (options.namespaces){
+            try{
+              namespaceKey (n, key, value);
+            }catch (error){
+              abort (error);
+            }
+          }else{
+            o[key] = value;
+          }
+        }
+      };
+    }
+  }else{
+    if (options.sections){
+      line = function (key, value){
+        if (options.include && key === INCLUDE_KEY) return include (value);
+
+        if (options.namespaces){
+          try{
+            namespaceKey (currentSection === null ? n : currentSection, key,
+                value);
+          }catch (error){
+            abort (error);
+          }
+        }else{
+          if (currentSection === null) o[key] = value;
+          else currentSection[key] = value;
+        }
+      };
+    }else{
+      line = function (key, value){
+        if (options.include && key === INCLUDE_KEY) return include (value);
+
+        if (options.namespaces){
+          try{
+            namespaceKey (n, key, value);
+          }catch (error){
+            abort (error);
+          }
+        }else{
+          o[key] = value;
+        }
+      };
+    }
+  }
+
+  //Section handler
+  var section;
+  if (options.sections){
+    if (options.reviver){
+      section = function (section){
+        currentSectionStr = section;
+        reviverLine.section = section;
+        reviver.isProperty = false;
+        reviver.isSection = true;
+
+        var add = options.reviver.call (reviver, null, null, section);
+        if (add){
+          if (options.namespaces){
+            try{
+              currentSection = namespaceSection (n, section);
+            }catch (error){
+              abort (error);
+            }
+          }else{
+            currentSection = o[section] = {};
+          }
+        }else{
+          control.skipSection = true;
+        }
+      };
+    }else{
+      section = function (section){
+        currentSectionStr = section;
+        if (options.namespaces){
+          try{
+            currentSection = namespaceSection (n, section);
+          }catch (error){
+            abort (error);
+          }
+        }else{
+          currentSection = o[section] = {};
+        }
+      };
+    }
+  }
+
+  //Variables
+  if (options.variables){
+    handlers.line = function (key, value){
+      expand (options.namespaces ? n : o, key, options, function (error, key){
+        if (error) return abort (error);
+
+        expand (options.namespaces ? n : o, value, options,
+            function (error, value){
+          if (error) return abort (error);
+
+          line (key, cast (value || null));
+        });
+      });
+    };
+
+    if (options.sections){
+      handlers.section = function (s){
+        expand (options.namespaces ? n : o, s, options, function (error, s){
+          if (error) return abort (error);
+
+          section (s);
+        });
+      };
+    }
+  }else{
+    handlers.line = function (key, value){
+      line (key, cast (value || null));
+    };
+
+    if (options.sections){
+      handlers.section = section;
+    }
+  }
+
+  parse (data, options, handlers, control);
+  if (control.error) return abort (control.error);
+
+  if (control.abort || control.pause) return;
+
+  if (cb) return cb (null, options.namespaces ? n : o);
+  return options.namespaces ? n : o;
+};
+
+var read = function (f, options, cb){
+  fs.stat (f, function (error, stats){
+    if (error) return cb (error);
+
+    var dirname;
+
+    if (stats.isDirectory ()){
+      dirname = f;
+      f = path.join (f, INDEX_FILE);
+    }else{
+      dirname = path.dirname (f);
+    }
+
+    fs.readFile (f, { encoding: "utf8" }, function (error, data){
+      if (error) return cb (error);
+      build (data, options, dirname, cb);
+    });
+  });
+};
+
+module.exports = function (data, options, cb){
+  if (typeof options === "function"){
+    cb = options;
+    options = {};
+  }
+
+  options = options || {};
+  var code;
+
+  if (options.include){
+    if (!cb) throw new Error ("A callback must be passed if the 'include' " +
+        "option is enabled");
+    options._included = {};
+  }
+
+  options = options || {};
+  options._strict = options.strict && (options.comments || options.separators);
+  options._vars = options.vars || {};
+
+  var comments = options.comments || [];
+  if (!Array.isArray (comments)) comments = [comments];
+  var c = {};
+  comments.forEach (function (comment){
+    code = comment.charCodeAt (0);
+    if (comment.length > 1 || code < 33 || code > 126){
+      throw new Error ("The comment token must be a single printable ASCII " +
+          "character");
+    }
+    c[comment] = true;
+  });
+  options._comments = c;
+
+  var separators = options.separators || [];
+  if (!Array.isArray (separators)) separators = [separators];
+  var s = {};
+  separators.forEach (function (separator){
+    code = separator.charCodeAt (0);
+    if (separator.length > 1 || code < 33 || code > 126){
+      throw new Error ("The separator token must be a single printable ASCII " +
+          "character");
+    }
+    s[separator] = true;
+  });
+  options._separators = s;
+
+  if (options.path){
+    if (!cb) throw new Error ("A callback must be passed if the 'path' " +
+        "option is enabled");
+    if (options.include){
+      read (data, options, cb);
+    }else{
+      fs.readFile (data, { encoding: "utf8" }, function (error, data){
+        if (error) return cb (error);
+        build (data, options, ".", cb);
+      });
+    }
+  }else{
+    return build (data, options, ".", cb);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/vendor/sprintf.js
@@ -0,0 +1,274 @@
+/**
+ * Copyright (c) 2007-2016, Alexandru Marasteanu <hello [at) alexei (dot] ro>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * * Neither the name of this software nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+/* globals window, exports, define */
+
+(function(window) {
+    'use strict'
+
+    var re = {
+        not_string: /[^s]/,
+        not_bool: /[^t]/,
+        not_type: /[^T]/,
+        not_primitive: /[^v]/,
+        number: /[diefg]/,
+        numeric_arg: /bcdiefguxX/,
+        json: /[j]/,
+        not_json: /[^j]/,
+        text: /^[^\x25]+/,
+        modulo: /^\x25{2}/,
+        placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosStTuvxX])/,
+        key: /^([a-z_][a-z_\d]*)/i,
+        key_access: /^\.([a-z_][a-z_\d]*)/i,
+        index_access: /^\[(\d+)\]/,
+        sign: /^[\+\-]/
+    }
+
+    function sprintf() {
+        var key = arguments[0], cache = sprintf.cache
+        if (!(cache[key] && cache.hasOwnProperty(key))) {
+            cache[key] = sprintf.parse(key)
+        }
+        return sprintf.format.call(null, cache[key], arguments)
+    }
+
+    sprintf.format = function(parse_tree, argv) {
+        var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = ''
+        for (i = 0; i < tree_length; i++) {
+            node_type = get_type(parse_tree[i])
+            if (node_type === 'string') {
+                output[output.length] = parse_tree[i]
+            }
+            else if (node_type === 'array') {
+                match = parse_tree[i] // convenience purposes only
+                if (match[2]) { // keyword argument
+                    arg = argv[cursor]
+                    for (k = 0; k < match[2].length; k++) {
+                        if (!arg.hasOwnProperty(match[2][k])) {
+                            throw new Error(sprintf('[sprintf] property "%s" does not exist', match[2][k]))
+                        }
+                        arg = arg[match[2][k]]
+                    }
+                }
+                else if (match[1]) { // positional argument (explicit)
+                    arg = argv[match[1]]
+                }
+                else { // positional argument (implicit)
+                    arg = argv[cursor++]
+                }
+
+                if (re.not_type.test(match[8]) && re.not_primitive.test(match[8]) && get_type(arg) == 'function') {
+                    arg = arg()
+                }
+
+                if (re.numeric_arg.test(match[8]) && (get_type(arg) != 'number' && isNaN(arg))) {
+                    throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg)))
+                }
+
+                if (re.number.test(match[8])) {
+                    is_positive = arg >= 0
+                }
+
+                switch (match[8]) {
+                    case 'b':
+                        arg = parseInt(arg, 10).toString(2)
+                    break
+                    case 'c':
+                        arg = String.fromCharCode(parseInt(arg, 10))
+                    break
+                    case 'd':
+                    case 'i':
+                        arg = parseInt(arg, 10)
+                    break
+                    case 'j':
+                        arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0)
+                    break
+                    case 'e':
+                        arg = match[7] ? parseFloat(arg).toExponential(match[7]) : parseFloat(arg).toExponential()
+                    break
+                    case 'f':
+                        arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg)
+                    break
+                    case 'g':
+                        arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg)
+                    break
+                    case 'o':
+                        arg = arg.toString(8)
+                    break
+                    case 's':
+                    case 'S':
+                        arg = String(arg)
+                        arg = (match[7] ? arg.substring(0, match[7]) : arg)
+                    break
+                    case 't':
+                        arg = String(!!arg)
+                        arg = (match[7] ? arg.substring(0, match[7]) : arg)
+                    break
+                    case 'T':
+                        arg = get_type(arg)
+                        arg = (match[7] ? arg.substring(0, match[7]) : arg)
+                    break
+                    case 'u':
+                        arg = parseInt(arg, 10) >>> 0
+                    break
+                    case 'v':
+                        arg = arg.valueOf()
+                        arg = (match[7] ? arg.substring(0, match[7]) : arg)
+                    break
+                    case 'x':
+                        arg = parseInt(arg, 10).toString(16)
+                    break
+                    case 'X':
+                        arg = parseInt(arg, 10).toString(16).toUpperCase()
+                    break
+                }
+                if (re.json.test(match[8])) {
+                    output[output.length] = arg
+                }
+                else {
+                    if (re.number.test(match[8]) && (!is_positive || match[3])) {
+                        sign = is_positive ? '+' : '-'
+                        arg = arg.toString().replace(re.sign, '')
+                    }
+                    else {
+                        sign = ''
+                    }
+                    pad_character = match[4] ? match[4] === '0' ? '0' : match[4].charAt(1) : ' '
+                    pad_length = match[6] - (sign + arg).length
+                    pad = match[6] ? (pad_length > 0 ? str_repeat(pad_character, pad_length) : '') : ''
+                    output[output.length] = match[5] ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
+                }
+            }
+        }
+        return output.join('')
+    }
+
+    sprintf.cache = {}
+
+    sprintf.parse = function(fmt) {
+        var _fmt = fmt, match = [], parse_tree = [], arg_names = 0
+        while (_fmt) {
+            if ((match = re.text.exec(_fmt)) !== null) {
+                parse_tree[parse_tree.length] = match[0]
+            }
+            else if ((match = re.modulo.exec(_fmt)) !== null) {
+                parse_tree[parse_tree.length] = '%'
+            }
+            else if ((match = re.placeholder.exec(_fmt)) !== null) {
+                if (match[2]) {
+                    arg_names |= 1
+                    var field_list = [], replacement_field = match[2], field_match = []
+                    if ((field_match = re.key.exec(replacement_field)) !== null) {
+                        field_list[field_list.length] = field_match[1]
+                        while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
+                            if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+                                field_list[field_list.length] = field_match[1]
+                            }
+                            else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+                                field_list[field_list.length] = field_match[1]
+                            }
+                            else {
+                                throw new SyntaxError("[sprintf] failed to parse named argument key")
+                            }
+                        }
+                    }
+                    else {
+                        throw new SyntaxError("[sprintf] failed to parse named argument key")
+                    }
+                    match[2] = field_list
+                }
+                else {
+                    arg_names |= 2
+                }
+                if (arg_names === 3) {
+                    throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported")
+                }
+                parse_tree[parse_tree.length] = match
+            }
+            else {
+                throw new SyntaxError("[sprintf] unexpected placeholder")
+            }
+            _fmt = _fmt.substring(match[0].length)
+        }
+        return parse_tree
+    }
+
+    var vsprintf = function(fmt, argv, _argv) {
+        _argv = (argv || []).slice(0)
+        _argv.splice(0, 0, fmt)
+        return sprintf.apply(null, _argv)
+    }
+
+    /**
+     * helpers
+     */
+    function get_type(variable) {
+        if (typeof variable === 'number') {
+            return 'number'
+        }
+        else if (typeof variable === 'string') {
+            return 'string'
+        }
+        else {
+            return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase()
+        }
+    }
+
+    var preformattedPadding = {
+        '0': ['', '0', '00', '000', '0000', '00000', '000000', '0000000'],
+        ' ': ['', ' ', '  ', '   ', '    ', '     ', '      ', '       '],
+        '_': ['', '_', '__', '___', '____', '_____', '______', '_______'],
+    }
+    function str_repeat(input, multiplier) {
+        if (multiplier >= 0 && multiplier <= 7 && preformattedPadding[input]) {
+            return preformattedPadding[input][multiplier]
+        }
+        return Array(multiplier + 1).join(input)
+    }
+
+    /**
+     * export to either browser or node.js
+     */
+    if (typeof exports !== 'undefined') {
+        exports.sprintf = sprintf
+        exports.vsprintf = vsprintf
+    }
+    else {
+        window.sprintf = sprintf
+        window.vsprintf = vsprintf
+
+        if (typeof define === 'function' && define.amd) {
+            define(function() {
+                return {
+                    sprintf: sprintf,
+                    vsprintf: vsprintf
+                }
+            })
+        }
+    }
+})(typeof window === 'undefined' ? this : window);