LI = (typeof(LI) !== "undefined" && LI) ? LI : {};
LI.i18n = LI.i18n || {};

(function(root, factory) {
  //call factory to create t8 object
  var lib = factory();

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;

  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }

  //root (window or global), factory
}(this, function() {
  "use strict";

  var __localeData = {};

//Utilities and shared JS
/**
 * A set of general purpose utility functions
 */
var Utils = (function(){

  "use strict";

  var Utils = {};

  /*******************************************************************
   * Underscore dependency removed by integrating necessary components of Underscore
   * directly into the t8.Utils framework
   *******************************************************************/
  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // Save bytes in the minified (but not gzipped) version:
  var ArrayProto      = Array.prototype, 
      ObjProto        = Object.prototype, 
      FuncProto       = Function.prototype;

  // Create quick reference variables for speed access to core prototypes.
  var
    push              = ArrayProto.push,
    slice             = ArrayProto.slice,
    concat            = ArrayProto.concat,
    toString          = ObjProto.toString,
    hasOwnProperty    = ObjProto.hasOwnProperty;

  // All **ECMAScript 5** native function implementations that we _hope_ to use
  // are declared here.
  var
    nativeForEach      = ArrayProto.forEach,
    nativeMap          = ArrayProto.map,
    nativeReduce       = ArrayProto.reduce,
    nativeReduceRight  = ArrayProto.reduceRight,
    nativeFilter       = ArrayProto.filter,
    nativeEvery        = ArrayProto.every,
    nativeSome         = ArrayProto.some,
    nativeIndexOf      = ArrayProto.indexOf,
    nativeLastIndexOf  = ArrayProto.lastIndexOf,
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind;

  // An internal function to generate lookup iterators.
  var lookupIterator = function(value) {
    return Utils.isFunction(value) ? value : function(obj){ return obj[value]; };
  };

  /**
   * Copy all of the properties in the source objects over to the destination object, and return the destination object. 
   * It's in-order, so the last source will override properties of the same name in previous arguments.
   * @param obj
   */
  Utils.extend = function(obj) {
    Utils.each(Array.prototype.slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  /**
   * Iterates over a list of elements, yielding each in turn to an iterator function.
   * @param obj
   * @param iterator
   * @param context
   */
  var each = Utils.each = function(obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, length = obj.length; i < length; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      var keys = Utils.keys(obj);
      for (var i = 0, length = keys.length; i < length; i++) {
        if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
      }
    }
  };

  /**
   * Returns true if any of the values in the list pass the iterator truth test. 
   * Short-circuits and stops traversing the list if a true element is found.
   * @param obj
   * @param iterator
   * @param context
   */
  var any = Utils.any = Utils.some = function(obj, iterator, context) {
    iterator || (iterator = Utils.identity);
    var result = false;
    if (obj == null) return result;
    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
    Utils.each(obj, function(value, index, list) {
      if (result || (result = iterator.call(context, value, index, list))) return breaker;
    });
    return !!result;
  };

  /**
   * Looks through each value in the list, returning the first one that passes a truth test (iterator), 
   * or undefined if no value passes the test. The function returns as soon as it finds an acceptable element, 
   * and doesn't traverse the entire list.
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.find = function(obj, iterator, context) {
    var result;
    any(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) {
        result = value;
        return true;
      }
    });
    return result;
  };

  /**
   * Returns a copy of the object where the keys have become the values and the values the keys. 
   * For this to work, all of your object's values should be unique and string serializable.
   * @param obj
   */
  Utils.invert = function(obj) {
    var result = {};
    var keys = Utils.keys(obj);
    for (var i = 0, length = keys.length; i < length; i++) {
      result[obj[keys[i]]] = keys[i];
    }
    return result;
  };

  /**
   * Returns the same value that is used as the argument. In math: f(x) = x
   * @param value
   */
  Utils.identity = function(value) {
    return value;
  };

  /**
   * Shortcut function for checking if an object has a given property directly on itself (in other words, not on a prototype).
   * @param obj
   * @param key
   */
  Utils.has = function(obj, key) {
    return hasOwnProperty.call(obj, key);
  };

  /**
   * Returns true if the value is present in the list. Uses indexOf internally, if list is an Array.
   * @param obj
   * @param key
   */
  Utils.contains = function(obj, target) {
    if (obj == null) return false;
    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
    return Utils.some(obj, function(value) {
      return value === target;
    });
  };

  /**
   * Retrieve all the names of the object's properties.
   * @param obj
   */
  Utils.keys = function(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj) if (Utils.has(obj, key)) keys.push(key);
    return keys;
  };

  /**
   * Produces a new array of values by mapping each value in list through a transformation function (iterator). 
   * If the native map method exists, it will be used instead. If list is a JavaScript object, iterator's arguments will be (value, key, list).
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.map = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
    Utils.each(obj, function(value, index, list) {
      results.push(iterator.call(context, value, index, list));
    });
    return results;
  };

  /**
   * Returns the index at which value can be found in the array, or -1 if value is not present in the array.
   * @param array
   * @param item
   * @param isSorted
   */
  Utils.indexOf = function(array, item, isSorted) {
    if (array == null) return -1;
    var i = 0, length = array.length;
    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
    for (; i < length; i++) if (array[i] === item) return i;
    return -1;
  };

  /**
   * Returns true if value is undefined.
   * @param obj
   * @returns {boolean}
   */
  Utils.isUndefined = function(obj) {
    return obj === void 0;
  };

  /**
   * Check that obj is not undefined/null
   * @param obj
   * @returns {boolean}
   */
  Utils.isDefined = function(obj) {
    return !Utils.isUndefined(obj) && !Utils.isNull(obj);
  };

  /**
   * Returns true if the value of object is null.
   * @param obj
   */
   Utils.isNull = function(obj) {
    return obj === null;
  };

  /**
   * Returns true if object is an Array.
   * @param object
   */
  Utils.isArray = function(obj) {
    return toString.call(obj) == '[object Array]';
  };

  /**
   * Returns true if object is a String.
   * @param object
   */
  Utils.isString = function(obj) {
    return toString.call(obj) == '[object String]';
  };

  /**
   * Returns true if object is a Number.
   * @param object
   */
  Utils.isNumber = function(obj) {
    return toString.call(obj) == '[object Number]';
  };

  /**
   * Returns true if object is a Function.
   * @param object
   */
  Utils.isFunction = function(obj) {
    return toString.call(obj) == '[object Function]';
  };

  /**
   * Returns true if object is Arguments.
   * @param object
   */
  Utils.isArguments = function(obj) {
    return toString.call(obj) == '[object Arguments]';
  };

  // Define a fallback version of the method in browsers (ahem, IE), where
  // there isn't any inspectable "Arguments" type.
  if (!Utils.isArguments(arguments)) {
    Utils.isArguments = function(obj) {
      return !!(obj && Utils.has(obj, 'callee'));
    };
  }

  // Optimize `isFunction` if appropriate.
  if (typeof (/./) !== 'function') {
    Utils.isFunction = function(obj) {
      return typeof obj === 'function';
    };
  }

  // List of HTML entities for escaping.
  var entityMap = {
    escape: {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;'
    }
  };
  entityMap.unescape = Utils.invert(entityMap.escape);

  // Regexes containing the keys and values listed immediately above.
  var entityRegexes = {
    escape:   new RegExp('[' + Utils.keys(entityMap.escape).join('') + ']', 'g'),
    unescape: new RegExp('(' + Utils.keys(entityMap.unescape).join('|') + ')', 'g')
  };

  // Functions for escaping and unescaping strings to/from HTML interpolation.
  Utils.each(['escape', 'unescape'], function(method) {
    Utils[method] = function(string) {
      if (string == null) return '';
      return ('' + string).replace(entityRegexes[method], function(match) {
        return entityMap[method][match];
      });
    };
  });

  /*
   * End necessary underscore components integration
   */

  /**
   * Assert that condition is true and throw an Error with message if it's not.
   * @param condition
   * @param message
   */
  Utils.assert = function(condition, message) {
    if (!condition) {
      throw new Error(message);
    }
  };

  /**
   * Assert that the given value is not undefined/null and throw an Error with message if it is
   * @param value
   * @param message
   */
  Utils.assertDefined = function(value, message) {
    Utils.assert(Utils.isDefined(value), message);
  };

  /**
   * Assert that the given value is undefined/null and throw an Error with message if it isn't
   * @param value
   * @param message
   */
  Utils.assertNotDefined = function(value, message) {
    Utils.assert(!Utils.isDefined(value), message);
  };

  /**
   * Stolen from com.linkedin.util.xmsg.XmsgUtils.doBestEffortNumberConversion
   *
   * Converts an object to a number using a not-so-best-effort approach. Strip all non-digits, including commas,
   * decimal points, and letters, and use parseInt to convert to a number. In case of any error, return 0.
   *
   * TODO: should this be in a NumberFormatter class?
   *
   * @param value
   * @returns {*}
   */
  Utils.bestEffortNumberConversion = function(value) {
    try {
      if (Utils.isDefined(value)) {
        if (Utils.isNumber(value)) {
          return value;
        }

        var digitsOnly = value.replace(/[^0-9]/g, "");
        var asInt = parseInt(digitsOnly, 10);

        if (isNaN(asInt)) {
          return 0;
        } else {
          return asInt;
        }
      }
    } catch(e) {
      // The fallback for any exception is to return 0
    }

    return 0;
  };

  /**
   * IE doesn't have startsWith for String: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/startsWith
   */
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function (searchString, position) {
      position = position || 0;
      return this.indexOf(searchString, position) === position;
    };
  }

  /**
   * Add endsWith to Strings: http://stackoverflow.com/questions/280634/endswith-in-javascript
   */
  if (!String.prototype.endsWith) {
    String.prototype.endsWith = function(suffix) {
      return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
  }

  /**
   * IE doesn't have trim: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim
   *
   */
  if(!String.prototype.trim) {
    String.prototype.trim = function () {
      return this.replace(/^\s+|\s+$/g,'');
    };
  }

  /**
   * Converts ISO format date string to a Date instance
   * @param s - string with ISO date
   * @returns {Date}
   */
  Utils.parseDateString = function( sDate, useTimeZone ){
    var d = new Date(sDate);

    if(isNaN(d)) {
      d = parseISO8601(sDate);
    }

    if(typeof d === 'undefined' || isNaN(d)){
      throw new Error('t8 could not parse date string \'' + sDate + '\'');
    }

    if(useTimeZone) {
      d = convertUTCToLocal(d);
    }

    return d;
  }

  function parseISO8601(s){
    var re = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:.(\d+))?(Z|[+-]\d{2})(?::(\d{2}))?/,
      d = s.match(re),
      i = 0, len;
    if( !d ){
      return null;
    }
    for(i, len = d.length; i < len; i++) {
      d[i] = ~~d[i];
    }
    return new Date(Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6], d[7]) + (d[8] * 60 + d[9]) * 60000);
  }

  /**
   * Converts date from UTC to local time
   * @param date
   * @returns {Date}
   */
  function convertUTCToLocal(date) {
    var localDate = new Date(date.getTime());
    localDate.setMinutes(date.getMinutes() - date.getTimezoneOffset());
    return localDate;
  }

  return Utils;
})();

// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
var tmpl = (function(){
  var cache = {};

  function tmpl(str, data){
    // Figure out if we're getting a template, or if we need to
    // load the template - and be sure to cache the result.
    var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
      // Generate a reusable function that will serve as a template
      // generator (and which will be cached).
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +

        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +

        // Convert the template into pure JavaScript
        str
          .replace(/[\r\t\n]/g, " ")
          .split("<%").join("\t")
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")
          .split("\t").join("');")
          .split("%>").join("p.push('")
          .split("\r").join("\\'")
      + "');}return p.join('');");

    // Provide some basic currying to the user
    return data ? fn( data ) : fn;
  };

  return tmpl;
})();

//Currency Formatting
var CurrencyFormatter = (function(){

  var formatter = function() {};

  formatter.prototype.format = function(amount, currency, locale) {
    var fmt,
        formattedValue = '',
        localeData = __localeData[locale],
        options = {
          style : 'currency',
          currency : currency,
          currencyDisplay: 'code',
          minimumFractionDigits :  2,
          maximumFractionDigits : 2
        };

    if(!localeData) {
      throw new Error('No locale data found for locale ' + locale);
    } else if(!localeData.intlLocale) {
      throw new Error('IntlLocale is not specified for locale ' + locale);
    }

    if(localeData.currency && typeof localeData.currency.getCurrencyDisplay === 'function') {
      options.currencyDisplay = localeData.currency.getCurrencyDisplay(currency);
    }

    //fractions do not make sense for INR and JPY
    if(currency === 'INR' || currency === 'JPY') {
      options.minimumFractionDigits = 0;
      options.maximumFractionDigits = 0;
    }

    try {
      fmt = new Intl.NumberFormat(localeData.intlLocale + '-u-nu-latn-ca-gregory', options);
      formattedValue = fmt.format(amount);
    } catch (err) {
      //in some versions of Firefox formatting with currency display 'code' is broken
      //instead, try formatting with currency display 'symbol' and replace symbol with currency code
      //TODO: Remove this hack once this issue is fixed in Firefox
      if(options.currencyDisplay === 'code') {
        try {
          options.currencyDisplay = 'symbol';
          fmt = new Intl.NumberFormat(localeData.intlLocale + '-u-nu-latn-ca-gregory', options);
          formattedValue = fmt.format(amount);
          var currencySymbols = [
                '(AU|A|US|BR|R|CAN|CA|C|A|HK|NZ|SG)\\u0024', //\u0024 is $ (Dollar Sign)
                '\\u0024(AU|A|US|BR|R|CAN|CA|C|A|HK|NZ|SG)|\\u0024',
                'GB\u00A3|\u00A3GB|\u00A3', //\u00A3 is £ (GBP)
                '\u20AC', //\u20AC is € (Euro Sign)
                '\u0631.\u0647.\u200F', //\u0631.\u0647.\u200F is ر.ه. (Arabic INR)
                'JP\u00A5|\u00A5JP|\u00A5', //\u00A5 is ¥ (Yen Sign)
                '\u20B9|Rs', //\u20B9 is ₹ (Indian Rupee Sign),
                'Dkr', //DKK
                'NKr', //NOK
                'kr'//SEK
              ],
              re = new RegExp('(' + currencySymbols.join('|') + ')');
          formattedValue = formattedValue.replace(re, currency);
        } catch (e) {
          //if everything else fails, fallback to plain amount and currency code
          formattedValue = amount + ' ' + currency;
        }
      }
    }

    //use \u00A0 (no-break space) across the board for all currencies and locales
    formattedValue = formattedValue.replace(/\u0020/g,'\u00A0');

    if(localeData.currency && localeData.currency.postFormatting){
      formattedValue = localeData.currency.postFormatting(currency, amount, formattedValue);
    }

    return formattedValue;
  };

  return formatter;
})();

//Date Formatting
/**
 * Given a piece of text (as a date) and a locale, make the date render in the correct format of that locale.
 */

var DateFormatter = (function(){

  var formatter = function() {};

  function pad(number) {
    if (number < 10) {
      return '0' + number;
    }
    return number;
  }

  function toISODateString(date) {
    return date.getUTCFullYear() +
      '-' + pad(date.getUTCMonth() + 1) +
      '-' + pad(date.getUTCDate());
  }

  formatter.prototype.format = function(date, locale, format, useTimeZone) {
    var fmt,
      formattedValue = "",
      localeData = __localeData[locale],
      options,
      intlLocale,
      dateToFormat = Utils.parseDateString(date, useTimeZone);

    if(format === 'iso'){
      return toISODateString(dateToFormat);
    }

    if(!localeData) {
      throw new Error('No locale data found for locale ' + locale);
    } else if(!localeData.intlLocale) {
      throw new Error('IntlLocale is not specified for locale ' + locale);
    }

    options = localeData.date.intlOptions[format];
    options.timeZone = "UTC";

    try {
      intlLocale = localeData.intlLocale;
      if(intlLocale !== 'zh') {
        intlLocale += '-u-nu-latn-ca-gregory';
      }
      fmt = new Intl.DateTimeFormat(intlLocale, options);
      formattedValue = fmt.format(dateToFormat);
    } catch(ex){
      //if pretty formatting fails, fall back to browser built in toLocaleString
      formattedValue = dateToFormat.toLocaleDateString();
    }

    //IE11 decided to add right to left mark(\u200e) to some formats in all locales
    //There are only few locales that actually need it
    formattedValue = formattedValue.replace(/\u200e/g,'');

    if(localeData.date && localeData.date.postFormatting){
      formattedValue = localeData.date.postFormatting(format, date, formattedValue);
    }

    return formattedValue;
  };

  return formatter;
})();

//Name Formatting
/* global tmpl:true*/

var NameFormatter = (function(){

  var formatter = function() {};

  formatter.formats = {
    FAMILIAR_NAME : 'FAMILIAR_NAME',
    FULL_NAME     : 'FULL_NAME',
    MICROFORMAT   : 'MICROFORMAT',
    LIST_VIEW     : 'LIST_VIEW'
  };

  formatter.templates = {
    MICROFORMAT : {
      firstName  : tmpl('<span class="given-name"><%=value%></span>'),
      lastName   : tmpl('<span class="family-name"><%=value%></span>'),
      maidenName : tmpl('<span class="additional-name"><%=value%></span>')
    },
    FAMILIAR_NAME : {
      defaultTemplate : tmpl('<%=firstName%>'),
      localeTemplates : [
        {
          locales     : ['de_DE', 'nl_NL','pl_PL','ro_RO', 'tr_TR'],
          template    : tmpl('<%=firstName%> <%=lastName%>')
        },
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%> <%=firstName%>')
        }
      ]
    },
    FULL_NAME : {
      defaultTemplate : tmpl('<%=firstName%><%if(maidenName){%> (<%=maidenName%>)<%}%> <%=lastName%>'),
      localeTemplates : [
        {
          locales     : ['ar_AE', 'th_TH'],
          template    : tmpl('<%=firstName%><%if(maidenName){%> <%=maidenName%><%}%><%if(lastName){%> <%=lastName%><%}%>')
        },
        {
          locales     : ['cs_CZ'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> (roz. <%=maidenName%>)<%}%>')
        },
        {
          locales     : ['de_DE'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> geb. <%=maidenName%><%}%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%><%if(firstName){%> <%=firstName%><%}%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['ms_MY'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['nl_NL'],
          template    : tmpl('<%=firstName%> <%=lastName%><%if(maidenName){%>-<%=maidenName%><%}%>')
        },
        {
          locales     : ['pl_PL'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> z d. <%=maidenName%><%}%>')
        }
      ]
    },
    LIST_VIEW: {
      defaultTemplate : tmpl('<%if(lastName){%><%=lastName%>, <%}%><%=firstName%>'),
      localeTemplates : [
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%> <%=firstName%>')
        },
        {
          locales     : ['ar_AE', 'in_ID', 'ms_MY', 'th_TH'],
          template    : tmpl('<%=firstName%> <%=lastName%>')
        }
      ]
    }
  };

  formatter.locales = {
    CJK : 'CJK',
    CJK_ja_JP : 'CJK-ja_JP',
    ja_JP : 'ja_JP'
  };

  formatter.charsets = {
    //Korean uAC00-uD7AF
    korean : {
      //uAC00: parseInt('AC00', 16) = 44032
      lowerbound : 44032,
      //uD7AF: parseInt('D7AF', 16) = 55215
      upperbound : 55215
    },
    CJ : [
      //Japanese Kanji char set u4E00--u9FBF : Common to Japanese and Chinese
      {
        //u4E00: parseInt('4E00', 16) = 19968
        lowerbound : 19968,
        //u9FBF: parseInt('9FBF', 16) = 40895
        upperbound : 40895
      },
      //Japanese Katakana u30A0 - u30FF
      {
        //u30A0: parseInt('30A0', 16) = 12448
        lowerbound : 12448,
        //u30FF: parseInt('30FF', 16) = 12543
        upperbound : 12543
      },
      //Japanese Katakana (Half Width): uFF61 - uFF9F
      {
        //uFF61: parseInt('FF61', 16) = 65377
        lowerbound : 65377,
        //uFF9F: parseInt('FF9F', 16) = 65439
        upperbound : 65439
      },
      //Chinese simplified: GB 2312
      {
        //u3040: parseInt('3040', 16) = 12352
        lowerbound : 12352,
        //u309F: parseInt('309F', 16) = 12447
        upperbound : 12447
      }
    ]
  };

  formatter.prototype.htmlEncode = function(str) {
    if((str === null) || (str === undefined)) {
      return null;
    }

    return str.toString().replace(/(.)/g, function(a) {
      if (a === '<') {
        return '&lt;';
      } else if (a === '>') {
        return '&gt;';
      } else if (a === '&') {
          return '&amp;';
      } else if (a === '"') {
        return '&quot;';
      } else {
        if (a.charCodeAt(0) < 127) {
          return a;
        }
        return '&#x' + a.charCodeAt(0).toString(16).toLowerCase() + ';';
      }
    });
  };

  formatter.prototype.format = function(name, format, locale) {

    function isKoreanCharset(lastName) {
      if (!lastName) {
        return false;
      }
      var koreanCharset = NameFormatter.charsets.korean,
        firstCharCode = lastName.charCodeAt(0);
      return firstCharCode >= koreanCharset.lowerbound && firstCharCode <= koreanCharset.upperbound;
    }

    function isCJCharset(lastName) {
      if (!lastName) {
        return false;
      }
      var firstCharCode = lastName.charCodeAt(0);
      return Utils.some(NameFormatter.charsets.CJ, function(charset) {
        return firstCharCode >= charset.lowerbound && firstCharCode >= charset.upperbound;
      });
    }

    function getLocaleTemplate(formatTemplates, locale) {
      var localeTemplate = Utils.find(formatTemplates.localeTemplates, function(currLocaleTemplate) {
        return Utils.indexOf(currLocaleTemplate.locales, locale) >= 0;
      });
      return localeTemplate ? localeTemplate.template : formatTemplates.defaultTemplate;
    }

    function getFormatTemplates(format) {
      var formats = NameFormatter.formats,
        templates = NameFormatter.templates;

      if (!format) {
        return templates.FAMILIAR_NAME;
      }

      if (Utils.isString(format)) {
        format = [format];
      }

      if (! Utils.isArray(format)) {
        return templates.FAMILIAR_NAME;
      }
      if (Utils.indexOf(format, formats.FULL_NAME) >= 0) {
          return templates.FULL_NAME;
      }
      if (Utils.indexOf(format, formats.LIST_VIEW ) >= 0) {
          return templates.LIST_VIEW;
      }
      return templates.FAMILIAR_NAME;
    }

    function hasMicroformat(format) {
      if (format) {
        if (Utils.isArray(format)) {
          return (Utils.indexOf(format, NameFormatter.formats.MICROFORMAT) >=0);
        } else if (Utils.isString(format)) {
          return format === NameFormatter.formats.MICROFORMAT;
        }
      }
      return false;
    }

    function processInputValue(value, isHTMLOutput, useMicroformat, htmlEncode, microformatTemplate) {
      if (!value) {
        return '';
      }

      //strip leading and trailing whitespace
      var processedValue = value.replace(TRIM_WHITESPACE_REGEX,'');
      //html encode if expected output is HTML
      if (isHTMLOutput) {
        processedValue = htmlEncode(processedValue);
      }
      //apply microformat template if MICROFORMAT format is requested
      if (useMicroformat) {
        processedValue = microformatTemplate({ value: processedValue });
      }
      return processedValue;
    }

    var TRIM_WHITESPACE_REGEX = /(^\s+|\s+$)/g,
      useMicroformat        = hasMicroformat(format),
      isHTMLOutput          = (useMicroformat || name.lastNameWithHighlight),
      microformatTemplates  = NameFormatter.templates.MICROFORMAT,
      firstName             = processInputValue(name.firstName, isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.firstName),
      lastName              = processInputValue(name.lastName,  isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.lastName),
      maidenName            = processInputValue(name.maidenName,isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.maidenName),
      //do not html encode lastNameWithHighlight because it was intentionally formatted with markup and that markup needs to be preserved
      lastNameWithHighlight = processInputValue(name.lastNameWithHighlight, false, useMicroformat, this.htmlEncode, microformatTemplates.lastName),
      formatTemplate        = '',
      nameFormatted         = '';

    //if last name starts with a CJK (chinese, japanese or korean) character we need to use CJK name format
    //looking at the non-processed value of last name to determine character set
    //(in order to maintain same behavior as JAVA version of name formatter)
    if (isKoreanCharset(name.lastName)) {
      //if it's korean character always use CJK format
      locale = NameFormatter.locales.CJK;
    } else if (isCJCharset(name.lastName)) {
      //if it's chinese or japanese and locale is ja_JP need to use special japanese display format
      if (locale === NameFormatter.locales.ja_JP) {
        locale = NameFormatter.locales.CJK_ja_JP;
      } else {
        //otherwise use standard CJK format
        locale = NameFormatter.locales.CJK;
      }
    }

    //get the template string we'll use to format the name
    formatTemplate = getLocaleTemplate(getFormatTemplates(format), locale);

    //format the name according to requested format
    nameFormatted = formatTemplate({
      firstName: firstName,
      lastName: lastNameWithHighlight? lastNameWithHighlight : lastName,
      maidenName: maidenName
    });

    //remove any trailing and leading whitespaces in resulting name
    nameFormatted = nameFormatted.replace(TRIM_WHITESPACE_REGEX,'');

    return nameFormatted;
  };

  return formatter;
})();

//Number Formatting
var NumberFormatter = (function(){

  var formatter = function() {};

  formatter.prototype.format = function(value, locale) {

    var fmt,
      formattedValue = '',
      localeData = __localeData[locale],
      options = {
        maximumFractionDigits : 3
      };

    if(!localeData) {
      throw new Error('No locale data found for locale ' + locale);
    } else if(!localeData.intlLocale) {
      throw new Error('IntlLocale is not specified for locale ' + locale);
    }

    if(localeData.number && localeData.number.maximumFractionDigits) {
      options.maximumFractionDigits = localeData.number.maximumFractionDigits;
    }

    try {
      fmt = new Intl.NumberFormat(localeData.intlLocale + '-u-nu-latn-ca-gregory', options);
      formattedValue = fmt.format(value);
    } catch (err) {
      //fallback to not formatted value if error happens
      formattedValue = value + '';
    }

    if(localeData.number && localeData.number.postFormatting) {
      formattedValue = localeData.number.postFormatting(value, formattedValue);
    }

    return formattedValue;
  };

  return formatter;
})();

//Possessive Formatting
/**
 * Given a piece of text and a locale, make the text possessive using the proper rules for the locale. This code
 * depends on src/formatters/possessive-grammar/*.js for the language-specific possessive definitions.
 *
 * Example: new t8.Possessive().format("Jim", "en_US")
 *
 * Output: Jim's
 *
 */
var Possessive = (function(){

  var formatter = function() {};

  formatter.prototype.format = function(text, locale) {
    var localeData = __localeData[locale],
        possessiveRules = localeData.possessive ? localeData.possessive : {} ;

    if (possessiveRules) {
      var suffixFromRules = Utils.find(possessiveRules.rules, function(suffix, regexString) {
        var regex = new RegExp(regexString);
        return regex.test(text);
      });

      if (Utils.isDefined(suffixFromRules)) {
        return text + suffixFromRules;
      } else if (possessiveRules.fallback) {
        return text + possessiveRules.fallback;
      }
    }

    return text;
  };

  return formatter;
})();

//Time Formatting
/**
 * Given a piece of text (as a date) and a locale, make the time render in the correct format of that locale.
 */

var TimeFormatter = (function(){

  var formatter = function() {};

  formatter.prototype.format = function(date, locale, format, useTimeZone) {
    var fmt,
      formattedValue = "",
      localeData = __localeData[locale],
      options,
      intlLocale,
      dateToFormat = Utils.parseDateString(date, useTimeZone),
      intlOptions = {
        'hm': {
          hour: "numeric",
          minute:"numeric"
        },
        'hms': {
          hour: "numeric",
          minute: "numeric",
          second: "numeric"
        }
      };

    if(!localeData) {
      throw new Error('No locale data found for locale ' + locale);
    } else if(!localeData.intlLocale) {
      throw new Error('IntlLocale is not specified for locale ' + locale);
    }

    options = (localeData.time && localeData.time.intlOptions) ?
      localeData.time.intlOptions[format] :
      intlOptions[format];

    //default to hms
    if(!options) {
      options = intlOptions.hms;
    }

    options.timeZone = "UTC";

    try {
      intlLocale = localeData.intlLocale;
      if(intlLocale !== 'zh') {
        intlLocale += '-u-nu-latn-ca-gregory';
      }
      fmt = new Intl.DateTimeFormat(intlLocale, options);
      formattedValue = fmt.format(dateToFormat);
    } catch(ex){
      //if pretty formatting fails, fall back to browser built in toLocaleString
      formattedValue = dateToFormat.toLocaleTimeString();
    }

    //IE11 decided to add right to left mark(\u200e) to some formats in all locales
    //There are only few locales that actually need it
    formattedValue = formattedValue.replace(/\u200e/g,'');

    if(localeData.time && localeData.time.postFormatting){
      formattedValue = localeData.time.postFormatting(format, date, formattedValue);
    }

    return formattedValue;
  };

  return formatter;
})();

//Truncation Formatting
var TruncationFormatter = (function(){

  var _ellipsis = '...',
    formatter =  function(ellipsis) {
      _ellipsis = typeof(ellipsis) !== 'undefined' ? ellipsis : _ellipsis;
    };

  /**
   * Returns the string truncated at the size if it is bigger.
   * @param value {string}
   * @param limit {integer}
   * @return substring of original {string}, truncated at the specified limit
   */
  formatter.prototype.format = function(value, limit) {
    if(!value || typeof value !== 'string') {
      // this is to help maintain functional parity
      if(typeof(value) === 'undefined' || value === '') {
        return '';
      }
      return null;
    }
    // this is to help maintain functional parity
    if(typeof limit === 'undefined') {
      return '...';
    }

    if(!limit ||
      typeof limit !== 'number' ||
      limit >= value.length ||
      limit < 0 ||
      value.replace(/\s/g,'').length === 0) {
      return value;
    }

    var truncated = value.substr(0, limit),
      chars = truncated.split(''),
      idx = limit - 1,
      output = '',
      regexPunctuation = /\s|\?|\!|\.|\,|\;|\:/g; // whitespace or punctuation regex

    while(idx >= 0) {
      if(!regexPunctuation.test(chars[idx])) {
        idx--;
      } else {
        break;
      }
    }

    if(idx > 0) {
      output = truncated.substr(0, idx);
    } else {
      // case where we couldn't find a single white space
      output = truncated;
    }

    output += _ellipsis;

    return output;
  };

  return formatter;
})();

//Chooser - handle single\plural\dual cases
/**
 * Given a list of chooser rules and a value, pick the rule that best matches the value for the current locale. This is
 * usually used for picking the proper singular/plural text based on some numeric value.
 *
 * This code depends on src/i18n/chooser-grammar/*.js files for the language-specific category definitions.
 *
 * There are two patterns of usage.
 *
 * Pattern one: purely numeric
 * ===========================
 *
 * In this pattern, all args are numbers and are treated as ranges.
 *
 * Example rules:
 *
 * [
 *   {
 *     "arg": 0,
 *     "comparison": "gte",
 *     "text": "zero"
 *   },
 *   {
 *     "arg": 2,
 *     "comparison": "gte",
 *     "text": "two"
 *   },
 *   {
 *     "arg": 20,
 *     "comparison": "gt",
 *     "text": "more than 20"
 *   }
 * ]
 *
 * Each rule is an object with an "arg" field that has a numeric value, a comparison type (gt = greater than, gte =
 * greater than or equal), and the text to use if that object is selected. The chooser will return the text for the
 * object with the largest arg that is less than the given value.
 *
 * For the example above:
 *
 *   1. A value of 0 returns "zero"
 *   2. Values 2 - 20 returns "two"
 *   3. Any value greater than 20 returns "more than 20"
 *
 *
 * Pattern 2: categories
 * ===========================
 *
 * In this pattern, in addition to numbers, you can use categories like "singular" and "plural" that are pre-defined
 * for every locale. Note that numbers here are treated as exact matches and not ranges.
 *
 * Example rules:
 *
 * [
 *   {
 *     "category": "singular",
 *     "comparison": "eq",
 *     "text": "connection"
 *   },
 *   {
 *     "category": "plural",
 *     "comparison": "eq",
 *     "text": "connections"
 *   }
 * ]
 *
 * Here, the rule selected depends on the grammar rules defined for the current locale in src/i18n/chooser-grammar/[locale].js file.
 * Note that numbers can also be included in the list of rules, but as soon as you have a single category, numbers are
 * treated as exact matches and not ranges.
 *
 */

var Chooser = (function(){
  var formatter = function() {},
      isDefined = Utils.isDefined,
      grammarLongestEndsWith = 2,
      defaultGrammarRules;

  formatter.CATEGORIES = {
    SINGULAR : 0,
    PLURAL : 1,
    DUAL : 2,
    FEW : 3,
    MANY : 4,
    ZERO : 5
  };

  defaultGrammarRules = {
    'equals': {
      '1': formatter.CATEGORIES.SINGULAR
    },
    'endsWith': {
      '0': formatter.CATEGORIES.PLURAL,
      '1': formatter.CATEGORIES.PLURAL,
      '2': formatter.CATEGORIES.PLURAL,
      '3': formatter.CATEGORIES.PLURAL,
      '4': formatter.CATEGORIES.PLURAL,
      '5': formatter.CATEGORIES.PLURAL,
      '6': formatter.CATEGORIES.PLURAL,
      '7': formatter.CATEGORIES.PLURAL,
      '8': formatter.CATEGORIES.PLURAL,
      '9': formatter.CATEGORIES.PLURAL
    }
  };

  formatter.COMPARISONS = {
    eq: function(left, right) { return left === right; },
    gt: function(left, right) { return left > right; },
    gte: function(left, right) { return left >= right; },
    endsWith: function(left, right) { return left.toString().endsWith(right.toString()); }
  };

  formatter.prototype.findRule = function(rules, value, comparison) {
    return Utils.find(rules, function(rule) {
      return isDefined(Utils.find(rule.values, function(v) { return comparison(value, v); }));
    });
  };

  formatter.prototype.pickCategory = function(gramma, value, longestEndsWith) {
    if (isDefined(gramma) && isDefined(value) && isDefined(longestEndsWith)) {

      // Look for an exact match
      var asString = value.toString();
      if (isDefined(gramma.equals) && isDefined(gramma.equals[asString])) {
        return gramma.equals[asString];
      }

      // Look for a category match, prefering longer matches over shorter ones
      if (isDefined(gramma.endsWith)) {
        var max = Math.min(longestEndsWith, asString.length);
        for (var i = max; i > 0; i--) {
          var suffix = asString.slice(-1 * i);
          if (isDefined(gramma.endsWith[suffix])) {
            return gramma.endsWith[suffix];
          }
        }
      }
    }

    return undefined;
  };

  formatter.prototype.findCategoryMatch = function(value, rules, grammarRules) {
    // When matching with categories, only integers are supported
    value = Math.floor(value);

    var numberMatch = this.findNumberMatchNoRanges(value, rules);
    if (isDefined(numberMatch)) {
      return numberMatch;
    }
    var categoryId = this.pickCategory(grammarRules, value, grammarLongestEndsWith),
        category;

    if (isDefined(categoryId)) {
      for(var currCategory in formatter.CATEGORIES){
        if(formatter.CATEGORIES[currCategory] === categoryId){
          category = currCategory.toLowerCase();
        }
      }
      return Utils.find(rules, function(rule) {
        return rule.category === category;
      });
    }
    return undefined;
  };

  formatter.prototype.findNumberMatchNoRanges = function(value, rules) {
    var rulesWithoutRanges = Utils.map(rules, function(rule) {
      if (isDefined(rule.arg) && rule.comparison === 'gte') {
        return Utils.extend({}, rule, {comparison: 'eq'});
      } else {
        return rule;
      }
    });

    return this.findNumberMatch(value, rulesWithoutRanges);
  };

  formatter.prototype.findNumberMatch = function(value, rules) {
    var bestMatch;

    for (var i = 0; i < rules.length; i++) {
      var rule = rules[i];
      var comparison = Chooser.COMPARISONS[rule.comparison];

      if (comparison(value, rule.arg) && (!isDefined(bestMatch) || rule.arg > bestMatch.arg)) {
        bestMatch = rule;
      }
    }

    return bestMatch;
  };

  formatter.prototype.isValidCategory = function(str) {
    return typeof formatter.CATEGORIES[str.toUpperCase()] !== 'undefined';
  };

  formatter.prototype.format = function(value, rules, locale) {
    var localeData = __localeData[locale],
        grammarRules = localeData.chooser ? localeData.chooser : defaultGrammarRules,
        numericValue = Utils.bestEffortNumberConversion(value),
        hasCategory, match, text;

    hasCategory = Utils.find(rules, function(rule) {
      return isDefined(rule.category);
    });

    if(hasCategory) {
      match = this.findCategoryMatch(numericValue, rules, grammarRules);
    } else {
      match = this.findNumberMatch(numericValue, rules);
    }

    if(match) {
      text = match.text;
      if (Utils.isFunction(text)) {
        return text();
      }
    }
    return text;
  };

  return formatter;
})();

//Resources - retrieve and format i18n string
/* global t8:true*/

/**
 * Client side i18n support. This class knows how to look up static and dynamic i18n keys in JavaScript. Static i18n
 * keys are expected to be in static i18n cache; dynamic keys are expected to be in dynamic i18n cache provided to
 * t8.Resources constructor
 *
 * var i18nStaticCache = {
 *   cache : {
 *     "mynamespace" : {
 *       "hello_world" : "Hello World"
 *     }
 *   }
 * },
 * i18nDynamicCache = {
 *   cache : {}
 * },
 * resources = new t8.Resources(i18nStaticCache, i18nDynamicCache);
 * resources .get("hello_world", "mynamespace");
 *
 * Output: "Hello World"
 *
 * t8 expects t8.renderDynamicString function to be defined by the user of t8.
 * t8.renderDynamicString function implements rendering logic of dynamic templates for
 * i18n strings with params. At LinkedIn, this function is defined in dust ui helpers
 * https://gitli.corp.linkedin.com/dust-ui-helpers/dust-ui-helpers/source/lib/dust-i18n-helpers.js#L9
 */
var Resources = (function(){
  var formatter  = function(i18nCacheStatic, i18nCacheDynamic) {
    this.i18nCacheStatic = i18nCacheStatic;
    this.i18nCacheDynamic = i18nCacheDynamic;
  };

  var DYNAMIC_KEY_PREFIX = "__i18n__";

  formatter.prototype.get = function(key, namespace, context, callback) {
    Utils.assert(callback, "get called with null callback");
    Utils.assert(key, "get called with null or empty key");
    Utils.assert(namespace, "get called with null or empty namespace");

    var staticString = this.getStaticString(key, namespace);
    if (Utils.isDefined(staticString)) {
      callback(null, staticString);
    } else {
      this.renderDynamicString(key, namespace, context, callback);
    }
  };

  formatter.prototype.getStaticString = function(key, namespace) {
    Utils.assert(key, "getStaticString called with null or empty key");
    Utils.assert(namespace, "getStaticString called with null or empty namespace");

    if (this.i18nCacheStatic && this.i18nCacheStatic.cache && this.i18nCacheStatic.cache[namespace]) {
      return this.i18nCacheStatic.cache[namespace][key];
    }
    return;
  };

  formatter.prototype.renderDynamicString = function(key, namespace, context, callback) {
    Utils.assert(callback, "renderDynamicString called with null callback");
    Utils.assert(key, "renderDynamicString called with null or empty key");
    Utils.assert(namespace, "renderDynamicString called with null or empty namespace");

    var dynamicKeyName = this.dynamicKeyName(key, namespace);
    if (this.i18nCacheDynamic && this.i18nCacheDynamic.cache && this.i18nCacheDynamic.cache[dynamicKeyName]) {
      //call user provided function to render dynamic i18n string
      t8.renderDynamicString(dynamicKeyName, this.i18nCacheDynamic.cache[dynamicKeyName], context, callback);
    } else {
      callback("Could not find static i18n key " + key + " in static i18n cache nor dynamic i18n template " + dynamicKeyName + " in dynamic i18n cache.");
    }
  };

  formatter.prototype.dynamicKeyName = function(key, namespace) {
    Utils.assert(key, "dynamicKeyName called with null or empty key");
    Utils.assert(namespace, "dynamicKeyName called with null or empty namespace");

    return DYNAMIC_KEY_PREFIX + namespace + "__" + key;
  };

  return formatter;
})();

//RTL
var Rtl = (function(){
  var formatter = function() {};

  /** ISO-LATIN-1 white space */
  var WHITESPACES = " \n\r\t\f\u00A0\u2028\u2029".split(""),

  /** Punctuation characters and other characters to exclude */
  EXCLUDED_CHARACTERS = "~!@#$%^&*()_+`1234567890-={}|[]\\:\";'<>?,./".split(""),

  /** Set of characters to exclude when scanning for first character to validate */
  EXCLUSION_SET = WHITESPACES.concat(EXCLUDED_CHARACTERS),

  /** Hebrew character code for lower limit. Range is 0590–05FF */
  ARABIC_LOW = '\u0590',

  /** Arabic character code for upper limit. Range is 0600–06FF */
  ARABIC_HIGH = '\u06FF';

  /**
   * Return index of first non-whitespace/punctuation
   * character if its an Arabic character or -1 if first
   * character is not an Arabic character.
   *
   * @param content content to check
   * @param contentLength length of string
   * @return index of first non-whitespace character
   *   if it is an Arabic character, -1 otherwise.
   * @see {@link WHITESPACES}
   * @see {@link EXCLUDED_CHARACTERS}
   */
  function firstRtlCharacter(content)
  {
    if(!Utils.isDefined(content)) {
      return -1;
    }

    var i = 0,
        ch = '\0',
        contentLength = content.length;

    for(i = 0; i < contentLength; i ++) {
      ch = content.charAt(i);
      if(!Utils.contains(EXCLUSION_SET, ch)) {
        break;
      }
    }

    if (i >= contentLength) {
      return -1;
    } else {
      return (ch >= ARABIC_LOW && ch <= ARABIC_HIGH) ? i : -1;
    }
  }

  /**
   * Decode the html content before evaluating the first RTL character
   * htmlDecode function used here is required by security team
   * and is implemented by Roman Shafigullin using RegExp
   *
   * Decode a given string by replacing a well defined set of html entities
   * and all XML save entities into their corresponding character.
   * The following characters are converted:
   * <xmp>
   * &amp;lt;    ->  &lt;
   * &amp;gt;    ->  &gt;
   * &amp;amp;   ->  &amp;
   * &amp;quot;  ->  &quot;
   * &amp;nbsp;  ->  [non-breaking space]
   * &#xXX;      ->  unescape("%" + XX)
   * &#X;        ->  String.fromCharCode(X)
   * &#0; &x#0;  ->  String.fromCharCode(0xfffd) null byte character is not allowed
   * </xmp>
   * @method decodeHTML
   * @param {String} encodedText The string to decode.
   * @return {String} The decoded string.
  */
  var htmlDecode = (function(undefined) {
    var namedEntities = {
      'nbsp': '\u00a0',
      'lt': '<',
      'gt': '>',
      'amp': '&',
      'quot': '"'
    };

    var rEntities = /&(?:(lt|gt|amp|quot|nbsp)|#x([\da-f]{1,4})|#(\d{1,5}));/ig;

    return function( encodedText ) {
      if (encodedText === null || encodedText === undefined) {
        return null;
      }

      return (encodedText + '').replace(rEntities, function(match, named, hex, dec) {
        // if it matched a named entity...
        if (named) {
          // return the mapped character
          return namedEntities[named];
        }
        // if it was a numeric entity...
        else if (hex || dec) {
          // return the character converted from the numeral
          return String.fromCharCode(parseInt(hex || dec, hex ? 16 : 10) || 0xfffd);
        }
        // otherwise...
        return '\ufffd';
      });
    };
  }());

  /**
   * Find the first non-whitespace/punctuation
   * character and determine whether it falls
   * into a character sequence which indicates
   * right-to-left content.
   *
   * @param content content to verify
   * @return true if content is right-to-left,
   *   false otherwise.
   * @see {@link WHITESPACES}
   * @see {@link EXCLUDED_CHARACTERS}
   *
   */

  formatter.prototype.isRtl = function(content) {
    return firstRtlCharacter(htmlDecode(content)) !== -1;
  };

  return formatter;
})();

  return {
    Chooser: Chooser,
    CurrencyFormatter : CurrencyFormatter,
    DateFormatter : DateFormatter,
    NameFormatter: NameFormatter,
    NumberFormatter: NumberFormatter,
    Possessive: Possessive,
    Resources: Resources,
    Rtl: Rtl,
    TimeFormatter: TimeFormatter,
    TruncationFormatter: TruncationFormatter,
    Utils: Utils,
    __addLocaleData : function(locale, localeData) {
      __localeData[locale] = localeData;
    }
  };

}));

(function(){
  var localeData = {
    intlLocale: 'en',
    date: {
      intlOptions: {}
    },
    time: {},
    currency: {},
    number: {}
  };

  localeData.date.postFormatting = function(format, date, formattedValue) {
  var formatName = format.split('.')[0];

  formattedValue = formattedValue
    .replace(/\s0/, ' '); //remove leading 0 for day and month
  //no need for comma in md formats
  if(formatName === 'my') {
    formattedValue = formattedValue.replace(/,/g, '');
  } else if (formatName === 'time'){
    formattedValue = formattedValue.split(' ');
    if(formattedValue.length === 5 && !/,$/.test(formattedValue[2])) {
      //medium or long
      formattedValue[2] = formattedValue[2] + ',';
    } else if(formattedValue.length === 3 && !/,$/.test(formattedValue[0])) {
      //medium or long
      formattedValue[0] = formattedValue[0]+ ',';
    }
    formattedValue = formattedValue.join(' ');
  }

  return formattedValue;
};

var intlOpts = localeData.date.intlOptions;

//date.time formats
intlOpts['time'] = {
  year     :'numeric',
  month    :'long',
  day      :'numeric',
  hour     :'numeric',
  minute   :'numeric'
};
intlOpts['time.long'] = intlOpts['time'];
intlOpts['time.medium'] = intlOpts['time'];
intlOpts['time.short'] = {
  year     :'2-digit',
  month    :'numeric',
  day      :'numeric',
  hour     :'numeric',
  minute   :'numeric'
};

//date.mdy formats
intlOpts['mdy'] = {
  year : 'numeric',
  month: 'long',
  day  : 'numeric'
};
intlOpts['mdy.long'] = intlOpts['mdy'];
intlOpts['mdy.medium'] = {
  year : 'numeric',
  month: 'short',
  day  : 'numeric'
};
intlOpts['mdy.short'] = {
  year : 'numeric',
  month: 'numeric',
  day  : 'numeric'
 };

//date.my formats
intlOpts['my'] = {
  year : 'numeric',
  month: 'long'
};
intlOpts['my.long'] = intlOpts['my'];
intlOpts['my.medium'] = {
  year : 'numeric',
  month: 'short'
};
intlOpts['my.short'] = intlOpts['my.medium'];

//date.md formats
intlOpts['md'] = {
  month: 'long',
  day :  'numeric'
};
intlOpts['md.long'] = intlOpts['md'];
intlOpts['md.medium'] = {
  month: 'short',
  day :  'numeric'
};
intlOpts['md.short'] = intlOpts['md.medium'];

//date.m formats
intlOpts['m'] = {
  month: 'long'
};
intlOpts['m.long'] = intlOpts['m'];
intlOpts['m.medium'] = {
  month: 'short'
};
intlOpts['m.short'] = {
  month: 'numeric'
};

//date.d formats
intlOpts['d'] = {
  weekday: 'long'
};
intlOpts['d.long'] = intlOpts['d'];
intlOpts['d.medium'] = {
  weekday: 'short'
};
intlOpts['d.short'] = {
  day: 'numeric'
};

//date.y formats
intlOpts['y'] = {
  year: 'numeric'
};
intlOpts['y.long'] = intlOpts['y'];
intlOpts['y.medium'] = intlOpts['y'];
intlOpts['y.short'] = {
  year:'2-digit'
};
  
  localeData.currency.getCurrencyDisplay = function(currency){
  if(/^(DKK|NOK|SGD|ZAR|SEK|CHF)$/.test(currency)) {
    return 'code';
  }
  return 'symbol';
};

localeData.currency.postFormatting = function(currency, amount, formattedValue){
  var dollarSignRegex = /^(\(|-)?\$/,
    currencySymbol = {
      AUD: 'A$',
      CAD: 'CA$',
      HKD: 'HK$',
      NZD: 'NZ$'
    };
  //UH-OH IE11 formats several currencies as $
  if(typeof currencySymbol[currency] !== 'undefined') {
    formattedValue = formattedValue.replace(dollarSignRegex, currencySymbol[currency]);
  } else if (currency === 'INR'){
    //use Ruppe symbol
    formattedValue = formattedValue.replace(/Rs\./, '\u20B9');
  }

  //Some browsers still format negative amounts using accounting style with ()
  //Replace () with -
  if(amount < 0 && formattedValue.indexOf(')') >= 0) {
    formattedValue = '\u002D' + formattedValue.replace(/[\(\)]/g,'');
  }

  return formattedValue.replace(/\s/,'');
};
  
  
  localeData.possessive = {
  "fallback": "\u2019s",
  "rules": {
    ".*[Ss]$": "\u2019",
    ".*[A-RT-Z]$": "\u2019S",
    ".*[a-rt-z]$": "\u2019s"
  }
};

  t8.__addLocaleData('en_US', localeData);
})();

/*! dust-ui-helpers - v1.8.0 Copyright © 2015 LinkedIn Corporation */
(function(root, factory) {
  factory(dust, t8);
}(this, function(dust, t8) {

  var dustVars = {
    i18n: dust.i18n || {cache: {}}
  };

  t8.renderDynamicString = function (dynamicTemplateName, dynamicTemplate, context, callback) {
    dust.render(dynamicTemplateName, context, callback);
  };

  // shared by dirAttr and isRtl
  var rtl = new t8.Rtl();

  //These variables are used by @format helper.
  //Variable `formatters` is a cache of t8's formatter instances. They will be
  //created and stored in cache on demand, when @format helper is called.
  var formatters = {},
    //Variable `formatHelpers` is  cache of helper functions for formatting.
    //They act as a bridge between @format helper input and t8's interface.
    formatHelpers = {
      name: function (format, locale, params, chunk, context) {
        var firstName = dust.helpers.tap(params.firstName, chunk, context),
          lastName = dust.helpers.tap(params.lastName, chunk, context),
          maidenName = dust.helpers.tap(params.maidenName, chunk, context),
          lastNameWithHighlight = dust.helpers.tap(params.lastNameWithHighlight, chunk, context);

        //create a new instance of NameFormatter if it does not exist yet
        if (!formatters.name) {
          if (typeof t8.NameFormatter !== 'undefined') {
            formatters.name = new t8.NameFormatter();
          } else {
            return dust.log('@format helper can not create instance of NameFormatter. t8.NameFormatter is null or undefined', 'ERROR');
          }
        }
        //mapping between helper format names and formats that NameFormatter understands
        var formatMap = {
          'familiar': 'FAMILIAR_NAME',
          'full'    : 'FULL_NAME',
          'list'    : 'LIST_VIEW',
          'micro'   : 'MICROFORMAT'
        },
        requestedFormat = format.split('.'),
        nameFormats = [];

        //convert helper formats into array of formats that NameFormatter can digest
        for(var i = 0; i < requestedFormat.length; i++) {
          //if requested format can be mapped to a format that t8.NameFormatter understands
          //add it to a list of formats we'll pass into NameFormatter.format call
          if (formatMap[requestedFormat[i]]) {
            nameFormats.push(formatMap[requestedFormat[i]]);
          }
        }

        return formatters.name.format({
          firstName: firstName,
          lastName: lastName,
          maidenName: maidenName,
          lastNameWithHighlight: lastNameWithHighlight
        }, nameFormats, locale);
      },
      date: function (format, locale, params, chunk, context) {
        var date = dust.helpers.tap(params.date, chunk, context),
          useTimeZone = dust.helpers.tap(params.useTimeZone, chunk, context);

        if (!formatters.date) {
          if (typeof t8.DateFormatter !== 'undefined') {
            formatters.date = new t8.DateFormatter();
          } else {
            return dust.log('@format helper can not create instance of DateFormatter. t8.DateFormatter is null or undefined', 'ERROR');
          }
        }

        if(!/^date\.(time|mdy|my|md|m|d|y|iso)(\.(long|medium|short))?$/.test(format)) {
          dust.log('@format helper was called with invalid format ' + format + '. Falling back to default date.mdy.long', 'WARN');
          format = 'mdy.long';
        } else {
          //t8.DateFormatter expects "mdy.long" instead of "date.mdy.long"
          format = format.replace(/date\./,'');
        }

        return formatters.date.format(date, locale, format, useTimeZone);
      },
      time: function (format, locale, params, chunk, context) {
        var date = dust.helpers.tap(params.date, chunk, context),
          useTimeZone = dust.helpers.tap(params.useTimeZone, chunk, context);

        if (!formatters.time) {
          if (typeof t8.TimeFormatter !== 'undefined') {
            formatters.time = new t8.TimeFormatter();
          } else {
            return dust.log('@format helper can not create instance of TimeFormatter. t8.TimeFormatter is null or undefined', 'ERROR');
          }
        }

        if(!/^time\.(hm|hms)?$/.test(format)) {
          dust.log('@format helper was called with invalid format ' + format + '. Falling back to default time.hms', 'WARN');
          format = 'hms';
        } else {
          //t8.TimeFormatter expects "hms" instead of "time.hms"
          format = format.replace(/time\./,'');
        }

        return formatters.time.format(date, locale, format, useTimeZone);
      },
      currency: function (format, locale, params, chunk, context) {
        var formattedValue,
          amount = dust.helpers.tap(params.amount, chunk, context),
          defaultAmount = dust.helpers.tap(params.defaultAmount, chunk, context),
          currency = dust.helpers.tap(params.currency, chunk, context);

        if (!formatters.currency) {
          if (typeof t8.CurrencyFormatter !== 'undefined') {
            formatters.currency = new t8.CurrencyFormatter();
          } else {
            return dust.log('@format helper can not create instance of CurrencyFormatter. t8.CurrencyFormatter is null or undefined', 'ERROR');
          }
        }

        if(isNaN(amount)) {
          if(isNaN(defaultAmount)){
            formattedValue = '';
          } else {
            formattedValue = formatters.currency.format(defaultAmount, currency, locale);
          }
        } else {
          formattedValue = formatters.currency.format(amount, currency, locale);
        }
        return formattedValue;
      },
      number: function (format, locale, params, chunk, context) {
        var formattedValue,
          value = dust.helpers.tap(params.value, chunk, context),
          defaultValue = dust.helpers.tap(params.defaultValue, chunk, context);

        if (!formatters.number) {
          if (typeof t8.NumberFormatter !== 'undefined') {
            formatters.number = new t8.NumberFormatter();
          } else {
            return dust.log('@format helper can not create instance of NumberFormatter. t8.NumberFormatter is null or undefined', 'ERROR');
          }
        }

        if(isNaN(value)) {
          if(isNaN(defaultValue)){
            formattedValue = '';
          } else {
            formattedValue = formatters.number.format(defaultValue, locale);
          }
        } else {
          formattedValue = formatters.number.format(value, locale);
        }
       return formattedValue;
      },
      string: function (format, locale,params, chunk, context) {
        var value = dust.helpers.tap(params.value, chunk, context),
            limit = dust.helpers.tap(params.limit, chunk, context);

        if (!formatters.truncation) {
          if (typeof t8.TruncationFormatter !== 'undefined') {
            formatters.truncation = new t8.TruncationFormatter();
          } else {
            return dust.log('@format helper can not create instance of stringFormatter. t8.stringFormatter is null or undefined', 'ERROR');
          }
        }
        return formatters.truncation.format(value, limit);
      }
    };

  //used by @choice helper
  var chooser;

  function getChooserParams(chunk, context, bodies, params) {
    var CHOOSER_TYPE_NUMBER = 'number',
      CHOOSER_TYPE_BOOLEAN = 'boolean',
      CHOOSER_TYPE_STRING = 'string',
      CHOOSER_DEFAULT_BLOCK = 'block',
      type = params.type ? params.type : CHOOSER_TYPE_NUMBER,
      omitParams = ['key', 'type', 'locale'],
      hasCategories = false,
      categories = {},
      key;

    //get categories for chooser rules skipping reserved params for key, locale and type
    for(key in params) {
      if (omitParams.indexOf(key) < 0) {
        hasCategories = true;
        categories[key] = params[key];
      }
    }

    for(key in bodies) {
      if(key !== CHOOSER_DEFAULT_BLOCK) {
        hasCategories = true;
        categories[key] = bodies[key];
      }
    }

    return  {
      key:  dust.helpers.tap(params.key, chunk, context),
      locale: getLocale(params),
      hasCategories: hasCategories,
      categories: categories,
      isBooleanComparison: type === CHOOSER_TYPE_BOOLEAN,
      isStringComparison: type === CHOOSER_TYPE_STRING,
      isNumericComparison: type === CHOOSER_TYPE_NUMBER
    };
  }

  function toChooserArg(str, text, chunk, context) {
    var CHOOSER_NUMERIC_PREFIX = '_',
      CHOOSER_GREATER_THAN_PREFIX = '_gt_',
      lazyText = function() {
        // Only call tap on the actual value the chooser selects
        return dust.helpers.tap(text, chunk, context);
      };

    if (chooser.isValidCategory(str)) {
      return {
        category: str,
        comparison: 'eq',
        text: lazyText
      };
    } else if (str.startsWith(CHOOSER_GREATER_THAN_PREFIX)) {
      return {
        arg: +str.substring(CHOOSER_GREATER_THAN_PREFIX.length),
        comparison: 'gt',
        text: lazyText
      };
    } else if (str.startsWith(CHOOSER_NUMERIC_PREFIX)) {
      return {
        arg: +str.substring(CHOOSER_NUMERIC_PREFIX.length),
        comparison: 'gte',
        text: lazyText
      };
    } else {
      return dust.log('@choice helper called with invalid chooser key: ' + str, 'ERROR');
    }
  }

  function chooseNumeric(chooserParams, chunk, context) {
    if (typeof chooser === 'undefined') {
      if (typeof t8.Chooser !== 'undefined') {
        chooser = new t8.Chooser();
      } else {
        return dust.log('@choice helper could not create an instance of t8.Chooser', 'ERROR');
      }
    }

    var chosen,
      rules = [];

    for (var chooserCategory in chooserParams.categories) {
      rules.push(toChooserArg(chooserCategory, chooserParams.categories[chooserCategory], chunk, context));
    }

    chosen = chooser.format(chooserParams.key, rules, chooserParams.locale);
    if (typeof chosen === 'undefined') {
      // Fallback to the first rule
      chosen = rules[0].text();
    }
    return chosen;
  }

  function chooseString(chooserParams, chunk, context) {
    var chosen,
      CHOOSER_DEFAULT = 'default';

    if( typeof chooserParams.categories[chooserParams.key] !== 'undefined') {
      chosen = dust.helpers.tap(chooserParams.categories[chooserParams.key], chunk, context);
    } else if ( chooserParams.isStringComparison && typeof chooserParams.categories[CHOOSER_DEFAULT] !== 'undefined') {
      //none of the categories matched, use it here if it was provided
      chosen = dust.helpers.tap(chooserParams.categories[CHOOSER_DEFAULT], chunk, context);
    }
    return chosen;
  }

  //end @choice helper functions

  function getLocale(params) {
    //use locale provided as a param
    if (params && params.locale) {
      return params.locale;
    //if no local is provided as param use local from LI.i18n.getLocale
    } else if (typeof LI !== 'undefined' && typeof LI.i18n !== 'undefined' && typeof LI.i18n.getLocale !== 'undefined') {
      return LI.i18n.getLocale().value;
    }
    return 'en_US';
  }

  function applyFilters(value, chunk, context, params) {
    var filters = '',
      //multiproduct is using attribute 'filters' vs network using attribute 'filter'
      rawFilters = params.filter || params.filters,
      ignoreDefaultFilter = params.ignoreDefaultFilter;

    if (typeof rawFilters !== 'undefined') {
      filters = dust.helpers.tap(rawFilters, chunk, context).split('|');
    }

    if (ignoreDefaultFilter) {
      return value;
    } else {
      return dust.filter(value, 'h', filters);
    }
  }

  var helpers = {
    /**
     * Return translated text for the specified key in the specified template.
     * Note: template is optional, if given overrides the name of the current template rendered
     * Example:
     * <p>{@translate key="hello_world"}Hello World{/i18n}</p>
     * <p>{@translate key="hello_world" text="Hello World"/}</p>
     * Output:
     * <p>Hello World</p>
     * <p>{@translate key="close" template="foo/global"/}</p>
     * Output: if the foo/global template existed
     *  <p>Hello World</p>
     * <p>{@translate key="close" hide="true"/}</p>
     * Output: no output because of hide true
     * <p>{@translate key="close" output="json"/}</p>
     * Output: no output because of output json and can be referenced later with {close|s}
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     *      <p>template="foo/global", lookup template cache</p>
     *      <p>hide="true", does not render the i18n in place, stores it in the given template cache</p>
     *      </p>output="json" , stores the value in the current template cache</p>
     */
    'translate': function(chunk, context, bodies, params) {

      if (typeof params === 'undefined' || typeof params.key === 'undefined') {
        return chunk.setError('@translate helper called with null or undefined "key" attribute');
      }

      var hide = params.hide ? dust.helpers.tap(params.hide, chunk, context) : null;
      if (hide === 'true') {
        // do not render
        // this @translate string will be added to i18n cache with given key, but not output here
        // it can be later output with {@translate key='somekey'/}
        return chunk;
      }

      if (typeof t8.Resources !== 'undefined') {
        dustVars.i18n.resources = new t8.Resources(dust.i18n, dust);
      } else {
        return chunk.setError('Can not create an instance of i18n.Resources. i18n.Resources is undefined');
      }

      function outputValue(key, out, myChunk) {

        if (!context.stack.head) {
          context.stack.head = {};
        }

        //output to chunk
        if (params.output === 'json') {
          context.stack.head[key] = out;
          myChunk.end('');
        } else {
          myChunk.end(applyFilters(out, myChunk, context, params));
        }
      }

      function useFallbackValue(key, myChunk) {
        if (typeof bodies !== 'undefined' && typeof bodies.block !== 'undefined') {
          myChunk.capture(bodies.block, context, function(out, captureChunk) {
            outputValue(key, out, myChunk);
            captureChunk.end('');
          }).end();
        } else {
          var output;
           if (typeof params.text !== 'undefined') {
             output = params.text;
           } else {
            output =  key;
           }
          outputValue(key, output, myChunk);
        }
      }

      var key = dust.helpers.tap(params.key, chunk, context),
        template = (typeof context.getTemplateName === 'function') ? context.getTemplateName() : context.global.__template_name__;

      //by default use context.getTemplateName(). It can be overwritten with attributes "templateName" or "template"
      //multiproduct @translate helper is using attribute 'templateName' vs network is using attribute 'template'
      if (params.template) {
        template = dust.helpers.tap(params.template, chunk, context);
      } else if (params.templateName) {
        template = dust.helpers.tap(params.templateName, chunk, context);
      }

      return chunk.map(function(chunk) {
        var i18nContext,
          excludeParams= ['key', 'template'],
          contextOverrides = {};

        //support overriding context with params on @i18n helper
        for (var param in params) {
          if(excludeParams.indexOf(param) < 0) {
            contextOverrides[param] = params[param];
          }
        }
        i18nContext = context.push(contextOverrides);

        if (typeof template !== 'undefined') {
          dustVars.i18n.resources.get(key, template, i18nContext, function(err, out) {
            if (err) {
              dust.log(err);
              useFallbackValue(key, chunk);
            } else {
              outputValue(key, out, chunk);
            }
          });
        } else {
          dust.log('@translate helper can not determine templateName');
          useFallbackValue(key, chunk);
        }
        chunk.end('');
      });
    },

    /**
     * Returns formatted value according to requested format
     * Note: Locale by default will be set to value in LI.i18n.getLocale. You can optionally override the default locale by
     * providing locale param to the helper
     *
     * ============================
     * NAME
     * ============================
     * Example:
     * <p>{@format key="invitee_full_Name" type="name.full" firstName="Kunal" lastName="Cholera" maidenName="Mukesh"/}</p>
     * Output (for en_US locale):
     * <p>Kunal (Mukesh) Cholera</p>
     *
     * <p>{@format key="invitee_full_Name" type="name.full" firstName="Kunal" lastName="Cholera" maidenName="Mukesh" locale="cs_CZ"/}</p>
     * Output (for cs_CZ locale):
     * <p>Kunal Cholera (roz. Mukesh)</p>
     *
     *{@format key="inviter_familiar_Name" type="name.familiar" firstName="Kunal" lastName="Cholera" maidenName="Mukesh" output="json"/}
     *<p>{inviter_familiar_Name|s}</p>
     * Output (for en_US locale):
     * <p>Kunal</p>
     *
     *
     * ============================
     * DATE
     * ============================
     * <p>{@format key="date_started_mdy_long" type="date.mdy.long" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>December 25 2012</p>
     *
     * <p>{@format key="date_started_md_medium" type="date.md.medium" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>Dec 25</p>
     *
     * <p>{@format key="date_started_time_short" type="date.time.short" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>6:45 AM</p>
     *
     *
     *
     *
     * Example:
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'format': function(chunk, context, bodies, params) {
      if (!params || !params.type) {
        return chunk.setError('@format helper called with null or undefined "format" attribute');
      }

      var format = dust.helpers.tap(params.type, chunk, context),
        key = dust.helpers.tap(params.key, chunk, context),
        type = format.split('.')[0],
        output = '',
        locale = getLocale(params);

      //get formatted value if that type of formatter exists
      if (typeof formatHelpers[type] !== 'undefined') {
        try {
          output = formatHelpers[type](format, locale, params, chunk, context);
        } catch(err){
          dust.log('@format failed to format value. ' + err.message, 'ERROR');
        }
      }

      //add the resulting formatted text into context if key is provided
      if (typeof key !== 'undefined') {
        context.stack.head[key] = output;
      }

      //if output is json do not write into output html
      if (params.output && params.output === 'json') {
        return chunk;
      } else {
        if (type === 'name' &&                                        // name that
            (format.indexOf('micro') !== -1 ||                        // is a microformat
            (typeof params.lastNameWithHighlight !== 'undefined'))) { // or has a lastName with html
          params.ignoreDefaultFilter = true;
        }

        return chunk.write(applyFilters(output, chunk, context, params));
      }
    },

    /**
     * Selects the proper singular/plural text to use depending on the value of some number and the rules
     * for the current locale.
     *
     * There are three types of comparison that this helper does: numeric, string and boolean. Numeric comparison is the
     * default type. Type of the comparison can be specified via type attribute. For example type="number", type="string"
     * or type="boolean".
     *
     * There are two patterns for using helper with numeric comparison. The first pattern is to use plural categories:
     *
     * You have {@choice key=count singular="{count} connections" dual="{count} connection" plural="{count} connections"/}.
     *
     * There are rules defined for each language that match a given number to a category like "singular" or "plural". For
     * English rules, if the count was 0, you'd get "0 connections" (English has an "endsWith" rule that classifies 0 as
     * "plural"); if the count was 1, you'd get "1 connection" (English has an "equals" rule that classifies 1 as
     * "singular").
     *
     *
     * The second pattern is to use numbers:
     *
     * You have {@choice key=count _0="{count} connections" _1="{count} connection" _2="{count} connections"/}.
     *
     * The helper above picks the right text to display based on the value of the variable count. It will pick the
     * largest param that is less than or equal to count (use _gt_<num> for strictly less than). In the example above, if
     * count was 0, you'd get "0 connections" (_0 is the closest match); if count was 50, you'd get "50 connections"
     * (_2 is the closest match).
     *
     * Note that if you combine first two patterns, the numbers act as exact matches:
     *
     * You have {@choice key=count _0="no connections" _gt_500="many connections" singular="{count} connections" dual="{count} connection" plural="{count} connections"/}.
     *
     * In the example above, the "no connections" text will *only* show up for a count of zero; everything between 0 and
     * 500 will be handled by the singular/dual/plural rules until you get to > 500, at which point the
     * "many connections" rule takes over.
     *
     *
     * String and boolean type of comparison behave the same. String comparison also supports default text, which boolean
     * does not. For both of these cases the value in the key will be exactly matched to the category name.
     *
     * Your {@choice type="string" key=type job_seeker="Job Seeker" recruiter="Recruiter" default="LinkedIn Premium"/} subscription allows you to reach people out of your network.
     *
     * Here "Job Seeker" will show when type equals "job_seeker" and "Recruiter" will be shown when type equals "recruiter".
     * "LinkedIn Premium" text will be selected when value in key parameter is neither job_seeker no recruiter.
     *
     * Here is an example of boolean comparison:
     *
     *  Your subscription {@choice type="boolean" key=hasInMails true="has" false="does not have"} InMail credits.
     *
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'choice': function(chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('key')) {
        return chunk.setError('@choice helper called without required parameter "key"');
      } else if(typeof(params.key) === 'undefined') {
        dust.log('@choice helper called with undefined key', 'WARN');
        return chunk.write('');
      }

      var chosen = '',
        chooserParams = getChooserParams(chunk, context, bodies, params);

      if (!chooserParams.hasCategories) {
        return chunk.setError('@choice helper called with no patterns to choose from');
      }

      if (chooserParams.isBooleanComparison || chooserParams.isStringComparison) {
        //if key is not a number pick category by exact key match
        chosen = chooseString(chooserParams, chunk, context);
      } else if (chooserParams.isNumericComparison) {
        //if key is a number pick category by range match
        chosen = chooseNumeric(chooserParams, chunk, context);
      }

      return chunk.write(applyFilters(chosen, chunk, context, params));
    },

    /**
     * A helper to render text as possessive for the current locale.
     *
     * Example:
     *
     * See all of {@possessive key=name/} connections.
     *
     * Output:
     *
     * See all of Jim's connections.
     *
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'possessive': function (chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('key')) {
        return chunk.setError('@possessive helper called without required parameter "key"');
      } else if(typeof(params.key) === 'undefined') {
        dust.log('@possessive helper called with undefined key', 'WARN');
        return chunk.write('');
      }

      var key = dust.helpers.tap(params.key, chunk, context),
        possessive = new t8.Possessive(),
        locale = getLocale(params),
        asPossessive = possessive.format(key, locale);

      return chunk.write(applyFilters(asPossessive, chunk, context, params));
    },

    /**
     * A helper to inject the dir attribute. If
     * the given text is considered right-to-left
     * "dir='rtl'" is rendered, otherwise "dir='ltr'"
     * is rendered.
     *
     * Example:
     *
     * <div {@dirAttr text=text}>{text}</div>
     *
     * Output:
     *
     * <div dir='rtl'>the text</div>
     * <div dir='ltr'>the text</div>
     */
    'dirAttr': function (chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('text')) {
        return chunk.setError('@dirAttr helper called without required parameter "text"');
      }
      return chunk.write('dir="' + (rtl.isRtl(dust.helpers.tap(params.text, chunk, context))? 'rtl' : 'ltr') + '"');
    },

    /**
     * A helper to conditionally render content.
     * If the given text is considered right-to-left
     * render if block, otherwise render else block.
     * The method will log a message if no body block
     * is provided.
     *
     * Example:
     *
     * {@isRtl text=ugc}yes{:else}no{/isRtl}</div>
     * {@isRtl text=ugc}foo{/isRtl}</div>
     *
     * Output:
     *
     * yes
     * no
     */
    'isRtl': function (chunk, context, bodies, params) {
      var body = bodies.block,
          skip = bodies['else'];

      if (!params || !params.hasOwnProperty('text')) {
        return chunk.setError('@isRtl helper called without required parameter "text"');
      }

      if(rtl.isRtl(dust.helpers.tap(params.text, chunk, context))) {
        if (body) {
          chunk.render(body, context);
        } else {
          dust.log( 'Missing body block in the isRtl helper!', 'INFO' );
        }
      } else {
        if (skip) {
          chunk.render(skip, context);
        }
      }

      return chunk;
    }
  };

  var key;

  for (key in dustVars) {
    dust[key] = dustVars[key];
  }

  for (key in helpers) {
    dust.helpers[key] = helpers[key];
  }
}));


// Monkey patch Dust.log
(function (root, dust) {
  var oldDustLog;
  if (dust.log) {
    oldDustLog = dust.log;
    dust.log = function dustErrorReportToJet(message, type) {
      try {
        if (root.jet && (type === 'ERROR' || type === 'WARN')) {
          if (message instanceof Error) {
            jet.error(message);
          } else if (typeof message === 'string') {
            try {
              throw new Error(message);
            } catch (err) {
              jet.error(err);
            }
          }
        }
      } finally {
        return oldDustLog.apply(dust, arguments);
      }
    };
  } else if (root.jet) {
    jet.error(new Error("The function dust.log doesn't exist in this version."));
  }
}(this, dust));

(function(root) {
  "use strict";
  root.play = root.play || {};
  root.sc = root.sc || {};
  sc.hashes = sc.hashes || {};
})(this);

/**
 * Utility JavaScript functions that are not related to dust, Play, or LinkedIn. Many of these exist in jQuery or
 * underscore, but we don't want the Play code to bring in these dependencies and have to deal with all sorts of
 * conflicts.
 */
(function (play, root) {

  "use strict";

  play.EVENTS = {};
  play.EVENTS.DUST_READY = 'playDustReady';

  play.Utils = {};

  var Utils = play.Utils;

  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // continue to polyfill startsWith and endsWith for play-urls and play apps
  // it was removed https://gitli.corp.linkedin.com/formatters/formatters-js/commit/786795508bf4af26b0a010fa78bd06686bc12328

  /* IE doesn't have startsWith for String: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/startsWith */
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function (searchString, position) {
      position = position || 0;
      return this.indexOf(searchString, position) === position;
    };
  }
  /**
   * Add endsWith to Strings: http://stackoverflow.com/questions/280634/endswith-in-javascript
   */
  if (!String.prototype.endsWith) {
    String.prototype.endsWith = function (suffix) {
      return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
  }
  // The ECMAScript 5 native function implementation
  // of filter
  var ArrayProto = Array.prototype,
      ObjProto = Object.prototype,
      nativeFilter = ArrayProto.filter,
      nativeForEach = ArrayProto.forEach,
      nativeMap = ArrayProto.map,
      nativeSome = ArrayProto.some,
      toString = ObjProto.toString,
      hasOwnProperty = ObjProto.hasOwnProperty,
      playEventsHash = {};

  /**
   * Shortcut function for checking if an object has a given property directly on itself (in other words, not on a prototype).
   * @param obj
   * @param key
   */
  Utils.has = function (obj, key) {
    return hasOwnProperty.call(obj, key);
  };

  /**
   * Retrieve all the names of the object's properties.
   * @param obj
   */
  Utils.keys = function (obj) {
    if (obj !== Object(obj)) {
      throw new TypeError('Invalid object');
    }
    var keys = [];
    for (var key in obj) {
      if (Utils.has(obj, key)) {
        keys.push(key);
      }
    }
    return keys;
  };

  /**
   * Produces a new array of values by executing a function on each value in list and
   * returning the value if the function returns true.
   * If the native filter method exists, it will be used instead. If list is a JavaScript object, iterator's arguments will be (value, key, list).
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.filter = function (obj, iterator, context) {
    var results = [];
    if (obj == null) {
      return results;
    }
    if (nativeFilter && obj.filter === nativeFilter) {
      return obj.filter(iterator, context);
    }
    Utils.each(obj, function (value, index, list) {
      if (iterator.call(context, value, index, list)) {
        results.push(value);
      }
    });
    return results;
  };

  /**
   * Assert that condition is true and throw an Error with message if it's not.
   * @param condition
   * @param message
   */
  Utils.assert = function (condition, message) {
    if (!condition) {
      throw new Error(message);
    }
  };

  /**
   * Iterates over a list of elements, yielding each in turn to an iterator function.
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.each = function (obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, length = obj.length; i < length; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      var keys = Utils.keys(obj);
      for (var i = 0, length = keys.length; i < length; i++) {
        if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
      }
    }
  };

  /**
   * Returns the same value that is used as the argument. In math: f(x) = x
   * @param value
   */
  Utils.identity = function (value) {
    return value;
  };

  /**
   * Returns true if any of the values in the list pass the iterator truth test.
   * Short-circuits and stops traversing the list if a true element is found.
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.any = Utils.some = function (obj, iterator, context) {
    iterator || (iterator = Utils.identity);
    var result = false;
    if (obj == null) return result;
    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
    Utils.each(obj, function (value, index, list) {
      if (result || (result = iterator.call(context, value, index, list))) return breaker;
    });
    return !!result;
  };

  /**
   * Returns true if value is undefined.
   * @param obj
   * @returns {boolean}
   */
  Utils.isUndefined = function (obj) {
    return obj === void 0;
  };

  /**
   * Check that obj is not undefined/null
   * @param obj
   * @returns {boolean}
   */
  Utils.isDefined = function (obj) {
    return !Utils.isUndefined(obj) && !Utils.isNull(obj);
  };

  /**
   * Returns true if the value of object is null.
   * @param obj
   */
  Utils.isNull = function (obj) {
    return obj === null;
  };

  /**
   * Assert that the given value is not undefined/null and throw an Error with message if it is
   * @param value
   * @param message
   */
  Utils.assertDefined = function (value, message) {
    Utils.assert(Utils.isDefined(value), message);
  };

  /**
   * Produces a new array of values by mapping each value in list through a transformation function (iterator).
   * If the native map method exists, it will be used instead. If list is a JavaScript object, iterator's arguments will be (value, key, list).
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.map = function (obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
    Utils.each(obj, function (value, index, list) {
      results.push(iterator.call(context, value, index, list));
    });
    return results;
  };

  /**
   * Copy all of the properties in the source objects over to the destination object, and return the destination object.
   * It's in-order, so the last source will override properties of the same name in previous arguments.
   * @param obj
   */
  Utils.extend = function (obj) {
    Utils.each(ArrayProto.slice.call(arguments, 1), function (source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  /**
   * Returns true if object is an Array.
   * @param object
   */
  Utils.isArray = function (obj) {
    return toString.call(obj) == '[object Array]';
  };

  /**
   * IE doesn't have indexOf for Array:  https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf
   *
   */
  if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function (searchElement /*, fromIndex */) {
      if (this === null) {
        throw new TypeError();
      }
      var t = Object(this);
      var len = t.length >>> 0;
      if (len === 0) {
        return -1;
      }
      var n = 0;
      if (arguments.length > 1) {
        n = Number(arguments[1]);
        if (n !== n) { // shortcut for verifying if it's NaN
          n = 0;
        } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
          n = (n > 0 || -1) * Math.floor(Math.abs(n));
        }
      }
      if (n >= len) {
        return -1;
      }
      var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
      for (; k < len; k++) {
        if (k in t && t[k] === searchElement) {
          return k;
        }
      }
      return -1;
    };
  }

  /**
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
   */
  if (!Array.isArray) {
    Array.isArray = function (vArg) {
      return Object.prototype.toString.call(vArg) === "[object Array]";
    };
  }

  /**
   * Download and execute the script at the given url. Fire callback when done.
   *
   * @param url
   * @param callback
   */
  play.getScript = function (url, callback) {
    Utils.assert(url, "getScript called with null url");

    var script = document.createElement("script");
    script.src = url;
    play.executeScript(script, callback);
  };

  /**
   * Execute the JavaScript in script, which should be a DOM script node. Fire callback when done.
   *
   * Stolen from: https://github.com/jquery/jquery/blob/master/src/ajax/script.js
   *
   * @param script
   * @param callback
   */
  play.executeScript = function (script, callback) {
    Utils.assert(play.isClient, "executeScript should only be used for client-side rendering!");
    Utils.assert(script, "executeScript called with null script");

    var head = document.head || document.getElementsByTagName("head")[0] || document.documentElement;

    script.async = "async";

    var cleanup = function () {
      if (head && script.parentNode) {
        head.removeChild(script);
      }
      script = undefined;
    };

    if (script.src) {
      script.onload = script.onreadystatechange = function (_, isAbort) {
        if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
          script.onload = script.onreadystatechange = null;
          cleanup();
          if (!isAbort && callback) {
            callback();
          }
        }
      };
    }

    /* We have to keep a reference to the inserted HTMLScriptElement from insertBefore return value as
     IE may trigger the onload/onreadystatechange callback before returning control if the script source
     file is already cached by the browser, and our cleanup function declared above would render the
     script variable undefined. */
    script = head.insertBefore(script, head.firstChild);
    if (typeof(script) !== 'undefined' && !script.src) {
      cleanup();
      if (callback) {
        callback();
      }
    }
  };

  /**
   * Given a path like "A.B.C", safely traverse obj and return the value obj["A"]["B"]["C"]. If at any point an
   * intermediate value is null (e.g. obj["A"]["B"] is null before getting to ["C"]), return null, unless required is
   * set to true, in which case, throw an exception.
   *
   * @param obj
   * @param path
   * @param required
   * @return {*}
   */
  play.traverseObject = function (obj, path, required) {
    Utils.assert(path, "traverseObject called with null path");
    Utils.assert(obj || !required, "traverseObject called with a null object, but required is set to true");

    var parts = path.split(".");

    for (var i = 0; i < parts.length; i++) {
      obj = obj ? obj[parts[i]] : obj;
      if (!obj) {
        Utils.assert(!required, "traverseObject could not find required path " + path);
        return null;
      }
    }

    return obj;
  };

  /**
   * Utility function to print a log. Can be used in both normal browsers and Rhino envrionment. Noop if not in either
   * environment.
   *
   * @param msg
   */
  play.log = function (msg) {
    if (root.console && root.console.log) {
      // Normal browsers
      root.console.log(msg);
    } else if (root.java && root.java.lang && root.java.lang.System && root.java.lang.System.out && root.java.lang.System.out.println) {
      // Rhino: http://stackoverflow.com/questions/12399462/rhino-print-function
      root.java.lang.System.out.println(msg);
    }
  };

  /**
   * Utility function for creating dust template alias.  This function is primarily used by DustPlugin when performing
   * client side rendering
   *
   * @param templatePath
   * @param alias
   */
  play.templateAlias = function (templatePath, alias) {
    if (dust && dust.cache) {
      if (templatePath in dust.cache) {
        dust.cache[alias] = dust.cache[templatePath];
      } else {
        dust.log("Unable to find template '" + templatePath + '" to create alias "' + alias + "'", 'ERROR');
      }
    } else {
      play.log("Unable to find dust or dust.cache.  Please ensure dust js is included in your base page.")
    }
  }

  /**
   * a namespace for utility functions inspired by underscore/lodash
   */
  play._ = {};

  /**
   * A specialized verson of some for arrays without support for callback shorthands and `this` binding
   * @see lodash.js arraySome
   */
  play._.some = function (array, predicate) {
    var index = -1,
        length = array.length;

    while (++index < length) {
      if (predicate(array[index], index, array)) {
        return true;
      }
    }
    return false;
  };

  /**
   * a simplified underscore.omit where we just support a flat object and a flat array
   */
  play._.omit = function (obj, arrayKeysToOmit) {
    var result = {};
    // loop through the object and copy every key/value that isn't being omitted
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop) && arrayKeysToOmit.indexOf(prop) === -1) {
        result[prop] = obj[prop];
      }
    }

    return result;
  };

  /**
   * A specialized verson of reduce for arrays without support for callbacks and this binding
   * @see lodash.js arrayReduce
   */
  play._.reduce = function (array, iteraree, accumulator, initFromArray) {
    var index = -1,
        length = array.length;

    if (initFromArray && length) {
      accumulator = array[++index];
    }
    while (++index < length) {
      accumulator = iteraree(accumulator, array[index], index, array);
    }
    return accumulator;
  };

  /**
   * copied from underscore
   */
  play._.result = function (object, property) {
    if (object === null) {
      return null;
    }

    var value = object[property];
    return typeof value === 'function' ? value.call(object) : value;
  };

  /**
   * Allows subscribing to events generated by Play
   * @param event
   * @param callback
   */
  play.on = function (event, callback) {
    if (typeof playEventsHash[event] === 'undefined') {
      playEventsHash[event] = [];
    }
    playEventsHash[event].push(callback);
  };

  /**
   * Allows unsubscribing from events generated by Play
   * @param event
   * @param callback
   */
  play.off = function (event, callback) {
    var callbacks = playEventsHash[event];

    if (typeof callbacks === 'undefined') {
      return;
    }

    for (var i = 0, len = callbacks.length; i < len; i++) {
      if (callbacks[i] === callback) {
        callbacks.splice(i, 1);
        break;
      }
    }
  };

  /**
   * Triggers Play events
   * @param event
   * @param params
   */
  play.trigger = function (event, params) {
    if (typeof playEventsHash[event] !== 'undefined') {
      for (var i = 0, len = playEventsHash[event].length; i < len; i++) {
        playEventsHash[event][i].apply(this, params);
      }
    }
  }

})(play, this);


/**
 * This file is copied from util-i18n-js/src/dust/html-utils.js
 *
 * Utility functions for creating HTML markup
 */
(function(play, LI, dust) {

  "use strict";

  var HtmlUtils = LI.HtmlUtils = {};
  var Utils = play.Utils;

  /**
   * Convenient wrapper extracts filter options from a params object
   * and uses dust.filter() to filter a string.
   *
   * Example:
   *
   * dustFilter(str, 's|j')
   *
   * Returns:
   *
   * str with HTML unescaped and Javascript escaped
   *
   * @param str string to filter
   * @param params
   * @return {String}
   */
  HtmlUtils.dustFilter = function(str, params) {
    var filters = [];

    if (params && params.filters) {
      filters = Utils.map(params.filters.split('|'), function(elem) { return elem.trim(); });
    }

    return dust.filter(str, 'h', filters);
  };

  /**
   * Create an HTML tag with the given name, attributes, and body. Properly handles escaping characters for display as
   * HTML. Correctly handles closing tags, including scripts. Note: this function does NOT HTML escape the (optional)
   * body parameter. As the body may contain arbitrary markup, you must ensure that your own code handles escaping for
   * it.
   *
   * Examples:
   *
   * createHtmlTag("div", {id: "foo"}, "bar")
   *
   * Returns: <div id="foo">bar</div>
   *
   * createHtmlTag("script", {src: "foo.js"})
   *
   * Returns: <script src="foo.js"></script>
   *
   * @param tagName
   * @param attributes
   * @param body
   * @return {String}
   */
  HtmlUtils.createHtmlTag = function(tagName, attributes, body) {
    Utils.assert(tagName, "createHtmlTag called with null or undefined tagName");

    var tagNameEscaped = dust.escapeHtml(tagName);
    var out = "<" + tagNameEscaped;

    if (attributes) {
      var htmlAttributes = HtmlUtils.objectToHtmlAttributes(attributes);
      if (htmlAttributes && htmlAttributes.length > 0) {
        out += " " + htmlAttributes;
      }
    }

    var closedTag = "</" + tagNameEscaped + ">";

    if (body) {
      out += ">" + body + closedTag;
    } else if (tagNameEscaped === "script") {
      out += ">" + closedTag;
    } else {
      out += "/>";
    }

    return out;
  };

  /**
   * Converts the key/value pairs in obj to key/value pairs for HTML attributes. Handles escaping correctly.
   *
   * Example:
   *
   * objectToHtmlAttributes({id: "foo", class: "bar"})
   *
   * Returns: id="foo" class="bar"
   *
   * @param obj
   * @return {*}
   */
  HtmlUtils.objectToHtmlAttributes = function(obj) {
    var attrs = [];
    Utils.map(obj, function(value, key) {
      if (Utils.isDefined(key) && Utils.isDefined(value)) {
        attrs.push(dust.escapeHtml(key) + '="' + dust.escapeHtml(value) + '"');
      }
    });

    return attrs.join(' ');
  };

})(play, LI, dust);


/**
 * Helpers that extend dust functionality in a non Play/LinkedIn manner. In fact, after some testing in Play apps, if
 * these prove to be generally useful helpers, they should be ported into the dust or dust-helpers open source
 * repositories.
 */
(function(play, dust) {

  "use strict";

  var Utils = play.Utils;

  /**
   * True iff this script is executing in a browser environment.
   *
   * TODO: USSR should set a flag instead of this hacky check
   *
   * @type {Boolean}
   */
  play.isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
  play.isDustReady = false;


  /**
   * Returns the current dust debug level.
   *
   * @returns {*}
   */
  play.getDustDebugLevel = function() {
    return play.getPageContextValue("dustDebug", false);
  };

  var resourcesInProgress = {};
  /**
   * Support lazy-loading of dust templates and partials. Dust will call dust.onLoad if you try to render a template
   * that is not in dust.cache. This method fetches it asynchronously from play.templateUrl.
   *
   * NOTE: We skip invocation of this when javadust is defined in order to keep the existing dust.onLoad behaviour of throwing an error.
   * This is to prevent errors during asynchronous operations (such as nextTick() calls) being lost when dust.onLoad is overwritten.
   *
   * @param name
   * @param callback
   */
  if (typeof javadust !== undefined) {
    dust.onLoad = function(name, callback) {
      Utils.assert(play.isClient, "Could not find template " + name + ". Lazy loading for dust templates is only available for client-side rendering!");

      var callbacks = resourcesInProgress[name];
      if (callbacks) { // someone is already fetching this template in the background, add myself as a callback
        callbacks.push(callback);
      } else {
        resourcesInProgress[name] = [callback];
        play.getScript(play.templateUrl(name), function() {
          // notify everyone who was waiting on this template that it's ready
          var toNotify = resourcesInProgress[name];
          while (toNotify && toNotify.length > 0) {
            var cb = toNotify.pop();
            cb();
          }
        });
      }
    };
  }

  /**
   * Tap all values from given dust params object. Returns an object with the same key and tapped values.
   *
   * @param params
   * @param chunk
   * @param context
   * @returns {{}}
   */
  dust.helpers.tapAll = function(params, chunk, context) {
    Utils.assert(params, "tapAll called with null params");

    var tappedParams = {};
    Utils.each(params, function(value, key) {
      tappedParams[key] = dust.helpers.tap(value, chunk, context);
    });

    return tappedParams;
  };

  /**
   * Add a variable to the current context with a key equal to the 'name' parameter and value equal to the body of this
   * tag.
   *
   * Example:
   *
   * {@addToContext name="myVariable"}This text is now available in the context{/addToContext}
   *
   * <p>Now I can use the new variable: {myVariable}</p>
   *
   * Output:
   *
   * <p>Now I can use the new variable: This text is now available in the context</p>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.addToContext = function(chunk, context, bodies, params) {
    Utils.assert(params.name, "@addToContext called with null params.name");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.capture(bodies.block, context, function(out, chunk) {
      context.current()[params.name] = out;
      return chunk.end('');
    });
  };


  /**
   * Writes the body of this tag iff this is the first iteration in a loop. This is the opposite of dust's built in
   * @sep helper.
   *
   * Example:
   *
   * people = ["Jim", "Dean", "Kunal"]
   *
   * {#people}
   *   <li>{.} {@first}is awesome!{/first}</li>
   * {/people}
   *
   * Output:
   *
   * <li>Jim is awesome!</li>
   * <li>Dean</li>
   * <li>Kunal</li>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @return {*}
   */
  dust.helpers.first = function(chunk, context, bodies) {
    if (context.stack.index === 0) {
      return bodies.block(chunk, context);
    } else {
      return chunk.write('');
    }
  };


  /**
   * Stubs out the @pre.i18n.translate helper (only used in network) in play (causes an
   *   "Invalid handler" error in JET)
   *
   * @param  chunk
   * @return {*}
   */
  dust.helpers['pre.i18n.translate'] = function(chunk) {
    return chunk;
  };

})(play, dust);

/**
 * Helpers for embedding JSON into HTML so it can be read client-side
 */
(function(play, LI, dust) {

  "use strict";

  var Utils = play.Utils;
  var HtmlUtils = LI.HtmlUtils;

  play.EMBEDDED_CONTEXT_ID = "__pageContext__";

  var ESC_FLAGS = "gi";

  var START_COMMENT = "<!--";
  var END_COMMENT = "-->";

  var HTML_ENTITY = {
    dsh: { escaped: '\\u002d\\u002d', unescaped: '--', escaped_re: '\\\\u002d\\\\u002d' },
    lt: { escaped: '\\u003c', unescaped: '<', escaped_re: '\\\\u003c' },
    gt: { escaped: '\\u003e', unescaped: '>', escaped_re: '\\\\u003e' }
  };

  /**
   * This will store Play context data for the current page: CDN URLs, locale to use, CSRF token, etc.
   *
   * This value is lazy loaded in one of two ways:
   *
   * 1. When rendering in USSR, this data will be part of the JSON payload and initialized in a dust base page using the
   *    @initContext helper.
   *
   * 2. When rendering in the browser, this will be read from the DOM.
   *
   * This value is now "private" to dust-utils. To access it, you should always use play.getPageContext() and
   * play.getPageContextValue(key).
   *
   * @type {*}
   */
  play.pageContext = null;

  /**
   * Get the Play context data for the current page, including the CDN URLs, locale to use, CSRF token, etc.
   *
   * @return {*}
   */
  play.getPageContext = function() {
    if (play.pageContext) {
      return play.pageContext;
    } else if (play.isClient) {
      play.setPageContext(play.getEmbeddedContent(play.EMBEDDED_CONTEXT_ID));
      return play.pageContext;
    } else {
      throw "The pageContext is null. Did you call the @initContext helper in the body of your dust base page?";
    }
  };

  /**
   * Returns true if the current page has page context, false otherwise.
   * @returns {boolean}
   */
  play.hasPageContext = function() {
    try {
      play.getPageContext();
      return true;
    } catch (err) {
      return false;
    }
  };

  /**
   * Set the pageContext to a custom value. In general, this should only be used by dust-utils and its tests.
   *
   * @param context
   */
  play.setPageContext = function(context) {
    Utils.assert(context, "setPageContext called with a null context");
    play.pageContext = context;
  };

  /*
   * Removes the pageContext and cleans up any side-effects of setting it.
   */
  play.removePageContext = function() {
    play.pageContext = null;
  };

  /**
   * Read a value from the Play context data for the current page. If required is set to true, this method will throw
   * an exception if the value is null or undefined.
   *
   * Example:
   *
   * var cdnUrl = play.getPageContextValue("cdnUrl")
   *
   * @param key
   * @param required
   * @return {*}
   */
  play.getPageContextValue = function(key, required) {
    var context = play.getPageContext();
    Utils.assert(context, "pageContext is null");

    var value = context[key];
    if (required) {
      Utils.assertDefined(value, 'The value for ' + key + ' in the pageContext was null or undefined');
    }
    return value;
  };

  /**
   * Escapes characters to their equivalent javascript unicode escape entities within the given string.
   * Does not escape other HTML entities such as the quote, since that's not necessary to make fizzy
   * work at this time. Currently, the only value is '-' which is '\\u002d'.
   *
   * See unescape comment below for more details.
   *
   * Stolen from fz.js
   *
   * @param str
   * @return {*}
   */
  play.escapeForEmbedding = function(str) {
    if (Utils.isDefined(str)) {
      return str.replace(new RegExp(HTML_ENTITY.dsh.unescaped, ESC_FLAGS), HTML_ENTITY.dsh.escaped)
                .replace(new RegExp(HTML_ENTITY.gt.unescaped, ESC_FLAGS), HTML_ENTITY.gt.escaped)
                .replace(new RegExp(HTML_ENTITY.lt.unescaped, ESC_FLAGS), HTML_ENTITY.lt.escaped);
    } else {
      return str;
    }
  };

  /**
   * Unescapes various special characters which fizzy escapes to prevent premature breakout of
   * surrounding tags. Currently unescapes the javascript string unicode escape sequence:
   * '\u002d\u002d' to '--' (dash). This is to protect against the appearance of --, which in some
   * browsers terminates an HTML comment block.
   *
   * This enables us to escape in play in a backwards and forwards compatible way, because old fizzy
   * client.js was expecting &dsh; (which won't appear anymore, so unescaping won't have any impact),
   * and the '\u002d\u002d' sequence, since it is only valid within a string for JSON, will naturally
   * unescape into '--'.
   *
   * Browsers that terminate comments on a single - are vulnerable.
   *
   * Stolen from fz.js
   *
   * @param str
   * @return {*}
   */
  play.unescapeForEmbedding = function (str) {
    if (Utils.isDefined(str)) {
      return str.replace(new RegExp(HTML_ENTITY.dsh.escaped_re, ESC_FLAGS), HTML_ENTITY.dsh.unescaped)
                .replace(new RegExp(HTML_ENTITY.gt.escaped_re, ESC_FLAGS), HTML_ENTITY.gt.unescaped)
                .replace(new RegExp(HTML_ENTITY.lt.escaped_re, ESC_FLAGS), HTML_ENTITY.lt.unescaped);
    } else {
      return str;
    }
  };

  /**
   * Creates an HTML tag with the given JSON embedded in a format that can be read safely on the client-side
   *
   * Example:
   *
   * embeddedJsonTag({foo: "bar"}, "my-tag")
   *
   * Output:
   *
   * <code id="my-tag" style="display: none;"><!--{"foo": "bar"}--></code>
   *
   * @param json
   * @param id
   * @return {String}
   */
  play.embeddedJsonTag = function(json, id) {
    Utils.assert(id, "embeddedJsonTag called with null id");

    var attrs = {id: id, style: "display: none;"};
    var body = play.wrapInComment(JSON.stringify(json));
    return HtmlUtils.createHtmlTag("code", attrs, body);
  };

  /**
   * Wrap a piece of text in HTML comments
   *
   * @param text
   * @return {String}
   */
  play.wrapInComment = function(text) {
    Utils.assertDefined(text, "wrapInComment called with null text");

    return START_COMMENT + play.escapeForEmbedding(text) + END_COMMENT;
  };

  /**
   * Read JSON content embedded in an HTML <code> tag. The play.embeddedJsonTag function and the @embedJSON helper can
   * safely store JSON in the body of an HTML document. This method can safely read it back out. This is useful for
   * exposing JSON assembled server-side to client-side JavaScript. Note that this method does not do any caching;
   * since JSON.parse can be expensive, you should locally cache the data yourself (e.g. in a backbone model) if you're
   * going to be accessing this data more than once for an id.
   *
   * This code is largely copied from fz.js: I'd prefer to use fs.payload, but it only works of fs.embed was called
   * previously, which does not apply to all embedded JSON use cases.
   *
   * @param id
   */
  play.getEmbeddedContent = function(id) {
    Utils.assert(id, "getEmbeddedContent called with null id");

    var contentElem = document.getElementById(id);
    Utils.assert(contentElem, "Could not find DOM node with id " + id);
    Utils.assert(contentElem.firstChild, "DOM node with id " + id + " did not have a child comment node");

    var innerContent = contentElem.firstChild.nodeValue;
    Utils.assert(innerContent !== null && innerContent.length > 0, "No inner contents found for DOM node with id " + id);

    contentElem.parentNode.removeChild(contentElem);
    return JSON.parse(innerContent);
  };

  /**
   * Include this in a base page to setup client-side rendering of a template. This is useful if your pages are simple,
   * or you already fetched all the JSON you need for the base page, or you don't want to incur an extra roundtrip from
   * Fizzy and setting up an extra endpoint for an embed.
   *
   * Supported parameters:
   *
   * 1. template: the template to render. Should be the name the template registers in dust.cache. Required.
   *
   * 2. data: the JSON payload that will be passed into the template during rendering. It will be embedded in the
   *          HTML as a <code> tag. Optional.
   *
   * 3. templateId: the id to use for the template script tag and the embedded JSON payload (the latter will have
   *                -content appended). Optional. Uses the template name if not provided. If you're calling @render
   *                multiple times for the same payload, make sure to give each one a unique id.
   *
   * 4. templateUrl: the URL for the template. Optional. Tries to guess URL using play.templateUrl if not provided.
   *
   * 5. skipTemplateUrl: if set to true, does not include a script tag to fetch the template, with the assumption that
   *                     the template is _already_ in dust.cache. Optional. Default is false.
   *
   * 6. containerId: the ID of the DOM element which the rendered template will be inserted into. Note, this DOM node
   *                 must already be in the DOM; during initial page load, this usually means the DOM comes before this
   *                 @render call. Optional. If no ID is given then the embed will be inserted just before the DOM
   *                 element with id templateId.
   *
   * Example:
   *
   * {@render template="/templates/home" data=contextData /}
   *
   * Output:
   *
   * <script src="/assets/templates/home.js"></script>
   * <code id="my-tag" style="display: none;"><!--{"foo": "bar"}--></code>
   * <script>fs.embed("templates/home", "templates/home");</script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.render = function(chunk, context, bodies, params) {
    Utils.assert(!play.isClient, "The @render helper is only used in a server-side rendered base page to setup client-side rendering. Perhaps you want fs.embed() instead?");
    Utils.assert(params.template, "@render called with null params.template");
    params = dust.helpers.tapAll(params, chunk, context);

    var template = params.template;
    var data = params.data || {};
    var templateId = params.templateId || template;
    var templateUrl = params.templateUrl || play.templateUrl(template);
    var skipTemplateUrl = params.skipTemplateUrl;
    var containerId = params.containerId;

    var scriptTagParams = {src: templateUrl, id: templateId};
    var fetchTemplateScriptTag = skipTemplateUrl ? "" : HtmlUtils.createHtmlTag("script", scriptTagParams);

    var embeddedJsonTag = play.embeddedJsonTag(data, templateId + "-content");

    var asJsString = function(str) {
      if (Utils.isDefined(str)) {
        return '"' + dust.escapeJs(str) + '"';
      } else {
        return "undefined";
      }
    };

    var fsEmbedParams = Utils.map([templateId, template, undefined, containerId], asJsString);

    var fsEmbedBody = 'fs.embed(' + fsEmbedParams.join(', ') + ');';
    var fsEmbedTag = HtmlUtils.createHtmlTag("script", {}, fsEmbedBody);

    return chunk.write(fetchTemplateScriptTag + embeddedJsonTag + fsEmbedTag);
  };

  /**
   * Embed the JSON specified in by the data parameter in a code tag with the id in the id parameter. This can be used
   * to take JSON assembled server-side and safely embed it in the HTML document. Use play.getEmbeddedContent to read
   * it back out later.
   *
   * Example: {@embedJSON id="my-data" data=someJsonInContext /}
   *
   * Output: <code id="my-data" style="display: none"><!--{"foo":"bar"}--></code>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.embedJSON = function(chunk, context, bodies, params) {
    Utils.assert(params.id, "@embedJSON called with null id");
    Utils.assert(params.data, "@embedJSON called with null data");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.embeddedJsonTag(params.data, params.id));
  };

  /**
   * Use this helper in your base pages to setup the page context for client-side rendering. Most helpers in dust-utils
   * will not work if this helper is not included.
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.initContext = function(chunk) {
    Utils.assert(!play.isClient, "The @initContext helper is only used in a server-side rendered base page to setup the page context for server and client-side rendering.");

    // Embed the page context JSON in the DOM so it is available to dust-utils for client-side rendering
    var embeddedJsonCodeTag = play.embeddedJsonTag(play.getPageContext(), play.EMBEDDED_CONTEXT_ID);
    
    dust.debugLevel = play.getDustDebugLevel();

    // Inject sc-hashes_xx_XX.js if useScHashesJs
    // TODO: Find a way to inject dust-utils.js and sc-hashes_xx_XX.js in a more performant manner (e.g. let users customize in their concat group)
    var scHashesScriptTag = play.useScHashesJs() ? HtmlUtils.createHtmlTag("script", {src: play.getPageContextValue("scHashesUrl", true)}) : '';

    // Inject dust-utils.js
    var dustUtilJSUrl = play.getPageContextValue("dustUtilsUrl", true),
      dustUtilScriptTag = HtmlUtils.createHtmlTag("script", {
        src: dustUtilJSUrl
      });

    // When JavaScript reverse routers are included in DustOptions, a URL to fetch them is included in the response to
    // Fizzy/USSR. The URL will return JavaScript with the JavaScript reverse routers so they are usable during server
    // side rendering. It also returns a play.jsRoutesString that we put into a script tag so the same routes are
    // available for client side rendering.
    var jsRoutesScriptTag = play.jsRoutesString ? HtmlUtils.createHtmlTag("script", {}, play.jsRoutesString.replace(/__NEW_LINE__/g, "\n")) : '';

    return chunk.write(embeddedJsonCodeTag + scHashesScriptTag + dustUtilScriptTag + jsRoutesScriptTag);
  };

  // When rendering server-side, hijack (monkey patch) the dust.render method so we can access the page context
  if (!play.isClient && !play.contextReady) {

    // Prevent the context from being initialized multiple times
    play.contextReady = true;

    var originalRenderFunction = dust.render;

    dust.render = function(name, context, callback) {
      var pageContext = context[play.EMBEDDED_CONTEXT_ID];
      Utils.assert(pageContext, "No page context found!");

      play.setPageContext(pageContext);
      dust.debugLevel = play.getDustDebugLevel();

      dust.render = originalRenderFunction;
      originalRenderFunction(name, context, callback);
    };
  }

})(play, LI, dust);


/**
 * Helpers for rendering forms in dust. Experimental.
 */
(function(play, LI, dust) {

  "use strict";

  var Utils = play.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Writes this tag's body surrounded by form tags. Includes a hidden CSRF input in the form. You can specify an
   * 'alias' parameter and corresponding arg0, arg1, etc. parameters to fill in the form's action attribute with a URL.
   * You can also specify a 'formData' parameter to provide "form data" for the @input helper.
   *
   * Any parameter other than 'alias' and 'formData' will be passed unchanged to the form tag.
   *
   * Example:
   *
   * {@form alias="controllers.Forms.submit" id="myForm"}
   *   <input type="text" name="name"/>
   *   <input type="submit"/>
   * {/form}
   *
   * Output:
   *
   * <form action="/dust-sample/submit" method="post" id="myForm">
   *   <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *   <input type="text" name="name"/>
   *   <input type="submit"/>
   * </form>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.form = function(chunk, context, bodies, params) {
    params = params || {};
    params = dust.helpers.tapAll(params, chunk, context);

    if (params.formData) {
      var formData = this.tap(params.formData, chunk, context);
      context = context.push({formData: formData});
    }

    var attrs = {method: "POST"};
    var body = play.createCsrfInput();

    if (params.alias) {
      attrs.action = play.url(params);
    }

    attrs = Utils.extend({}, attrs, play._.omit(params, ['alias', 'formData']));

    if (bodies && bodies.block) {
      return chunk.capture(bodies.block, context, function(out, chunk) {
        body += out;
        return chunk.end(HtmlUtils.createHtmlTag("form", attrs, body));
      });
    } else {
      return chunk.write(HtmlUtils.createHtmlTag("form", attrs, body));
    }
  };

  // TODO: handle enums as drop downs or radio buttons
  // TODO: handle booleans as check boxes
  /**
   * Write a form input based on JSON form data produced by PegasusFormPlugin.recordTemplateAsFormJson. Specify the
   * field to render via the 'field' param and the form data for the fields via the `formData` param. Alternatively, if
   * this is inside of a @form tag, you can add the `formData` to the @form tag and it'll apply to all @input calls
   * inside of it.
   *
   * The input will use the field's id as its id and name attributes. It will also include a label and a div for
   * displaying errors.
   *
   * You can customize behavior using the following parameters:
   *
   * 1. label: custom label text (default: field name)
   * 2. noLabel: don't include a label
   * 3. error: error text to display (default: none)
   * 4. noError: don't include an error div
   * 5. arrayIndex: will append [arrayIndex] to the id and name of the field. Useful for submitting lists.
   *
   * Examples:
   *
   * {@input field="name" formData="myFormData"/}
   *
   * {@form alias="controllers.Forms.submit" formData="myFormData"}
   *   {@input field="name"/}
   *   {@input field="age" noLabel="true" noError="true" id="custom-id"/}
   *   {@input field="preferences" type="checkbox" arrayIndex="0" noLabel="true" noError="true"/}
   *   {@input field="preferences" type="checkbox" arrayIndex="1" noLabel="true" noError="true"/}
   * {/form}
   *
   * Output:
   *
   * <label for="name">name</label>
   * <div class="error" id="name-error"></div>
   * <input type="text" id="name" name="name"/>
   *
   * <form action="/dust-sample/submit" method="post">
   *   <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *
   *   <label for="name">name</label>
   *   <div class="error" id="name-error"></div>
   *   <input type="text" id="name" name="name"/>
   *
   *   <input type="text" id="custom-id" name="age"/>
   *
   *   <input type="checkbox" id="preferences[0]" name="preferences[0]"/>
   *   <input type="checkbox" id="preferences[0]" name="preferences[0]"/>
   * </form>
   *
   * {@input fiel
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.input = function(chunk, context, bodies, params) {
    Utils.assert(params, "@input called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var formData = context.get("formData") || params.formData;
    Utils.assert(formData, "@input called with null formData");

    var field = play.getFormField(formData, params.field);
    Utils.assert(field, "@input did not find field " + params.field + " in the formData");
    Utils.assert(field.id, "@input did not find an id in field " + params.field);
    Utils.assert(field.name, "@input did not find a name in field " + params.field);

    var id = field.id;

    if (params.arrayIndex) {
      id += "[" + params.arrayIndex + "]";
    }

    var defaultAttrs = {type: "text", id: id, name: id};

    var value = field.value || field['default'];
    if (value) {
      defaultAttrs.value = value;
    }

    var attrs = Utils.extend({}, defaultAttrs, play._.omit(params, ['field', 'label', 'noLabel', 'noError', 'error', 'formData', 'field', 'arrayIndex']));

    var out = '';

    if (!params.noLabel) {
      var label = params.label || field.name;
      var labelAttrs = {"for": attrs.id};
      if (!field.optional) {
        labelAttrs['class'] = 'required';
      }
      out += HtmlUtils.createHtmlTag("label", labelAttrs, label);
    }

    if (!params.noError) {
      var error = params.error || field.error || '';
      var divAttrs = {
        id: attrs.id + "-error",
        "class": "error"
      };
      out += HtmlUtils.createHtmlTag("div", divAttrs, error);
    }

    out += HtmlUtils.createHtmlTag("input", attrs);

    return chunk.write(out);
  };

  /**
   * Traverses a pegasus form object to find a field. The fieldName will be of the format "A.B.C".
   *
   * @param formData
   * @param fieldName
   * @return {*}
   */
  play.getFormField = function(formData, fieldName) {
    Utils.assert(formData, "getFormField called with null formData");
    Utils.assert(fieldName, "getFormField called with null fieldName");

    var field = formData;

    var parts = fieldName.split(".");
    for (var i = 0; i < parts.length; i++) {
      field = field.fields[parts[i]];
      Utils.assert(field, "Could not find field " + fieldName + " in form data");
    }

    return field;
  };

})(play, LI, dust);


/**
 * Helpers for programmatically generating HTML markup. In most cases, you should use a dust template to render markup,
 * but the functions in this file are more convenient for rendering markup from a helper, where the ratio of logic to
 * markup is very high.
 */
(function(play, LI) {

  "use strict";

  var Utils = play.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Create a <script> tag with attributes obtained by merging customAttrs and params.
   *
   * @param customAttrs
   * @param params
   * @return {*}
   */
  play.createScriptTag = function(customAttrs, params) {
    Utils.assert(customAttrs, "createScriptTag called with null customAttrs");
    Utils.assert(params, "createScriptTag called with null params");

    var attrs = Utils.extend({type: "text/javascript"}, Utils.extend({}, customAttrs, play._.omit(params, ['path', 'paths'])));
    return HtmlUtils.createHtmlTag("script", attrs);
  };

  /**
   * Create a <link> tag to load CSS files
   *
   * @param customAttrs
   * @param params
   * @return {*}
   */
  play.createCssTag = function(customAttrs, params) {
    Utils.assert(customAttrs, "createCssTag called with null customAttrs");
    Utils.assert(params, "createCssTag called with null params");

    var attrs = Utils.extend({rel: 'stylesheet'}, Utils.extend({}, customAttrs, play._.omit(params, ['path', 'paths'])));
    return HtmlUtils.createHtmlTag("link", attrs);
  };

})(play, LI);


/**
 * Helpers for dealing with i18n and text formatting in JavaScript.
 */
(function(play, dust, LI) {

  "use strict";

  var Utils = play.Utils,
    DEFAULT_LOCALE = "en_US",
    dustRenderCalls = [],
    originalRenderFunction;

  //hijack dust.render if we are on client side and there's no Intl object
  //we want to load Intl polyfill before rendering any templates to make sure
  //i18n formatting is handled correctly
  if(play.isClient && typeof Intl === 'undefined') {
    originalRenderFunction = dust.render;

    dust.render = function() {
      dustRenderCalls.push(arguments);
    };

    play.getScript(play.getPageContextValue('intlPolyfillUrl', true), function() {
      var dustRenderCall;
      dust.render = originalRenderFunction;
      //execute all accumulated dust render calls once Intl polyfill was loaded
      while(dustRenderCall = dustRenderCalls.shift()){
        dust.render.apply(this, dustRenderCall);
      }
      //Done loading Intl polyfill
      //Fire event to indicate that dust is ready to be used
      play.isDustReady = true;
      play.trigger(play.EVENTS.DUST_READY);
    });
  } else {
    //Intl is natively supported by the browser
    //Fire an event indicating that Dust is ready
    play.isDustReady = true;
    play.trigger(play.EVENTS.DUST_READY);
  }

  /**
   * The @i18n helper is replaced by @translate in t8. To remain backward compatibility, make @i18n an alias of @translate
   */
  dust.helpers.i18n = dust.helpers.translate;

  /**
   * Keep original LI.i18n.getLocale function to remain backward compatible
   */
  var originalLIi18nGetLocale = LI.i18n.getLocale || function() { return {value: DEFAULT_LOCALE }; };

  /**
   * Hijack LI.i18n.getLocale to get the current locale directly from Play's page context. So that we don't need to
   * explicitly set it.
   *
   * t8 uses this method to determine locale if it's not specified in the helper.
   * Falls back to en_US if not found.
   *
   * If there is no page context defined (used in a non-play app), it will fall back to the original LI.i18n.getLocale function
   *
   * @returns {*|string}
   */
  LI.i18n.getLocale = function() {
    try {
      return {value: play.getPageContextValue("locale", false) || DEFAULT_LOCALE };
    } catch (err) {
      return originalLIi18nGetLocale();
    }
  };

  /**
   * Returns a truncated version of the given input string. Refer to the examples below for
   * truncation behavior.
   *
   * IMPORTANT: Do not use this for strings containing markup
   * IMPORTANT: Only use this for English
   *
   * Examples:
   *
   * {@truncate value="foo bar" length="3"/}
   * {@truncate value="foo bar" length="4"/}
   * {@truncate value="foo bar" length="5"/}
   * {@truncate value="foo bar" length="7"/}
   * {@truncate value="Hoboken, NJ" length="8" /}
   *
   * Output:
   *
   * "foo…"     // straight forward truncate
   * "foo…"     // truncate on whitespace
   * "foo…"     // truncate but don't include partial words
   * "foo bar"  // no truncation needed
   * "Hoboken…" // remove trailing punctuation mark after truncation
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.truncate = function(chunk, context, bodies, params) {
    Utils.assert(params.value, "@truncate called with null value param");
    Utils.assert(params.length, "@truncate called with null length param");
    Utils.assert(params.length > 0, "@truncate called with length param less than one");
    params = dust.helpers.tapAll(params, chunk, context);

    // removes punctuation from the end of a string (and removes trailing+leading whitespace)
    function removeTrailingPunctuation(value) {
      var lastChar = value.charAt(value.length - 1);

      if(lastChar === '.' || lastChar === ',') {
        value = value.substring(0, value.length - 1);
      }
      return value.trim();
    }

    var value = params.value.trim(),
        indexOfLastSpace = 0;

    if(value.length > params.length) {
      var lastChar = value.charAt(params.length - 1);

      if(lastChar === ' ' || lastChar === '\r' || lastChar === '\n' || lastChar === '\t') {
        // if the string is truncated at a white space character, just truncate from there
        value = removeTrailingPunctuation(value.substring(0, params.length)) + '\u2026';
      } else {
        // otherwise truncate from last space to prevent truncation in a word
        value = value.substring(0, params.length);
        indexOfLastSpace = value.lastIndexOf(' ');
        if(indexOfLastSpace > 0) {
          value = removeTrailingPunctuation(value.substring(0, indexOfLastSpace)) + '\u2026';
        } else {
          // allow word truncation when no other option
          value = removeTrailingPunctuation(value) + '\u2026';
        }
      }
    }

    return chunk.write(dust.escapeHtml(value));
  };

})(play, dust, LI);

/**
 * Security related helpers, mostly for dealing with CSRF.
 */
(function(play, LI, dust) {

  "use strict";

  var Utils = play.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Create a hidden <input> element for the CSRF token
   *
   * @return {*}
   */
  play.createCsrfInput = function() {
    var attributes = {
      type: "hidden",
      name: "csrfToken",
      value: play.getPageContextValue("csrfToken", true)
    };
    return HtmlUtils.createHtmlTag("input", attributes, null);
  };

  /**
   * Add the CSRF token to the given URL
   *
   * @param url
   * @return {*}
   */
  play.addCsrfTokenToUrl = function(url) {
    Utils.assert(url, "addCsrfTokenToUrl called with null url");

    return play.addQueryParameter(url, "csrfToken", play.getPageContextValue("csrfToken", true));
  };

  /**
   * Write the CSRF token.
   *
   * Example:
   *
   * {@csrf/}
   *
   * Output:
   *
   * ajax:12345
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.csrf = function(chunk) {
    return chunk.write(dust.escapeHtml(play.getPageContextValue("csrfToken", true)));
  };

  /**
   * Create a hidden input for a CSRF token.
   *
   * Example:
   *
   * {@createCsrfInput/}
   *
   * Output:
   *
   * <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.createCsrfInput = function(chunk) {
    return chunk.write(play.createCsrfInput());
  };

  /**
   * Helper to check if current member is a csUser
   *
   * Example:
   *
   * {@isCsUser}
   *   fizzy embeds here
   * {/isCsUser}
   *
   * Output:
   *
   * boolean
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.isCsUser = function(chunk, context, bodies) {
    if (play.getPageContextValue("isCsUser", false) && bodies.block) {
      return chunk.render(bodies.block, context)
    }
    return chunk;
  };

})(play, LI, dust);

(function(play, dust) {

  "use strict";

  /**
   * Convenience method to get the appName value from the pageContext.
   *
   * TODO BL I'm not (yet) sure whether it was intended to not expose the page context to helpers. If my guess is wrong,
   * then perhaps we can create a more generalised helper to access an arbitrary page context value.
   */
  dust.helpers.contextPath = function(chunk) {
    return chunk.write(play.getPageContextValue("contextPath"));
  }

})(play, dust);

/**
 * Helpers for generating URLs in JavaScript and dust templates.
 */
(function(play, LI, dust, sc) {

  'use strict';

  var Utils = play.Utils;
  var HtmlUtils = LI.HtmlUtils;
  var URL_MAX_LENGTH = 1024;
  var URL_HASH_LENGTH = 25;
  var TYPICAL_CDN_URL_LENGTH = 50;

  play.MEDIA_URN_PREFIX = 'urn:li:media:';

  /**
   * Initialize the asset cache, which is a map of assets already
   * included in this dust instance
   *
   * @type {Object}
   */
  var assetCache = {
    cache: {},
    add: function(assetPath) {
      this.cache[assetPath] = true;
    },
    remove:  function(assetPath) {
      if (assetPath in this.cache) {
        delete this.cache[assetPath];
      }
    },
    exists: function(assetPath) {
      return (assetPath in this.cache);
    },
    clear: function(assetPath) {
      this.cache = {};
    },
    getAssets: function() {
      var assets = [];
      for(var assetPath in this.cache) {
        assets.push(assetPath);
      }
      return assets;
    }
  };

  /**
   * Make assetCache#exists and assetCache#getAssets publicly
   * accessible
   **/
  play.assetCache = {
    exists: function(assetPath) {
      return assetCache.exists(assetPath);
    },
    getAssets: function(assetPath) {
      return assetCache.getAssets(assetPath);
    }
  };

  /**
   * If value is a single value, wrap it in an array. If it is already an array, return it unchanged.
   *
   * @param value
   * @returns {*}
   */
  var asArray = function(value) {
    return Utils.isArray(value) ? value : [value];
  };

  /**
   * Estimates the length of generated url in prod. Will throw exception in dev mode if either the generated versioned or hashed
   * url may exceed the length limit.
   *
   * TODO: CORP apps don't use spark URLs, so we need to a) not do this check and b) prevent them from using dynamic concat in general.
   * @param paths
   */
  var checkUrlLength = function(paths) {
    var sparkBaseForFiles = play.getPageContextValue('baseSparkUrlForFiles', true);
    var sparkBaseForHashes = play.getPageContextValue('baseSparkUrlForHashes', true);
    var appName = play.getPageContextValue('appName', true);

    var versionedUrlLength = formatPathsForSpark(appName, paths).length + sparkBaseForFiles.length;
    // +1 for the comma
    var estHashedUrlLength = paths.length * (URL_HASH_LENGTH + 1) + sparkBaseForHashes.length;

    var shorterUrlLength = (versionedUrlLength > estHashedUrlLength) ? estHashedUrlLength : versionedUrlLength;

    if (shorterUrlLength > URL_MAX_LENGTH - TYPICAL_CDN_URL_LENGTH) {
      var errorMsg = 'Error: The generated URL for paths ' + paths + ' MAY exceed the max length of ' +
                      URL_MAX_LENGTH + ' in production. Please break up the URLs into multiple pieces, or use build time concat.';
      if (!play.isProd()) {
        play.log(errorMsg);
      }
    }
  };

  /**
   * Converts the given paths into a format Spark (and the Play assets controller) can understand.
   *
   * See: https://iwww.corp.linkedin.com/wiki/cf/display/ENGS/Spark+Server#SparkServer-ContentServingEndpoints
   *
   * In particular, we:
   *
   * 1. Prepend the app name to each path. This is because we package all static content for a multiproduct into one
   *    artifact and use the app name as a namespace.
   *
   * 2. URL encode each path
   *
   * 3. Join them all with commas
   *
   * @param paths
   * @returns {*}
   */
  var formatPathsForSpark = function(appName, paths) {
    return Utils.map(paths, function(path) {
      return encodeURIComponent(play.buildPath('/', appName, '/', path));
    }).join(',');
  };

  /**
   * Builds a versioned spark url from given paths in this form:
   *
   * http://<cdn-host>/sc/p/<mp-org>:<mp-name>-static-content+<app-version>/f/<paths>
   *
   * @param paths
   * @returns {}
   */
  var versionedSparkUrl = function(appName, paths) {
    paths = formatPathsForSpark(appName, paths);

    var sparkBasePath = play.getPageContextValue('baseSparkUrlForFiles', true);
    return play.appendCdnUrlIfNeeded(play.combineUrlPieces(sparkBasePath, paths));
  };

  /**
   * Builds a hashed spark url from given paths in this form:
   *
   * http://<cdn-host>/sc/h/<hashes>
   *
   * @param paths
   * @returns {}
   */
  var hashedSparkUrl = function(appName, paths) {
    paths = Utils.map(paths, function(path) {return sc.hashes[appName][path];}).join(',');

    var sparkBasePath = play.getPageContextValue('baseSparkUrlForHashes', true);
    return play.appendCdnUrlIfNeeded(play.combineUrlPieces(sparkBasePath, paths));
  };

  /**
   * Produce multiple script tags if disableDynamicConcat is true, otherwise, output one script tag with dynamic concat URL.
   */
  var maybeDynamicConcatJs = function(paths, generateUrl, params) {
    if (play.getPageContextValue("disableDynamicConcat", false)) {
      return play._.reduce(paths, function(acc, path) { return acc + play.createScriptTag({src: generateUrl(path)}, params) + "\n"; }, "");
    } else {
      return play.createScriptTag({src: generateUrl(paths)}, params);
    }
  };

  /**
   * Produce multiple link tags if disableDynamicConcat is true, otherwise, output one script tag with dynamic concat URL.
   */
  var maybeDynamicConcatCss = function(paths, generateUrl, params) {
    if (play.getPageContextValue("disableDynamicConcat", false)) {
      return play._.reduce(paths, function(acc, path) { return acc + play.createCssTag({href: generateUrl(path)}, params) + "\n"; }, "");
    } else {
      return play.createCssTag({href: generateUrl(paths)}, params);
    }
  };

  /**
   * Returns true iff a CDN should be used when building URLs to static assets
   *
   * @return
   */
  play.useCdn = function() {
    return play.getPageContextValue('useCdn', false);
  };

  /**
   * Returns true iff this Play app is running in production mode
   *
   * @return
   */
  play.isProd = function() {
    return play.getPageContextValue('isProd', false);
  };

  /**
   * Returns true iff using sc-hashes.js file on this page
   *
   * @return
   */
  play.useScHashesJs = function() {
    return play.getPageContextValue('useScHashesJs', false);
  };

  /**
   * Returns true iff hashed URL is disabled by uh=f query param
   *
   * @return
   */
  play.hashesDisabledByQueryParam = function() {
    return play.getPageContextValue('hashesDisabledByQueryParam', false);
  };

  /**
   * Add the given key value pair as a query string parameter to the given url. If the key value pair is already in the
   * URL, this will replace the old value.
   *
   * @param url
   * @param key
   * @param value
   * @return {*}
   */
  play.addQueryParameter = function(url, key, value) {
    Utils.assert(url, 'addQueryParameter called with null url');
    Utils.assert(key, 'addQueryParameter called with null key');
    Utils.assert(value, 'addQueryParameter called with null value');

    key = encodeURIComponent(key);
    value = encodeURIComponent(value);

    var re = new RegExp('([?|&])' + key + '=.*?(&|$)', 'i');
    if (url.match(re)) {
      return url.replace(re, '$1' + key + '=' + value + '$2');
    } else {
      var questionIndex = url.indexOf('?');
      var separator = '&';
      if (questionIndex < 0) {
        separator = '?';
      } else if (questionIndex === url.length - 1) {
        separator = '';
      }

      return url + separator + key + '=' + value;
    }
  };

  /**
   * Use this helper to get all the parameters as a json object from an url.
   * It accepts a full url like "http://www.linkedin.com?par1=val1&par2=val2"
   * or only the queryString like "?par1=val1&par2=val2".
   * In both cases it returns {par1: ["val1"], par2: ["val2"]} as a js object.
   * Special characters are also welcome, they will be decoded properly.
   * All values for each parameter are returned as arrays to keep type consistence with multiple value parameters.
   * For instance:
   *
   * parseQueryString("par1=val1a&par1=val1b&par2=val2")
   * returns:
   * {par1: ["val1a", "val1b"], par2: ["val2"]}
   *
   * It throws an exception if url is undefined, null, empty or there are more than one "?" character.
   *
   * @param url
   * @return {*}
   */
  play.parseQueryString = function(url) {
    Utils.assertDefined(url, 'parseQueryString called with null url');

    var urlParts = url.split('?');
    Utils.assert(urlParts.length <=2 , 'Malformed url');

    if (urlParts.length<2) {
      return {};
    }

    var queryString = urlParts[1];  //urlParts.length == 2, so takes the queryString to parse

    // loops and split all parameters, allowing array of parameters to be stored as a js array
    var result = {};
    var keyValues = queryString.split('&');
    Utils.each(keyValues, function(keyValue) {
      var keyValueParts = keyValue.split('=');
      var key = decodeURIComponent(keyValueParts[0]);
      //discards consecutive ampersands since keys cannot be empty, only values
      if (key) {
        // ternary operator handles parameters equaling to no value like 'param1=&param2=value2'
        var value = (keyValueParts.length > 1) ? decodeURIComponent(keyValueParts[1]) : '';
        //creates an array of values even if it's just one
        if (result[key]) {
          result[key].push(value);
        }
        else {
          result[key] = [value];
        }
      }
    });

    return result;
  };

  /**
   * This is a wrapper for parseQueryString that returns only the first element for each key.
   * It's comfortable to use when all parameters in the url have single values, since it eliminates the need
   * to dereference the array.
   *
   * Example:
   * parseQueryString("http://www.linkedin.com?par1=val1&par2=val2") returns {par1: ["val1"], par2: ["val2"]}
   * parseQueryStringSimple("http://www.linkedin.com?par1=val1&par2=val2") returns {par1: "val1", par2: "val2"}
   */
  play.parseQueryStringSimple = function(url) {
    var queryString = play.parseQueryString(url);
    var queryStringSimple = {};

    Utils.each(queryString, function(value, key) {
      queryStringSimple[key] = value[0];
    });

    return queryStringSimple;
  };

  /**
   * Use this helper to build an url with parameters from a map.
   * Since it allows parameters to be arrays, each key must map to an array of values, even if it is just one (see examples below).
   * Url maybe null or empty so to build only the query string.
   * The initial url may contain additional parameters which will be replaced by values in the  map when names match.
   *
   * Examples:
   *
   * buildUrl("http://www.linkedin.com?par1=val1", {par1: ["val1new"], par2: ["val2new"]})
   * returns:
   * "http://www.linkedin.com?par1=val1new&par2=val2new"
   *
   * buildUrl("", {par1: ["val1new"],par2: ["val2new"]})
   * returns:
   * "par1=val1new&par2=val2new"
   *
   * Examples to build urls with array parameters:
   *
   * buildUrl("http://www.linkedin.com?par1=val1&par2=val2", {par1: ["val1new"], par2: ["val2anew", "val2bnew"]})
   * returns:
   * "http://www.linkedin.com?par1=val1new&par2=val2anew&par2=val2bnew"
   *
   * It throws an exception if urlParameters is undefined or null.
   *
   * @param url
   * @param urlParameters
   * @return {*}
   */
  play.buildUrl = function(url, urlParameters) {
    url = url || '';
    urlParameters = urlParameters || {};

    // takes care of nulls and undefined for url
    var baseUrl = url.split('?')[0];
    var parameters = play.parseQueryString(url);

    //if an existing parameter is also in the urlParameters map, its value is replaced by the new one
    Utils.each(urlParameters, function(value, key) {
      parameters[key] = value;
    });

    var keyValues = [];
    Utils.each(parameters, function(values, key) {
      Utils.assert(Array.isArray(values), 'All parameter values must be arrays, see buildUrlSimple for simple values');
      Utils.each(values, function(value) {
        keyValues.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
      });
    });

    var queryString = keyValues.join('&');
    return baseUrl + '?' + queryString;
  };

  /**
   * This is a wrapper for buildUrl when all parameters have only one value.
   * It eliminates the need to create one-element arrays.
   *
   * Example:
   *
   * @param url
   * @param urlParameters
   * @returns {*}
   */
  play.buildUrlSimple = function(url, urlParameters) {
    url = url || '';
    urlParameters = urlParameters || {};

    var urlParametersArray = {};

    Utils.each(urlParameters, function(value, key) {
      Utils.assert(typeof(urlParameters[key])==='string', 'All values must be simple strings, for array parameters see buildUrl');
      urlParametersArray[key] = [value];
    });

    return play.buildUrl(url, urlParametersArray);
  };

  /**
   * Use this helper to get a simple value of a single parameter by name from an url.
   * It's a wrapper for parseQueryStringSimple(url)[name] so behaviour is exactly the same for just one parameter.
   * For parameters with multiple values use the most general function parseQueryString.
   *
   * Example:
   *
   * getUrlParameter("http://www.linkedin.com?par1=val1&par2=val2","par1") returns "val1"
   *
   * @param url
   * @param name
   * @return {*}
   */
  play.getUrlParameter = function(url, name) {
    Utils.assert(url, 'getUrlParameter called with null url');
    Utils.assert(name, 'getUrlParameter called with null parameter name');
    return play.parseQueryStringSimple(url)[name];
  };

  /**
   * Combine two pieces of a URL and ensure that there is only one slash between them.
   *
   * Example:
   *
   * combineUrlPieces("/foo/bar/", "/baz/blah/abc.js")
   *
   * Returns: "/foo/bar/baz/blah/abc.js"
   *
   * @param leftPiece
   * @param rightPiece
   * @return {*}
   */
  play.combineUrlPieces = function(leftPiece, rightPiece) {
    if (!leftPiece) {
      return rightPiece;
    }

    if (!rightPiece) {
      return leftPiece;
    }

    if (/\?$/.test(leftPiece) || /^\?/.test(rightPiece)) {
      return leftPiece + rightPiece;
    } else {
      return leftPiece.replace(/\/$/, '') + '/' + rightPiece.replace(/^\//, '');
    }
  };

  /**
   * Combine multiple parts of a URL to create a single path, ensuring just one slash between any two parts.
   *
   * Example:
   *
   * buildPath("foo", "/bar/", "/baz")
   *
   * Returns: "foo/bar/baz"
   *
   * @returns {*}
   */
  play.buildPath = function() {
    if (arguments.length === 0) {
      return '';
    }

    return play._.reduce(arguments, function(acc, path) { return play.combineUrlPieces(acc, path); });
  };

  /**
   * Add the given locale to the given path.
   *
   * Example:
   *
   * addLocale("/foo/bar", "en_US")
   *
   * Returns: "/foo/bar_en_US"
   *
   * @param path
   * @param locale
   * @return {*}
   */
  play.addLocale = function(path, locale) {
    Utils.assert(path, 'addLocale called with null or empty path');
    Utils.assert(locale, 'addLocale called with null or empty locale');

    return path.endsWith(locale) ? path : path + '_' + locale;
  };

  /**
   * Replace <locale> placeholders with the current locale
   *
   * Example:
   *
   * replaceWithLocale("/foo/<locale>/bar", "en_US")
   *
   * Returns: "/foo/en_US/bar"
   *
   * @param paths
   * @returns {*}
   */
  play.replaceWithLocale = function(paths) {
    Utils.assert(paths, 'replaceWithLocale called with null or empty path');

    paths = asArray(paths);
    var locale = play.getPageContextValue('locale', true);
    return Utils.map(paths, function(path) { return path.replace(/<locale>/g, locale);});
  };

  /**
   * Adds the file extension to path, unless path already ends with that file extension
   *
   * Example:
   *
   * addExtension("/foo/bar", ".js")
   *
   * Returns: "/foo/bar.js"
   *
   * @param path
   * @param ext
   * @return {*}
   */
  play.addExtension = function(path, ext) {
    Utils.assert(path, 'addExtension called with null path');
    Utils.assert(ext, 'addExtension called with null extension');

    return path.endsWith(ext) ? path : path + ext;
  };

  /**
   * If path ends with extension ext, remove it, otherwise return path unchanged.
   *
   * @param path
   * @param ext
   * @return {*}
   */
  play.removeExtension = function(path, ext) {
    Utils.assert(path, 'removeExtension called with null path');
    Utils.assert(ext, 'removeExtension called with null extension');

    return path.endsWith(ext) ? path.substring(0, path.length - ext.length) : path;
  };

  /**
   * Extract the values of all "argXXX" keys within given length, where XXX is an int (e.g. arg0, arg1, arg2) and return them as an array.
   *
   * Example:
   *
   * extractUrlArgs({arg0: "foo", arg1: "bar", someOtherThing: "will be ignored"}, 3)
   *
   * Returns: ["foo", "bar"]
   *
   * extractUrlArgs({arg1: "bar", arg2: "baz"}, 3)
   *
   * Returns: [null, "bar", "baz"]
   *
   * @param params
   * @return {Array}
   */
  play.extractUrlArgs = function(params, len) {
    params = params || {};
    var args = [];

    for (var i = 0; i < len; ++i) {
      if (params['arg' + i] !== undefined) {
        args.push(params['arg' + i]);
      } else {
        args.push(null);
      }
    }

    return args;
  };

  /**
   * Play can generate JavaScript reverse routers: that is, JavaScript code that builds URLs to controllers in your
   * routes file. This method finds the reverse router for the alias specified in the params object or throws an
   * exception if a router cannot be found for the given alias.
   *
   * Params can contain:
   *
   * 1. alias: the name of the alias, which should be of the form <controller>.<action> (e.g. controllers.HelloWorld.index)
   * 2. args0, args1, args...: the arguments to pass to the action method to generate the URL
   *
   * The returned router will have url and absoluteUrl properties.
   *
   * @param params
   * @return {*}
   */
  play.reverseRouterForAlias = function(params) {
    Utils.assert(params, 'reverseRouterForAlias called with null params object');
    Utils.assert(params.alias, 'reverseRouterForAlias called with a params object that does not define an alias');
    Utils.assert(play.jsRoutes, 'Could not find any JavaScript reverse routers. Did you define any in your DustOptions object?');

    var alias = params.alias;
    var route = play.traverseObject(play.jsRoutes, alias);

    Utils.assert(route, 'Could not find alias ' + alias + '. Make sure to define the proper JavaScriptRoutes in your DustOptions object.');

    var args = params.args || play.extractUrlArgs(params, route.length);

    args = args || [];
    var argList = args instanceof Array ? args : [args];

    return route.apply(route, argList);
  };

  /**
   * Use the reverseRouterForAlias function to find the JavaScript reverse router for the alias specified in params and
   * use it to generate a URL. If params.withCsrf is specified, a CSRF token will be appended to the URL.
   *
   * @param params
   * @param {boolean} [absolute=false] If true, an absolute URL will be returned
   * @return {*}
   */
  play.url = function(params, absolute) {
    absolute = absolute || false;

    Utils.assert(params, 'url called with null params object');

    var reverseRoute = play.reverseRouterForAlias(params);
    var url = play._.result(reverseRoute, absolute ? 'absoluteURL' : 'url');

    if (params.withCsrf) {
      url = play.addCsrfTokenToUrl(url);
    }

    // Allow track or trk because the naming is sometimes confusing.
    var track = params.track || params.trk;
    if (track) {
      url = play.addQueryParameter(url, 'trk', track);
    }

    return url;
  };

  /**
   * Build a URL to the static asset(s) specified by paths. If paths is an array of multiple paths, you will get a
   * dynamically concatenated resource. In dev mode, this will go back to your Play app. In prod, this will go via the
   * CDN to spark.
   *
   * @param paths a single path (String) or multiple
   * @return {String}
   */
  play.assetUrl = function(paths) {
    Utils.assert(paths, 'assetUrl called with null paths');
    paths = asArray(paths);
    checkUrlLength(paths);

    var oldAssetUrlRouter = play.traverseObject(play.jsRoutes, 'controllers.Assets');
    var newAssetUrlRouter = play.traverseObject(play.jsRoutes, 'com.linkedin.assets.AssetsController');
    var appName = play.getPageContextValue('appName', true);

    //cdn urls exits only when we are in prod mode
    if (play.useCdn()) {
      return play.sparkUrl(paths);
    } else if (oldAssetUrlRouter) {
      return oldAssetUrlRouter.at(paths).url;
    } else if (newAssetUrlRouter) {
      return newAssetUrlRouter.at(formatPathsForSpark(appName, paths)).url;
    } else {
      return play.combineUrlPieces(play.getPageContextValue('baseAssetsUrl', true), formatPathsForSpark(appName, paths));
    }
  };

  /**
   * Build a URL back to spark for static content for the given paths. If paths has more than one file in it, you will
   * get a dynamically concatenated resource. In dev mode, this URL will go back to the Play app, which will act as a
   * fake spark and serve files from the file system. In prod mode, the URL will be a CDN URL that goes to spark.
   * If static content hashes of all paths exist in the global namespace, the result URL will be hash based.
   *
   * @param paths
   * @returns {*}
   */
  play.sparkUrl = function(paths) {
    Utils.assert(paths, 'sparkUrl called with null paths');
    var appName = play.getPageContextValue('appName', true);

    var hashNotFound = function (path) {
      var result = !Utils.isDefined(sc.hashes[appName][path]);
      return result;
    };

    if (play.hashesDisabledByQueryParam() || !Utils.isDefined(sc.hashes[appName]) || Utils.some(paths, hashNotFound)) {
      return versionedSparkUrl(appName, paths);
    } else {
      return hashedSparkUrl(appName, paths);
    }
  };

  /**
   * Build a URL to the "unversioned" static content directory of SCDS. This will be a relative URL in dev and a CDN URL
   * in prod.
   *
   * Example:
   *
   * scdsDirectUnversionedUrl("foo.js")
   *
   * Returns: /scds/common/u/foo.js
   *
   * @param path
   * @return {*}
   */
  play.scdsDirectUnversionedUrl = function(path) {
    Utils.assert(path, 'scdsDirectUnversionedUrl called with null path');

    return play.scdsDirectUrl(play.combineUrlPieces('common/u/', path));
  };

  /**
   * Build a URL to SCDS. This will be a relative URL in dev and a CDN URL in prod.
   *
   * Example:
   *
   * scdsDirectUrl("foo.js")
   *
   * Returns: /scds/foo.js
   *
   * @param path
   * @return {*}
   */
  play.scdsDirectUrl = function(path) {
    Utils.assert(path, 'scdsDirectUrl called with null path');

    if (/^http/.test(path)) {
      return path;
    } else {
      return play.combineUrlPieces(play.getPageContextValue('baseScdsUrl', true), path);
    }
  };

  /**
   * For relative urls, append the cdn url. For absolute urls, keep them as they are. Note: in dev mode, the cdn url
   * is typically an empty string, so the URL will remain a relative URL.
   *
   * @param path
   * @return {*}
   */
  play.appendCdnUrlIfNeeded = function(path) {
    Utils.assert(path, 'appendCdnUrlIfNeeded called with null path');

    if (/^http/.test(path)) {
      return path;
    } else {
      return play.combineUrlPieces(play.getPageContextValue('cdnUrl', true), path);
    }
  };

  /**
   * Create a URL for the given path(s) that will include the current locale and given extension. If paths is an array,
   * the locale will be added to each path in it, and the URL will be to a dynamically concatenated resource. If a path
   * already had an extension, it will be replaced.
   *
   * Example:
   *
   * localizedAssetUrl("/foo/bar.js", ".css")
   *
   * Output:
   *
   * "/foo/bar_en_US.css"
   *
   * @param paths
   * @param extension
   * @return {*}
   */
  play.localizedAssetUrl = function(paths, extension) {
    Utils.assert(paths, 'localizedAssetUrl called with null or empty paths');

    paths = asArray(paths);

    var locale = play.getPageContextValue('locale', true);
    var pathsWithLocale = Utils.map(paths, function(path) { return play.addExtension(play.addLocale(play.removeExtension(path, extension), locale), extension); });

    return play.assetUrl(pathsWithLocale);
  };

  /**
   * Short hand function for play.localizedAssetUrl(paths, ".js")
   *
   * @param paths
   * @returns {*}
   */
  play.localizedJsAssetUrl = function(paths) {
    return play.localizedAssetUrl(paths, ".js");
  };

  /**
   * Short hand function for play.localizedAssetUrl(paths, ".css")
   *
   * @param paths
   * @returns {*}
   */
  play.localizedCssAssetUrl = function(paths) {
    return play.localizedAssetUrl(paths, ".css");
  };

  /**
   * Create the URL for the given dust template path(s) under public/templates. This method will automatically add the
   * locale and the .js extension to the URL. If paths is an array, the locale will be added to each path in it and
   * the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * templateUrl("foo")
   *
   * Returns: "/assets/templates/foo_en_US.js"
   *
   * @param paths
   * @return {*}
   */
  play.templateUrl = function(paths) {
    Utils.assert(paths, 'templateUrl called with null or empty paths');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) {
      return (path.startsWith('templates') || path.startsWith('scmp')) ? path : play.combineUrlPieces('templates', path);
    });
    return play.localizedAssetUrl(pathsWithPrefix, '.js');
  };

  /**
   * Create the URL for build-time concatenated css path(s) under public/concat. This method will automatically add the
   * locale and the .css extension to the URL. If paths is an array, the locale and extension will be added to each path
   * in it and the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * concatCssUrl("foo/bar")
   *
   * Output:
   *
   * "/assets/concat/foo/bar.css"
   *
   * @param paths
   * @return {*}
   */
  play.concatCssUrl = function(paths) {
    Utils.assert(paths, 'concatCssUrl called with null or empty paths');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) { return play.combineUrlPieces('concat', path); });
    return play.localizedAssetUrl(pathsWithPrefix, '.css');
  };

  /**
   * Create the URL for build-time concatenated js path(s) under public/concat. This method will automatically add the
   * locale and the .js extension to the URL. If paths is an array, the locale and extension will be added to each path
   * in it and the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * concatJsUrl("foo/bar")
   *
   * Output:
   *
   * "/assets/concat/foo/bar.js"
   *
   * @param paths
   * @return {*}
   */
  play.concatJsUrl = function(paths) {
    Utils.assert(paths, 'concatJsUrl called with null or empty paths');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) { return play.combineUrlPieces('concat', path); });
    return play.localizedAssetUrl(pathsWithPrefix, '.js');
  };

  /**
   * Returns a URL to the JavaScript path(s) under public/javascripts. The .js extension will be added automatically. If
   * paths is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.jsUrl = function(paths) {
    Utils.assert(paths, 'jsUrl called with null paths');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) { return play.combineUrlPieces('javascripts', play.addExtension(path, '.js')); });
    return play.assetUrl(pathsWithPrefix);
  };

  /**
   * Returns a URL to the CSS path(s) under public/stylesheets. The .css extension will be added automatically. If paths
   * is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.cssUrl = function(paths) {
    Utils.assert(paths, 'cssUrl called with null path');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) { return play.combineUrlPieces('stylesheets', play.addExtension(path, '.css')); });
    return play.assetUrl(pathsWithPrefix);
  };

  /**
   * Returns a URL to the CSS path(s) compiled from SCSS under public/scss. The .css or scss extension is optional. If
   * paths is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.scssUrl = function(paths) {
    Utils.assert(paths, 'scssUrl called with null path');

    paths = asArray(paths);
    var pathsWithPrefix = Utils.map(paths, function(path) {
      var withoutExtension = play.removeExtension(path, '.scss');
      return play.combineUrlPieces('scss', withoutExtension);
    });


    return play.localizedAssetUrl(pathsWithPrefix, '.css');
  };

  /**
   * Returns a URL to an image file under public/images.
   *
   * @param path
   * @return {*}
   */
  play.imgUrl = function(path) {
    Utils.assert(path, 'imgUrl called with null path');

    return play.assetUrl('images/' + path);
  };

  /**
   * Returns a URL to an unfiltered image hosted by the mpr service.
   *
   * @param mediaId
   * @return {*}
   */
  play.rawMprUrl = function(mediaId) {
    Utils.assert(mediaId, 'rawMprUrl called with null mediaId');

    return play.combineUrlPieces(play.getPageContextValue('baseMprUrl', true), mediaId);
  };

  /**
   * Returns a URL to an image hosted by the mpr service.
   *
   * @param mediaId
   * @param width
   * @param height
   * @param withoutPadding
   * @return {*}
   */
  play.mprUrl = function(mediaId, width, height, withoutPadding) {
    Utils.assert(mediaId, 'mprUrl called with null mediaId');
    Utils.assert(width, 'mprUrl called with null width/size');

    var imageWidth = width,
        imageHeight,
        filterPostfix;

    if (typeof height === 'boolean') {
      imageHeight = width;
      withoutPadding = height;
    } else {
      imageHeight = height || width;
    }
    filterPostfix = (withoutPadding) ? 'np' : '';

    return play.combineUrlPieces(play.getPageContextValue('baseMprUrl', true), 'shrink' + filterPostfix + '_' + imageWidth + '_' + imageHeight + mediaId);
  };

  /**
   * Returns a URL to an image via the media path.
   *
   * @param mediaId
   * @return {*}
   */
  play.mediaUrl = function(mediaId) {
    Utils.assert(mediaId, 'mediaUrl called with null mediaId');

    return play.combineUrlPieces(play.getPageContextValue('baseMediaUrl', true), mediaId);
  };

  /**
   * Returns a URL to a "no photo" ghost profile image
   *
   * @param width
   * @param height
   * @return {*}
   */
  play.noPhotoUrl = function(width, height) {
    Utils.assert(width, 'noPhotoUrl called with null width/size');

    var imageWidth = width;
    var imageHeight = height || width;
    return play.scdsDirectUnversionedUrl('/images/themes/katy/ghosts/person/ghost_person_' + imageWidth + 'x' + imageHeight + '_v1.png');
  };

  /**
   * Extracts a list of paths from params. The list will contain all entries in params.path and params.paths; the
   * latter can contain a comma separated list that will be automatically split into separate entries.
   *
   * Example:
   *
   * getPathList({path: "foo", paths: "bar,baz,blah"})
   *
   * Returns: ["foo", "bar", "baz", "blah"]
   *
   * @param params
   * @return {Object}
   */
  play.getPathList = function(params) {
    params = params || {};

    var allPaths = [];

    if (params.path) {
      allPaths.push(params.path);
    }

    if (params.paths) {
      allPaths = allPaths.concat(Array.isArray(params.paths) ? params.paths : params.paths.split(','));
    }

    return Utils.map(allPaths, function(path) { return path.trim(); });
  };

  /**
   * Extract mediaId from URN
   *
   * Example
   * Input: "urn:li:media:/p/1/000/000/1b4/1305a66.png"
   * Output: "/p/1/000/000/1b4/1305a66.png"
   *
   * @param urn
   * @return {String}
   */
  play.getMediaIdFromUrn = function(urn) {
    var indexOfUrnMediaPrefix = urn.lastIndexOf(play.MEDIA_URN_PREFIX);

    Utils.assert(indexOfUrnMediaPrefix > -1, 'Incorrectly formatted URN');

    return urn.slice(indexOfUrnMediaPrefix + play.MEDIA_URN_PREFIX.length);
  };

  /**
   * Extract mediaId from a params object, returns params.mediaId if present
   * otherwise the mediaId is extracted from params.urn. If neither returns undefined.
   *
   * @param params
   * @returns {*}
   */
  play.getMediaIdFromParams = function(params) {
    Utils.assert(params, 'getMediaIdFromParams called with null params');

    if(params.mediaId) {
      return params.mediaId;
    } else if(params.urn) {
      return play.getMediaIdFromUrn(params.urn);
    } else {
      return undefined;
    }
  };

  /**
   * Writes the URL for the asset(s) specified in the 'path' or 'paths' parameters. If multiple paths are specified,
   * the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@assetUrl path="foo.js"/}
   * {@assetUrl paths="foo.js, bar.js"/}
   *
   * Output:
   *
   * /dust-sample/assets/foo.js
   * /dust-sample/assets/foo.js,bar.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.assetUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.assetUrl(paths)));
  };

  /**
   * Writes the URL for the dust template path(s) under public/templates. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@templateUrl path="foo"/}
   * {@templateUrl path="foo, bar"/}
   *
   * Output:
   *
   * /assets/templates/foo_en_US.js
   * /assets/templates/foo_en_US.js,bar_en_US.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.templateUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.templateUrl(paths)));
  };

  /**
   * Writes the URL for the build-time concatenated css path(s) under public/concat. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@concatCssUrl path="foo"/}
   *
   * Output:
   *
   * /assets/concat/foo_en_US.css
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatCssUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.concatCssUrl(paths)));
  };

  /**
   * Writes the URL for the build-time concatenated js path(s) under public/concat. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@concatJsUrl path="foo"/}
   *
   * Output:
   *
   * /assets/concat/foo_en_US.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatJsUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.concatJsUrl(paths)));
  };

  /**
   * Creates a script tag for the linkedin-dust JavaScript library.
   *
   * Example:
   *
   * {@linkedInDustScriptTag/}
   *
   * Output:
   *
   * <script src="/assets/dust/dev/linkedin-dust.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.linkedInDustScriptTag = function(chunk, context, bodies, params) {
    var liDustJSURL = play.getPageContextValue('serveT8WithDust', true) ?
        play.getPageContextValue('linkedInDustI18nJsUrl', true) : play.getPageContextValue('linkedInDustJsUrl', true);


    return chunk.write(play.createScriptTag({src: liDustJSURL}, params || {}));
  };

  /**
   * Writes a script tag with the URL for the dust template path(s) under public/templates. If more than one path is
   * specified, the URL will be for a dynamically concatenated resource.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@template path="foo"/}
   * {@template paths="bar, a/b/c" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/templates/foo_en_US.js"></script>
   * <script type="text/javascript" src="/assets/templates/bar_en_US.js,/assets/templates/a/b/c_en_US.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.template = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatJs(paths, play.templateUrl, params));
  };

  /**
   * Writes a link tag with the URL for the build-time concatenated css path(s) under public/concat. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource.
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag. The URLs will automatically include the proper locale.
   *
   * Examples:
   *
   * {@concatCss path="foo"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/concat/foo_en_US.css"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatCss = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatCss(paths, play.concatCssUrl, params));
  };

  /**
   * Writes a script tag with the URL for the build-time concatenated js path(s) under public/concat. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag. The URLs will automatically include the proper locale.
   *
   * Examples:
   *
   * {@concatJs path="foo"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/concat/foo_en_US.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatJs = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatJs(paths, play.concatJsUrl, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource. If the path contains <locale> placeholders,
   * they will be replaced by the current locale. This is useful if you only want some of the js be localized.
   * All paths are assumed to be relative to /public/javascripts.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@js path="jquery"/}
   * {@js paths="dust, controllers/foo" id="bar"/}
   * {@js paths="dust, lib/t8/<locale>/t8.js" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/javascripts/jquery.js"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust.js,/assets/javascripts/controllers/foo.js" id="bar"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust.js,/assets/lib/t8/en_US/t8.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.js = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.replaceWithLocale(play.getPathList(params));

    return chunk.write(maybeDynamicConcatJs(paths, play.jsUrl, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified the URL will be for a dynamically concatenated resource. The URL will  include the current
   * locale. All paths are assumed to be relative to /public/javascripts.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsLocalized path="jquery"/}
   * {@jsLocalized paths="dust, controllers/foo" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/javascripts/jquery_en_US.js"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust_en_US.js,/assets/javascripts/controllers/foo_en_US.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsLocalized = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatJs(paths, play.localizedJsAssetUrl, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource. If the path contains <locale> placeholders,
   * they will be replaced by the current locale. This is useful if you only want some of the js be localized. Unlike @js,
   * all paths are assumed to be relative to /public/ (ie, the /javascripts path is NOT assumed).
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsAsset path="foo"/}
   * {@jsAsset paths="bar, /home/dashboard/charts" id="bar"/}
   * {@jsAsset paths="lib/t8/<locale>/t8.js, /home/dashboard/charts" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/foo.js"></script>
   * <script type="text/javascript" src="/assets/bar.js,/home/dashboard/charts.js" id="bar"></script>
   * <script type="text/javascript" src="/assets/lib/t8/en_US/t8.js,/home/dashboard/charts.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsAsset = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.replaceWithLocale(play.getPathList(params));
    var jsPaths = Utils.map(paths, function(path) { return play.addExtension(path, '.js'); });

    return chunk.write(maybeDynamicConcatJs(jsPaths, play.assetUrl, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. The URL  will include the the current locale.
   * Unlike @jsLocalized, all paths are assumed to be relative to /public/ (ie, the /javascripts path is NOT assumed).
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsAssetLocalized path="foo"/}
   * {@jsAssetLocalized paths="bar, /home/dashboard/charts" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/foo_en_US.js"></script>
   * <script type="text/javascript" src="/assets/bar_en_US.js,/assets/home/dashboard/charts_en_US.js" id="bar"></script>

   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsAssetLocalized = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatJs(paths, play.localizedJsAssetUrl, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. All paths are assumed to be relative to
   * /public/stylesheets.
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@css path="main"/}
   * {@css paths="common, pages/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/stylesheets/main.css"/>
   * <link rel="stylesheet" href="/assets/stylesheets/common.css,/assets/stylesheets/pages/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.css = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatCss(paths, play.cssUrl, params));
  };

  /**
   * Writes a link tag with the URL for the SCSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. All paths are assumed to be relative to
   * /public/scss.
   *
   * The .scss extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@scss path="main"/}
   * {@scss paths="common, pages/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/scss/main.css"/>
   * <link rel="stylesheet" href="/assets/scss/common.css,/assets/scss/pages/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scss = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatCss(paths, play.scssUrl, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. Unlike @css, all paths are assumed to
   * be relative to /public/ (ie, the /css path is NOT assumed).
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@cssAsset path="home/main"/}
   * {@cssAsset paths="common, pages/dashboard/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/home/main.css"/>
   * <link rel="stylesheet" href="/assets/common.css,/assets/pages/dashboard/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.cssAsset = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);
    var cssPaths = Utils.map(paths, function(path) { return play.addExtension(path, '.css'); });

    return chunk.write(maybeDynamicConcatCss(cssPaths, play.assetUrl, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. The URL will include the current locale
   * appended. Unlike @css, all paths are assumed to be relative to /public/ (ie, the /css path is NOT assumed).
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@cssAssetLocalized path="home/main"/}
   * {@cssAssetLocalized paths="common, pages/dashboard/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/home/main_en_US.css"/>
   * <link rel="stylesheet" href="/assets/common_en_US.css,/assets/pages/dashboard/home_en_US.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.cssAssetLocalized = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(maybeDynamicConcatCss(paths, play.localizedCssAssetUrl, params));
  };

  /**
   * Writes an img tag with the URL specified in the 'path' parameter. The path is assumed to be relative to
   * /public/images. Any parameter other than 'path' will be passed unchanged to the img tag.
   *
   * Examples:
   *
   * {@img path="logo.png"/}
   * {@img path="icons/like.gif" alt="Like"/}
   *
   * Output:
   *
   * <img src="/assets/images/logo.png"/>
   * <img src="/assets/images/icons/like.gif" alt="Like"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.img = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = Utils.extend({src: play.imgUrl(params.path)}, play._.omit(params, ['path']));
    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Writes an img tag for an image hosted on scds with path 'path'. The image comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the img tag.
   *
   * Example:
   *
   * {@scdsImg path="img/foo/bar.png" class="baz"/}
   *
   * Output:
   *
   * <img src="/scds/common/u/img/foo/bar.png" class="baz"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsImg = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = Utils.extend({src: play.scdsDirectUnversionedUrl(params.path)}, play._.omit(params, ['path']));
    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Writes a script tag for JS hosted on scds with path 'path'. The JS comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the script tag.
   *
   * Example:
   *
   * {@scdsJs path="lib/foo/bar"/}
   *
   * Output:
   *
   * <script src="/scds/common/u/lib/foo/bar.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsJs = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.createScriptTag({src: play.scdsDirectUnversionedUrl(play.addExtension(params.path, '.js'))}, params));
  };

  /**
   * Builds a SCDS "unversioned" URL with the passed in 'path'.
   *
   * Example:
   *
   * {@scdsUrl path="lib/foo/bar.js"/}
   *
   * Output:
   *
   * /scds/common/u/lib/foo/bar.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.scdsDirectUnversionedUrl(params.path)));
  };

  /**
   * Writes a link tag for CSS hosted on scds with path 'path'. The CSS comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the link tag.
   *
   * Example:
   *
   * {@scdsCss path="foo/bar"/}
   *
   * Output:
   *
   * <link href="/scds/common/u/foo/bar.css"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsCss = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.createCssTag({href: play.scdsDirectUnversionedUrl(play.addExtension(params.path, '.css'))}, params));
  };

  /**
   * Endpoint for dynamic JS concatenation.
   * @type {string}
   */
  var SCDS_CONCAT_JS_URL = play.combineUrlPieces('concat/common', 'js');

  /**
   * Writes out a script tag with its src pointing to a dynamic concat group of
   * JS files.
   *
   * All params other than paths are applied directly to the generated script
   * tag.
   *
   * Example:
   * {@scdsJsConcat paths="one, two, three"/}
   *
   * Output:
   * <script type="text/javascript" src="/scds/concat/common/js?f=one&f=two&f=three"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.scdsJsConcat = function(chunk, context, bodies, params) {
    var paths,
      date = new Date(),
      versionDate = '&_v=' + date.getFullYear() + date.getMonth() + Math.ceil(date.getDate() / 7); // used for cache busting (busts ~weekly)

    Utils.assert(params, '@scdsJsConcatUrl called without params');
    Utils.assert(params.paths, '@scdsJsConcatUrl called without paths param');

    if (params.cacheBuster && params.cacheBuster.length) {
      versionDate = '&_v=' + params.cacheBuster;
      params = play._.omit(params, 'cacheBuster');
    }

    paths = play.getPathList(params);
    return chunk.write(play.createScriptTag({ src: play.scdsDirectUrl(play.buildUrl(SCDS_CONCAT_JS_URL, { 'f': paths })) + versionDate }, params));
  };

  /**
   * Writes an img tag with for the mpr image specified by the 'mediaId' or 'urn' params (if both are specified, mediaId
   * will be used). The size of the image is determined by the 'size' or 'width' and 'height' params (if both are
   * specified, size will be used). All other parameters will be passed unchanged to the img tag.
   *
   * Example:
   *
   * {@mprImg mediaId="/p/000/001/profile-photo.png" width="200" height="100" alt="My profile photo"/}
   * {@mprImg urn="urn:li:media:/p/1/000/000/1b4/1305a66.png" width="100" height="60" class="logo" /}
   *
   * Output:
   *
   * <img src="/mpr/mpr/shrink_80_80/p/000/001/profile-photo.png" width="200" height="100" alt="My profile photo"/>
   * <img src="/mpr/mpr/shrink_100_60/p/1/000/000/1b4/1305a66.png" width="100" height="60" class="logo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.mprImg = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var imageWidth = params.size || params.width,
      imageHeight = params.size || params.height,
      mprWidth = params.mprSize || params.mprWidth || imageWidth,
      mprHeight = params.mprSize || params.mprHeight || imageHeight,
      mprWithoutPadding = params.withoutPadding === 'true',
      attrs = play._.omit(params,
        ['mediaId', 'urn', 'size', 'width', 'height', 'imageWidth', 'imageHeight', 'mprHeight', 'mprWidth', 'mprSize', 'withoutPadding']);

    attrs[params.lazyLoad === 'true' ? 'data-delayed-url':'src'] = play.mprUrl(play.getMediaIdFromParams(params), mprWidth, mprHeight, mprWithoutPadding);

    if(imageWidth) {
      attrs.width = parseInt(imageWidth, 10) + '';
    }

    if(imageHeight) {
      attrs.height = parseInt(imageHeight, 10) + '';
    }

    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Writes an img tag with for the profile photo specified by the 'mediaId' or 'urn' params (if both are specified,
   * mediaId will be used). If neither 'mediaId' nor 'urn' can be found, shows the 'no-photo' silhouette instead. The
   * size of the image is determined by the 'size' or 'width' and 'height' params (if both are specified, size will be
   * used). All other parameters will be passed unchanged to the img tag.
   *
   * Examples:
   *
   * {@profileImg mediaId="/p/000/001/profile-photo.png" width="80" height="80" alt="My profile photo"/}
   * {@profileImg urn="urn:li:media:/p/1/000/000/2cc/004eb3a.jpg" width="100" height="100"/}
   * {@profileImg mediaId=undefinedVariable width="40" height="40" alt="Should show no photo"/}
   *
   * Output:
   *
   * <img src="/mpr/mpr/shrink_80_80//p/000/001/profile-photo.png" width="80" height="80" alt="My profile photo"/>
   * <img src="/mpr/mpr/shrink_100_100/p/1/000/000/2cc/004eb3a.jpg" width="100" height="100"/>
   * <img src="/scds/common/u/img/icon/icon_no_photo_no_border_40x40.png" width="40" height="40" alt="Should show no photo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.profileImg = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var imageWidth = params.size || params.width,
      imageHeight = params.size || params.height,
      mprWidth = params.mprSize || params.mprWidth || imageWidth,
      mprHeight = params.mprSize || params.mprHeight || imageHeight,
      mprWithoutPadding = params.withoutPadding === 'true',
      mediaId = play.getMediaIdFromParams(params),
      src = mediaId ? play.mprUrl(mediaId, mprWidth, mprHeight, mprWithoutPadding) : play.noPhotoUrl(imageWidth, imageHeight),
      attrs = Utils.extend({src: src}, play._.omit(params,
        ['mediaId', 'urn', 'size', 'width', 'height', 'mprHeight', 'mprWidth', 'mprSize']));

    if(imageWidth) {
      attrs.width = parseInt(imageWidth, 10) + '';
    }

    if(imageHeight) {
      attrs.height = parseInt(imageHeight, 10) + '';
    }

    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Writes an img tag for the image specified by the urn or mediaId directly from /media (not from /mpr).
   *
   * Examples:
   *
   * {@mediaImg mediaId="/p/000/001/profile-photo.png" alt="My profile photo"/}
   * {@mediaImg urn="urn:li:media:/p/1/000/000/196/3283598.png" class="logo" /}
   *
   * Output:
   *
   * <img src="/media/p/000/001/profile-photo.png" alt="My profile photo"/>
   * <img src="/media/p/1/000/000/196/3283598.png" class="logo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.mediaImg = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = Utils.extend({src: play.mediaUrl(play.getMediaIdFromParams(params))}, play._.omit(params, ['mediaId', 'urn']));
    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Builds a URL for the image specified by the urn or mediaId directly from /media (not from /mpr).
   *
   * Examples:
   *
   * {@mediaUrl mediaId="/p/000/001/profile-photo.png" /}
   * {@mediaUrl urn="urn:li:media:/p/1/000/000/196/3283598.png" /}
   *
   * Output:
   *
   * "/media/p/000/001/profile-photo.png"
   * "/media/p/1/000/000/196/3283598.png"
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.mediaUrl = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.mediaUrl(play.getMediaIdFromParams(params))));
  };

  /**
   * Write the URL specified by the 'alias' and (optional) 'arg0', 'arg1', etc. parameters.
   *
   * Examples:
   *
   * {@url alias="controllers.HelloWorld.index"/}
   * {@url alias="controllers.Helpers.threeParams" arg0=firstName arg1=lastName arg2=age/}
   * {@url alias="controllers.Helpers.index" track="some-track"/}
   *
   * Output:
   *
   * /dust-sample
   * /dust-sample/helpers/threeParams?firstName=Jim&lastName=Brikman&age=28
   * /dust-sample?trk=some-track
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.url = function(chunk, context, bodies, params) {
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.url(params, params.absolute === 'true')));
  };

  /**
   * Adds a key-value pair to the querystring of a URL, overwriting the
   * existing value if one by that name already exists.
   *
   * Examples:
   * {@addQueryParameter url="/foo" key="bar" value="baz"/}
   * {@addQueryParameter url="/foo?bar=baz" key="biz" value="buzz"/}
   * {@addQueryParameter url="/foo?bar=baz" key="bar" value="biz"/}
   *
   * Output:
   * /foo?bar=baz
   * /foo?bar=baz&biz=buzz
   * /foo?bar=biz
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.addQueryParameter = function(chunk, context, bodies, params) {
    Utils.assert(params.url, '@addQueryParameter called without URL param');
    Utils.assert(params.key, '@addQueryParameter called without key name');
    Utils.assert(params.value, '@addQueryParameter called without value for key');
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.addQueryParameter(params.url, params.key, params.value)));
  };

  /**
   * Adds a tracking code to a URL.
   *
   * Example:
   * {@addTrackingCode url="/bar" code="some-track-code"/}
   *
   * Output:
   * /bar?trk=some-track-code
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.addTrackingCode = function(chunk, context, bodies, params) {
    Utils.assert(params.url, '@addTracking called without url');
    Utils.assert(params.code, '@addTracking called without tracking code');
    params = dust.helpers.tapAll(params, chunk, context);

    return dust.helpers.addQueryParameter(chunk, context, bodies, {
      key: 'trk',
      value: params.code,
      url: params.url
    });
  };

})(play, LI, dust, sc);


(function() {var require = function(moduleName) {  return this[moduleName];}
function exportAsGlobal(exports, require) {'use strict';

var t8 = require('t8');
t8 = ('default' in t8 ? t8['default'] : t8);

"use strict";

var xmessage_reader__CHAR_BACK_SLASH = "\\";

/**
 * A simple string reader, similar to an iterator or input stream. Allows the
 * character-by-character traversal of a string until there is not more input.
 *
 * @constructs Reader
 * @param {String} input - the string to traverse
 */
function Reader(input) {
  this._input = input;
  this._index = 0;
  this._last = null;
  this.next = this.next.bind(this);
  this.consume = this.consume.bind(this);
}

/**
 * Returns the next character in the traversal. Does not advance the cursor.
 * When there are no more characters to be read this returns Reader.END_TOKEN.
 *
 * @see Reader.prototype.consume
 * @return {String|Null} a single character value or null if at the end
 */
Reader.prototype.next = function () {
  if (this._index < this._input.length) {
    return this._input[this._index];
  }

  return null;
};

/**
 * Consumes the current charcter and advances the Reader's iterator forward by
 * one character.
 */
Reader.prototype.consume = function () {
  this._last = this._input[this._index];
  this._index++;
};

/**
 * Returns whether the current character is escaped or not.
 *
 * @return {Boolean}
 */
Reader.prototype.isEscaped = function () {
  return this._last === xmessage_reader__CHAR_BACK_SLASH;
};

Reader.prototype.constructor = Reader;

var xmessage_reader = Reader;

"use strict";

var CHAR_LEFT_BRACE = "{",
    CHAR_RIGHT_BRACE = "}",
    CHAR_COMMA = ",",
    CHAR_COLON = ":",
    CHAR_PIPE = "|",
    CHAR_ZERO = "0",
    CHAR_SINGLE_QUOTE = "'",
    parser__CHAR_BACK_SLASH = "\\",
    EMPTY_STRING = "",
    REGEX_LETTERS = /[a-zA-Z]/,
    REGEX_NUMBERS_LEADING = /[1-9]/,
    REGEX_NUMBERS_TRAILING = /[0-9]/,
    REGEX_LETTERS_OR_NUMBERS = /[a-zA-Z0-9]/,
    REGEX_KEYWORD_ALLOWED_CHARS = /[a-zA-Z0-9_\-~.|\[\]\/]/,
    REGEX_STYLE_DELIMITER_DEFAULT = /[#]/,
    REGEX_STYLE_DELIMITER_CHOICE = /[#+<]/,
    SPECIAL_STYLE_PARSERS = {},
    SPECIAL_DELIMITER_PATTERNS = {},
    KEEP_DELIMITER_FLAGS = {},
    KEEP_STYLE_ORDER_FLAGS = {};

SPECIAL_DELIMITER_PATTERNS.choice = REGEX_STYLE_DELIMITER_CHOICE;
KEEP_DELIMITER_FLAGS.choice = true;
KEEP_STYLE_ORDER_FLAGS.choice = true;

/**
 * Throws an error. Convenience method to save bytes.
 */
function throwError(message) {
  throw new Error(message);
}

/**
 * Given an input string, extracts the segments of simple text as well as placeholders.
 * Text segments are returned as plain Strings. Placeholders are returned as Objects with
 * a `text` property containing the unparsed placeholder text. The validity of placeholders
 * is not checked.
 *
 * Throws exceptions if unexpected or unmatched "{" or "}" characters are found.
 *
 * Example:
 *     extractMessage("Hello, {:name}!") => ["Hello, ", { text: ":name" }]
 *
 * @param {String} input
 * @return {Object[]}
 */
function extractMessage(input) {
  var segments = [],
      curlyStack = [],
      isInsideQuote = false,
      buffer = [],
      reader = new xmessage_reader(input),
      currentFn = reader.next,
      nextFn = reader.consume;

  while (currentFn()) {
    // SPECIAL CASE: Skip over back slashes unless they're escaped.
    if (currentFn() === parser__CHAR_BACK_SLASH && !reader.isEscaped()) {
      nextFn();
      continue;
    }

    // SPECIAL CASE: If a character is escaped push it in the buffer and move on to the next one.
    if (reader.isEscaped()) {
      buffer.push(currentFn());
      nextFn();
      continue;
    }

    // Check for single-quoted ranges of text.
    if (curlyStack.length === 0) {
      if (currentFn() === CHAR_SINGLE_QUOTE) {
        isInsideQuote = !isInsideQuote;
        nextFn();

        // Flush buffer into a segment.
        if (buffer.length > 0) {
          segments.push(buffer.join(EMPTY_STRING));
          buffer.length = 0;
        }
      }
    }

    if (!isInsideQuote) {
      // SPECIAL CASE: Found a closing curly but not inside a placeholder
      if (currentFn() === CHAR_RIGHT_BRACE && !reader.isEscaped() && curlyStack.length === 0) {
        throwError("Unexpected \"}\"");
      } else if (currentFn() === CHAR_LEFT_BRACE) {
        // Handle nested messages.
        // If we just found the beginning of a message...
        if (curlyStack.length === 0) {
          // Flush buffer into a segment.
          if (buffer.length > 0) {
            segments.push(buffer.join(EMPTY_STRING));
            buffer.length = 0;
          }
        } else {
          // Only push curlies into the buffer if they're nested.
          buffer.push(currentFn());
        }

        curlyStack.push(currentFn());
        nextFn();
      } else if (currentFn() === CHAR_RIGHT_BRACE) {
        curlyStack.pop();

        // If we just found the end of a message...
        if (curlyStack.length === 0) {
          // Flush buffer into a segment.
          if (buffer.length > 0) {
            segments.push({
              text: buffer.join(EMPTY_STRING)
            });
            buffer.length = 0;
          } else {
            throwError("Unexpected end of placeholder (found no content)");
          }
        } else {
          // Only push curlies into the buffer if they're nested.
          buffer.push(currentFn());
        }

        nextFn();
      } else {
        // When inside a quoted range we just collect all text.
        if (currentFn()) {
          buffer.push(currentFn());
        }

        nextFn();
      }
    } else {
      // When outside a quoted range we just collect all text.
      if (currentFn()) {
        buffer.push(currentFn());
      }

      nextFn();
    }
  }

  if (curlyStack.length !== 0) {
    throwError("Unexpected end of placeholder (unmatched \"{\")");
  }

  if (buffer.length > 0) {
    segments.push(buffer.join(EMPTY_STRING));
    buffer.length = 0;
  }

  return segments;
}

/**
 * Expects to find and index at the beginning of the Reader. Reads until
 * it finds the end of an index or the Reader is done.
 *
 * Throws if an index could not be parsed.
 *
 * @return {Object} an object describing the index
 */
function expectIndex(currentFn, nextFn) {
  var indexBuffer = [],
      keywordBuffer = [];

  // The regexp /^((0|[1-9][0-9]*)|:([a-zA-Z]+)|((0|[1-9][0-9]*)(:([a-zA-Z]+))))$/
  // could potentially replace this code. Needs more investigation.

  if (currentFn() === CHAR_ZERO) {
    indexBuffer.push(currentFn());
    nextFn();

    if (currentFn() && (currentFn() !== CHAR_COMMA && currentFn() !== CHAR_COLON)) {
      throwError("Could not parse index; expected \":\" or end of identifier but found \"" + currentFn() + "\"");
    }
  } else if (currentFn() && REGEX_NUMBERS_LEADING.test(currentFn())) {
    while (currentFn() && REGEX_NUMBERS_TRAILING.test(currentFn())) {
      indexBuffer.push(currentFn());
      nextFn();
    }
  }

  if (currentFn() === CHAR_COLON) {
    nextFn(); // Consume the ":"

    // First character of a keyword has to be a letter or a number
    if (currentFn() && REGEX_LETTERS_OR_NUMBERS.test(currentFn())) {
      keywordBuffer.push(currentFn());
      nextFn();
    } else {
      throwError("Expected letter (a-zA-Z) or number (0-9) but found \"" + currentFn() + "\"");
    }

    // Parse keyword
    while (currentFn() && REGEX_KEYWORD_ALLOWED_CHARS.test(currentFn())) {
      keywordBuffer.push(currentFn());
      nextFn();
    }
  } else {
    if (indexBuffer.length === 0 && currentFn()) {
      throwError("Unexpected character; expected \":\" but found \"" + currentFn() + "\"");
    }
  }

  if (currentFn() === CHAR_COMMA || !currentFn()) {
    indexBuffer = parseInt(indexBuffer.join(EMPTY_STRING), 10);

    if (isNaN(indexBuffer)) {
      indexBuffer = null;
    }

    keywordBuffer = keywordBuffer.join(EMPTY_STRING) || null; // If there was no keyword make sure it's null
  } else {
    throwError("Unexpected character; expected \",\" or end of identifier but found \"" + currentFn() + "\"");
  }

  return {
    number: indexBuffer,
    keyword: keywordBuffer
  };
}

/**
 * Expects to find a placeholder type. Throws if invalid characters are found.
 */
function expectType(currentFn, nextFn) {
  var buffer = [];

  if (!currentFn()) {
    throwError("Unable to parse type. Expected letter (a-zA-Z) but found end of identifier after \",\"");
  } else {
    while (currentFn() && REGEX_LETTERS.test(currentFn())) {
      buffer.push(currentFn());
      nextFn();
    }
  }

  return buffer.join(EMPTY_STRING);
}

/**
 * Expects to find a style. A style may be a key only, or a key/value
 * pair. For key/value pairs the delimiter is captured as well.
 */
function expectStyleTuple(currentFn, nextFn, parseMode) {
  var DELIMITER_PATTERN = SPECIAL_DELIMITER_PATTERNS[parseMode] || REGEX_STYLE_DELIMITER_DEFAULT,
      KEEP_DELIMITER = KEEP_DELIMITER_FLAGS[parseMode] || false;

  var buffer = [],
      delimiter = null,
      key = null,
      value = undefined,
      result = {};

  while (currentFn()) {
    if (DELIMITER_PATTERN.test(currentFn())) {
      // We found the delimiter. If they key hasn't been discovered
      // yet then this is an error.
      if (buffer.length === 0) {
        throwError("Error parsing style key/value. Found delimiter \"" + currentFn() + "\" but expected key.");
      } else {
        if (key === null) {
          key = buffer.join(EMPTY_STRING);
          buffer.length = 0;
          delimiter = currentFn();
          nextFn(); // Eat the delimiter.
        }
      }
    }

    buffer.push(currentFn());
    nextFn();
  }

  if (delimiter === null) {
    // This means we never found a delimiter, so the buffer is filled
    // with a key.
    key = buffer.join(EMPTY_STRING);
    buffer.length = 0;
  } else {
    // Since we found a delimiter, anything remaining in the buffer
    // must be the value.
    value = buffer.join(EMPTY_STRING);
    buffer.length = 0;
  }

  result.key = key || null;
  result.value = value || null;

  // Choice is really the only placeholder that needs delimiter, so we omit it
  // unless configured to preserve it.
  if (KEEP_DELIMITER) {
    result.delimiter = delimiter || null;
  }

  return result;
}

function expectDelimitedStyleList(currentFn, nextFn, parseMode, styleIndexOffset) {
  var KEEP_STYLE_ORDER = KEEP_STYLE_ORDER_FLAGS[parseMode] || false;

  var curlyStack = [],
      // A stack to keep track of nested xmessages
  parsedStyles = {},
      unprocessedStyles = [],
      buffer = [];

  styleIndexOffset = styleIndexOffset || 0;

  // Break the stream into chunks based on the style delimiter. We'll make a
  // second pass later to determine if they're simple or complex styles.
  while (currentFn()) {
    if (currentFn() === CHAR_LEFT_BRACE) {
      curlyStack.push(currentFn());
    } else if (currentFn() === CHAR_RIGHT_BRACE) {
      curlyStack.pop();
    }

    if (curlyStack.length === 0 && currentFn() === CHAR_PIPE) {
      // If we found a pipe that should mean that at least a style name has been
      // pushed into the buffer. If the buffer is empty then something is wrong.
      if (buffer.length === 0) {
        throwError("Unexpected \"" + CHAR_PIPE + "\" in style list.");
      }

      // Push watever is in the buffer into the style buffer as a string. We'll
      // try and work out if it is a key/value or just a straight value later.
      unprocessedStyles.push(buffer.join(EMPTY_STRING));

      // Clear the buffer.
      buffer.length = 0;

      // Eat the delimiter character.
      nextFn();
    } else {
      // Consume the character and push it into the buffer because it's either
      // a style name or key/value pair.
      buffer.push(currentFn());
      nextFn();
    }
  }

  // Take care of the last style or a singleton style.
  if (buffer.length > 0) {
    unprocessedStyles.push(buffer.join(EMPTY_STRING));
    buffer.length = 0;
  }

  // Now we take a second pass and parse each style individually.
  // First we map each item into a style object, then we merge them
  // into a map and include their order (index).
  unprocessedStyles.map(function (style) {
    var reader = new xmessage_reader(style);
    return expectStyleTuple(reader.next, reader.consume, parseMode);
  }).forEach(function (style, index) {
    // Most placeholders don't need to know the order of the styles. Only
    // "choice" really cares, so omit it for unless configured to keep it.
    if (KEEP_STYLE_ORDER) {
      style.order = index + styleIndexOffset;
    }

    if (!parsedStyles.hasOwnProperty(style.key)) {
      parsedStyles[style.key] = style;
    } else {
      throwError("Found duplicate style key \"" + style.key + "\". Styles must have unique names.");
    }
  });

  return parsedStyles;
}

function expectRemainderAsStyle(currentFn, nextFn /* , parseMode */) {
  var buffer = [],
      value = undefined,
      result = {};

  while (currentFn()) {
    buffer.push(currentFn());
    nextFn();
  }

  value = buffer.join(EMPTY_STRING);

  result[value] = {
    key: value,
    value: null
    /* It's unlikely that delimiter or order will be useful, so omit them. */
    /*
    delimiter: null,
    order: 0
    */
  };

  return result;
}

SPECIAL_STYLE_PARSERS.choice = function expectChoiceStyleList(currentFn, nextFn, parseMode) {
  return expectDelimitedStyleList(currentFn, nextFn, parseMode);
};

SPECIAL_STYLE_PARSERS.date = function expectDateStyleList(currentFn, nextFn, parseMode) {
  return expectRemainderAsStyle(currentFn, nextFn, parseMode);
};

SPECIAL_STYLE_PARSERS.number = function expectDateStyleList(currentFn, nextFn, parseMode) {
  return expectRemainderAsStyle(currentFn, nextFn, parseMode);
};

SPECIAL_STYLE_PARSERS.list = function expectListStyleList(currentFn, nextFn, parseMode) {
  var remainder = [],
      remainderText,
      parameters,
      styleParameters,
      styleIterator,
      key;

  while (currentFn()) {
    remainder.push(currentFn());
    nextFn();
  }

  remainderText = remainder.join(EMPTY_STRING);

  if (/^(name$|name\,)/.test(remainderText)) {
    parameters = {
      name: {
        key: "name",
        value: "name"
        /* It's unlikely that delimiter or order will be useful, so omit them. */
        /*
        delimiter: null,
        order: -1
        */
      }
    };

    styleIterator = new xmessage_reader(remainderText.substr("name".length));
  } else if (/^(text$|text\,)/.test(remainderText)) {
    parameters = {
      text: {
        key: "text",
        value: "text"
        /* It's unlikely that delimiter or order will be useful, so omit them. */
        /*
        delimiter: null,
        order: -1
        */
      }
    };

    styleIterator = new xmessage_reader(remainderText.substr("text".length));
  } else {
    throwError("Invalid style list for List placeholder.");
  }

  // If there is anything left to do, we'll parse the styles as normal.
  // Then we merge them.
  if (styleIterator) {
    // Eat any comma and parse the rest of the string.
    if (styleIterator.next() === CHAR_COMMA) {
      styleIterator.consume();

      styleParameters = expectDelimitedStyleList(styleIterator.next, styleIterator.consume, parseMode);

      for (key in styleParameters) {
        if (styleParameters.hasOwnProperty(key)) {
          // Only copy keys that don't replace the list type.
          if (!/^(name$|name\,)/.test(key) && !/^(text$|text\,)/.test(key)) {
            parameters[key] = styleParameters[key];
          }
        }
      }
    }
  }

  return parameters;
};

function expectParameters(currentFn, nextFn, parseMode) {
  if (SPECIAL_STYLE_PARSERS[parseMode]) {
    return SPECIAL_STYLE_PARSERS[parseMode](currentFn, nextFn, parseMode);
  }

  return expectDelimitedStyleList(currentFn, nextFn, parseMode);
}

function expectPlaceholder(currentFn, nextFn) {
  var index = null,
      type = null,
      parameters = null;

  if (currentFn()) {
    index = expectIndex(currentFn, nextFn);

    if (currentFn() === CHAR_COMMA) {
      nextFn(); // Consume the "," and extract the type
      type = expectType(currentFn, nextFn);

      if (currentFn() === CHAR_COMMA) {
        nextFn(); // Consume the "," and extract the parameters
        parameters = expectParameters(currentFn, nextFn, type);
      }
    } else {
      type = "simple";
    }
  } else {
    throwError("Error parsing placeholder. Unexpected end of input.");
  }

  if (currentFn()) {
    throwError("Unexpected character \"" + currentFn() + "\".");
  }

  return {
    index: index,
    type: type,
    parameters: parameters
  };
}

function parsePlaceholder(str) {
  var reader = new xmessage_reader(str);
  return expectPlaceholder(reader.next, reader.consume);
}

/**
 * Recursively parses a string into a message. Handles nested messages and segments of text.
 */
function parseMessage(str) {
  return extractMessage(str).map(function (textOrPlaceholder) {
    if (typeof textOrPlaceholder === "string") {
      return textOrPlaceholder;
    } else {
      return parsePlaceholder(textOrPlaceholder.text);
    }
  }).map(function parseSubexpressions(textOrPlaceholder) {
    if (typeof textOrPlaceholder === "string") {
      return textOrPlaceholder;
    } else {
      if (typeof textOrPlaceholder.parameters === "object") {
        var key = undefined,
            value = undefined;

        for (key in textOrPlaceholder.parameters) {
          if (textOrPlaceholder.parameters.hasOwnProperty(key)) {
            value = textOrPlaceholder.parameters[key];

            if (value.value) {
              value.value = parseMessage(value.value);
            }
          }
        }
      }

      return textOrPlaceholder;
    }
  });
}

var parser = parseMessage;

"use strict";

var locale = "default",
    ruleSets = {
  "default": {
    list: {
      start: "{0}, {1}",
      middle: "{0}, {1}",
      end: "{0}, {1}",
      "2": "{0}, {1}"
    },
    number: {
      percent: "{0}%",
      integer: {
        separator: ","
      }
    },
    suffix: {}
  } , 'en_US': {"date":{"long":"LL","medium":"ll","short":"M/D/YY","full":"EEEE, MMMM D, YYYY"},"list":{"2":"{0} and {1}","start":"{0}, {1}","middle":"{0}, {1}","end":"{0}, and {1}"},"number":{"percent":"{0}%"},"possessive":{"fallback":"’s","rules":{".*[Ss]$":"’",".*[A-RT-Z]$":"’S",".*[a-rt-z]$":"’s"}},"time":{"short":"h:mm a","medium":"h:mm:ss a","long":"h:mm:ss a ZZ","full":"h:mm:ss a ZZ"}}
},
    placeholder_placeholder__evaluator = function evaluator() {};

/**
 * Sets the placeholder evaluator function. This function will be used to
 * evaluate placeholders, converting them into string values.
 *
 * This is structured this way in order to prevent circular references. The
 * default evaluator is a no-op function.
 *
 * @param {Function} fn - the function to use for evaluation
 */
function setEvaluator(fn) {
  placeholder_placeholder__evaluator = fn;
}

function getRules(locale, type) {
  var ruleSet = ruleSets[locale];

  if (type && ruleSet) {
    return ruleSet[type];
  }

  return ruleSet;
}

function addRuleSet(locale, rules) {
  ruleSets[locale] = rules; // TODO: Consider something like _.extend() here instead.
}

/**
 * Gets the value of the style with the name `key` in the specified placeholder,
 * or `undefined` if the style could not be found.
 *
 * @param  {Object} placeholder - a well-formed placeholder object
 * @param  {String} key - the name of the style to get
 * @return {String|Array}
 */
function getStyle(placeholder, key) {
  var result;

  if (placeholder.parameters) {
    result = placeholder.parameters[key];
  }

  if (result) {
    return result.value;
  }

  return undefined;
}

/**
 * Gets the style with the index `index` in the specified placeholder,
 * or `undefined` if the style could not be found.
 *
 * This function returns the entire style object, not just the value.
 *
 * @param  {Object} placeholder - a well-formed placeholder object
 * @param  {Number} index - the 0-based index of the style to get
 * @return {Object}
 */
function getStyleByIndex(placeholder, index) {
  var styles = placeholder.parameters,
      name;

  if (styles) {
    for (name in styles) {
      if (styles.hasOwnProperty(name)) {
        if (styles[name].order === index) {
          return styles[name];
        }
      }
    }
  }

  return undefined;
}

/**
 * Evaluates a particular style, specified by `key`, from the given placeholder
 * using the specified locale. Styles may contain entire XMessages, that is,
 * nested placeholders. Those will be evaluated using the `evaluator` function.
 *
 * If no style is found with the name specified by `key`, `undefined` is the
 * return value.
 *
 * @param  {Object} placeholder - a well-formed placeholder object
 * @param  {String} key - the name of the style to evaluate
 * @param  {Array} context - an array of values, may be primitives or objects
 * @param  {String} locale - the locale
 * @return {String}
 * @see setEvaluator
 */
function evaluateStyle(placeholder, key, context, locale) {
  var maybeStyle = getStyle(placeholder, key),
      result;

  if (maybeStyle) {
    result = maybeStyle.map(function (segment) {
      if (segment instanceof Object) {
        return placeholder_placeholder__evaluator(segment, context, locale);
      }

      return segment.toString();
    }).join("");
  }

  return result;
}

/**
 * Tries to resolve the value of a placeholder's index from the supplied
 * context. For example, an index may be {1}, {:name}, {1:name}, etc. If the
 * index doesn't specify a number, 0 will be used.
 *
 * @param  {Object} placeholder - a well-formed placeholder object
 * @param  {Array} context - an array of values, may be primitives or objects
 * @return {*}
 */
function resolveIndex(placeholder, context) {
  var result = null;

  if (placeholder && placeholder.index && context) {
    var index = placeholder.index;

    if (typeof index.number === "number") {
      result = context[index.number];
    } else if (index.number === null) {
      result = context[0]; // Default to the 0th element if no number is specified.
    }

    if (result !== undefined && result !== null) {
      if (typeof index.keyword === "string") {
        result = result[index.keyword];
      }
    } else {
      result = undefined;
    }
  }

  return result;
}

"use strict";

var anchor_placeholder = evaluateAnchorPlaceholder;
var anchor_placeholder__STRING_TEXT = "text",
    anchor_placeholder__STRING_TITLE = "title",
    STRING_ID = "id",
    STRING_CLASS = "class";
function evaluateAnchorPlaceholder(placeholderAst, context, locale) {
  var resolved = resolveIndex(placeholderAst, context),
      isMap = resolved && typeof resolved === "object",
      hrefAttr = isMap ? resolved.href : resolved,
      idAttr = isMap ? resolved.id : null,
      classAttr = isMap ? resolved["class"] : null,
      textStyle = evaluateStyle(placeholderAst, anchor_placeholder__STRING_TEXT, context, locale),
      titleStyle = evaluateStyle(placeholderAst, anchor_placeholder__STRING_TITLE, context, locale),
      result = "<a";

  if (hrefAttr) {
    result += " href=\"" + hrefAttr + "\"";
  }

  if (titleStyle) {
    result += " title=\"" + titleStyle + "\"";
  }

  if (idAttr) {
    result += " id=\"" + idAttr + "\"";
  }

  if (classAttr) {
    result += " class=\"" + classAttr + "\"";
  }

  result += ">" + textStyle + "</a>";

  return result;
}

"use strict";

var boolean_placeholder = evaluateBooleanPlaceholder;
var boolean_placeholder__STRING_TRUE = "true",
    boolean_placeholder__STRING_FALSE = "false";
function evaluateBooleanPlaceholder(placeholderAst, context, locale) {
  var indexValue = resolveIndex(placeholderAst, context),
      result = "";

  if (indexValue === true || indexValue === boolean_placeholder__STRING_TRUE) {
    result = evaluateStyle(placeholderAst, boolean_placeholder__STRING_TRUE, context, locale);
  } else if (indexValue === false || indexValue === boolean_placeholder__STRING_FALSE) {
    result = evaluateStyle(placeholderAst, boolean_placeholder__STRING_FALSE, context, locale);
  } else {
    // This is an error. Truthy values are not supported.
    throw new Error("Invalid argument for BooleanPlaceholder. Expected boolean or \"true\" or \"false\" but found \"" + indexValue + "\"");
  }

  return result;
}

"use strict";

var choice_placeholder = evaluateChoicePlaceholder;
var choice_placeholder__REGEX_NUMBERS = /-?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/,
    choice_placeholder__REGEX_CATEGORIES = /^(zero|singular|dual|few|many|plural|other)$/;
function evaluateChoicePlaceholder(placeholderAst, context, locale) {
  var indexValue = resolveIndex(placeholderAst, context),
      result = "",
      stylesAsRules,
      styleToRender,
      chooserResult,
      styles,
      params = placeholderAst.parameters,
      currentStyle,
      key;

  styles = [];

  for (key in params) {
    if (params.hasOwnProperty(key)) {
      currentStyle = params[key];
      styles[currentStyle.order] = currentStyle;
    }
  }

  if (!choice_placeholder__REGEX_NUMBERS.test(indexValue)) {
    throw new Error("Invalid context value for ChoicePlaceholder. \"" + indexValue + "\" is not a valid number.");
  }

  stylesAsRules = styles.map(function (style, index) {
    var result = {};

    if (choice_placeholder__REGEX_CATEGORIES.test(style.key)) {
      result.category = style.key;
      result.comparison = "eq";
      result.text = index.toString();
    } else {
      // Assume number
      result.arg = parseFloat(style.key);

      switch (style.delimiter) {
        case "<":
          result.comparison = "gt";
          break;
        case "+":
          result.comparison = "gte";
          break;
        case "#":
          result.comparison = "gte";
          break;
        default:
          result.comparison = "eq";
          break;
      }
      result.text = index.toString();
    }

    return result;
  });

  chooserResult = new t8.Chooser().format(parseFloat(indexValue), stylesAsRules, locale);

  if (chooserResult !== undefined) {
    styleToRender = styles[parseInt(chooserResult, 10)];
    result = evaluateStyle(placeholderAst, styleToRender.key, context, locale);
  }

  return result;
}

"use strict";

var map_placeholder = evaluateMapPlaceholder;
var DEFAULT_TEXT = "DEFAULT_TEXT";
function evaluateMapPlaceholder(placeholderAst, context, locale) {
  var indexValue = resolveIndex(placeholderAst, context),
      style,
      result = "";

  if (indexValue !== undefined) {
    indexValue = indexValue.toString();
    style = getStyle(placeholderAst, indexValue);

    if (style) {
      result = evaluateStyle(placeholderAst, indexValue, context, locale);
    } else {
      result = evaluateStyle(placeholderAst, DEFAULT_TEXT, context, locale);
    }
  } else {
    result = evaluateStyle(placeholderAst, DEFAULT_TEXT, context, locale);
  }

  return result;
}

"use strict";

function formatPossessive(value, locale) {
  var result = "",
      key = undefined;

  if (value !== undefined) {
    var possessiveRules = getRules(locale, "possessive");

    if (possessiveRules) {
      var suffixFromRules = undefined;

      if (possessiveRules.rules) {
        for (key in possessiveRules.rules) {
          if (possessiveRules.rules.hasOwnProperty(key)) {
            var suffixValue = possessiveRules.rules[key];
            var regex = new RegExp(key);

            if (regex.test(value)) {
              suffixFromRules = suffixValue;
            }
          }
        }
      }

      if (suffixFromRules !== undefined) {
        result = suffixFromRules;
      } else if (possessiveRules.fallback) {
        result = possessiveRules.fallback;
      }
    }
  }

  return result;
}

"use strict";

var possessive_placeholder = evaluatePossessivePlaceholder;
function evaluatePossessivePlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      result = "";

  if (resolvedValue !== undefined) {
    result = formatPossessive(resolvedValue, locale);
  }

  return result;
}

"use strict";

var simple_placeholder = evaluateSimplePlaceholder;
function evaluateSimplePlaceholder(placeholderAst, context /* , locale */) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      output;

  if (resolvedValue === undefined) {
    output = "{" + (placeholderAst.index.number !== null ? placeholderAst.index.number : "") + (placeholderAst.index.keyword !== null ? ":" + placeholderAst.index.keyword : "") + "}";
  } else {
    output = resolvedValue;
  }

  return output;
}

/**
 * Formats a value with a prefix or suffix provided by a set of styles.
 * @param {Object} styles - a placeholder's style map
 * @param {String} value - the value to decorate
 */
"use strict";

var prefix_suffix_formatter = formatPrefixOrSuffix;
var STYLE_PREFIX = "prefix",
    STYLE_SUFFIX = "suffix";
function formatPrefixOrSuffix(styles, value) {
  if (styles) {
    var maybePrefix = styles[STYLE_PREFIX];
    var maybeSuffix = styles[STYLE_SUFFIX];

    // TODO: Could prefix or suffix ever be a placeholder, or only values allowed?
    if (maybePrefix) {
      value = maybePrefix.value + value;
    }

    if (maybeSuffix) {
      value = value + maybeSuffix.value;
    }
  }

  return value;
}

"use strict";

var text_placeholder = evaluateTextPlaceholder;
function evaluateTextPlaceholder(placeholderAst, context /* , locale */) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      output;

  if (resolvedValue === undefined) {
    output = "{" + (placeholderAst.index.number !== null ? placeholderAst.index.number : "") + (placeholderAst.index.keyword !== null ? ":" + placeholderAst.index.keyword : "") + "}";
  } else {
    output = resolvedValue;
  }

  output = prefix_suffix_formatter(placeholderAst.parameters, output);

  return output;
}

/**
 * Applies placeholder substitution on a string template.
 * @param {String} template - the template containing placeholders {0} and {1}, etc.
 * @return {String} an interpolated template
 */
"use strict";

function stringFormat(template /* , args... */) {
  // Shamelessly copied from: http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format/4673436#4673436
  var args = Array.prototype.slice.call(arguments, 1);
  return template.replace(/{(\d+)}/g, function (match, number) {
    return typeof args[number] !== "undefined" ? args[number] : match;
  });
}

function findFirstProperty(properties, object) {
  var length, index;

  for (index = 0, length = properties.length; index < length; ++index) {
    var key = properties[index];

    if (object.hasOwnProperty(key)) {
      return object[key];
    }
  }

  return null;
}

function defaults(target, source) {
  var key;

  if (target && source) {
    for (key in source) {
      if (source.hasOwnProperty(key)) {
        if (!target.hasOwnProperty(key)) {
          target[key] = source[key];
        }
      }
    }
  }

  return target;
}

"use strict";

var number_placeholder = evaluateNumberPlaceholder;
function formatPercentage(template, value) {
  value = Math.floor(value * 100);
  return stringFormat(template, value);
}

/**
 * Gets the set of locale-specific rules to use for number formatting.
 */
function getFormattingRules(locale) {
  return defaults(defaults({}, getRules(locale, "number")), getRules("default", "number"));
}
function evaluateNumberPlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      styles = placeholderAst.parameters,
      rules = getFormattingRules(locale),
      output;

  if (resolvedValue !== undefined) {
    if (styles) {
      if (styles.integer) {
        output = new t8.NumberFormatter().format(Math.floor(resolvedValue), locale);
      } else if (styles.currency) {
        output = new t8.CurrencyFormatter().format(resolvedValue, undefined, locale);
      } else if (styles.percent) {
        output = formatPercentage(rules.percent, resolvedValue);
      }
    } else {
      output = new t8.NumberFormatter().format(resolvedValue, locale);
    }
  }

  return output;
}

"use strict";

var date_placeholder = evaluateDatePlaceholder;
function evaluateDatePlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      output = "",
      style,
      pattern,
      t8Impl,
      rules = getRules(locale, "date");

  style = getStyleByIndex(placeholderAst, 0);

  if (!style) {
    style = "medium";
  } else {
    style = style.key;
  }

  pattern = rules[style];

  if (!pattern) {
    // Might be a custom format...
    pattern = style; // Need to replace single quotes with square brackets for Moment.

    if (pattern) {
      // It's a custom pattern for sure, so we need to do some doctoring on the pattern.
      pattern = pattern.replace(/Z/, "ZZ"); // Since moment.js renders "Z" as XX:XX and "ZZ" as XXXX
    }
  }

  if (pattern) {
    t8Impl = new t8.DateFormatter();
    output = t8Impl.format(new Date(resolvedValue), locale, pattern, false);
  }

  return output;
}

"use strict";

var time_placeholder = evaluateTimePlaceholder;
function evaluateTimePlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      output = "",
      style,
      pattern,
      t8Impl,
      rules = getRules(locale, "time");

  style = getStyleByIndex(placeholderAst, 0);

  if (!style) {
    style = "medium";
  } else {
    style = style.key;
  }

  pattern = rules[style];

  if (!pattern) {
    // Might be a custom format...
    pattern = style;
  }

  if (pattern) {
    t8Impl = new t8.DateFormatter();
    output = t8Impl.format(new Date(resolvedValue), locale, pattern, false);
  }

  return output;
}

"use strict";

var suffix_placeholder = evaluateSuffixPlaceholder;
var CHAR_SPACE = " ",
    CHAR_TAB = "\t",
    suffix_placeholder__STRING_SEP = "sep",
    STRATEGY_REVERSE_SEARCH_FOR_VOWEL = "reverseSearchForVowel";
function evaluateSuffixPlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      result = "",
      addSeparator = false,
      rules,
      key,
      value,
      regex,
      suffixFromRules,
      currentChar,
      i,
      lastIndex,
      vowels,
      endsWithVowel,
      params = placeholderAst.parameters;

  if (resolvedValue !== undefined) {
    if (params) {
      addSeparator = !!params[suffix_placeholder__STRING_SEP];
    }

    rules = getRules(locale, "suffix");

    if (rules) {
      vowels = (rules.hardVowels || "") + (rules.softVowels || "");

      switch (rules.strategy) {
        case STRATEGY_REVERSE_SEARCH_FOR_VOWEL:
          if (resolvedValue.length > 0) {
            lastIndex = resolvedValue.length - 1;

            for (i = lastIndex; i >= 0 && currentChar !== CHAR_SPACE && currentChar !== CHAR_TAB; i--) {
              currentChar = resolvedValue.charAt(i);

              if (vowels.indexOf(currentChar) !== -1) {
                endsWithVowel = i === lastIndex;
                suffixFromRules = rules.hardVowels && rules.hardVowels.indexOf(currentChar) > -1 ? rules.hardVowelSuffix : rules.fallbackSuffix;
                result = "" + (endsWithVowel ? rules.bufferChar : "") + suffixFromRules;
                return addSeparator ? rules.separator + result : result;
              }
            }

            // Fall back to check if it ends with a consonant.
            for (key in rules.nonVowelToSuffix) {
              if (rules.nonVowelToSuffix.hasOwnProperty(key)) {
                value = rules.nonVowelToSuffix[key];
                regex = new RegExp(key);

                if (regex.test(resolvedValue.charAt(lastIndex))) {
                  suffixFromRules = value;
                  break;
                }
              }
            }

            if (!suffixFromRules) {
              suffixFromRules = rules.defaultBufferChar;
            }

            result = addSeparator ? rules.separator + suffixFromRules : suffixFromRules;
            break;
          }
          break;
        default:
          break;
      }
    }
  }

  return result;
}

/**
 * Formats a value as a name according to a set of styles.
 * @param {Object} styles - a placeholder's style map
 * @param {Object} value - the value to decorate
 */
"use strict";

var name_formatter = formatName;
var DEFAULT_NAME_STYLE = "FULL_NAME",
    NAME_STYLES = ["familiar", "family", "full", "given", "list", "maiden"];
function formatName(styles, value, locale) {
  var output,
      nameAdapter = {},
      formatAdapter,
      formatStyle,
      hasMicroformat = false,
      t8Impl;

  if (value !== undefined) {
    nameAdapter.firstName = value.givenName;
    nameAdapter.lastName = value.familyName;
    nameAdapter.maidenName = value.maidenName;

    formatStyle = findFirstProperty(NAME_STYLES, styles);

    if (formatStyle) {
      formatStyle = formatStyle.key;
    } else {
      formatStyle = "familiar";
    }

    hasMicroformat = !!styles.micro;

    if (formatStyle) {
      switch (formatStyle) {
        case "given":
          output = value.givenName || "";

          if (hasMicroformat) {
            output = "<span class=\"given-name\">" + output + "</span>";
          }

          break;
        case "family":
          output = value.familyName || "";

          if (hasMicroformat) {
            output = "<span class=\"family-name\">" + output + "</span>";
          }

          break;
        case "maiden":
          output = value.maidenName || "";

          if (hasMicroformat) {
            output = "<span class=\"additional-name\">" + output + "</span>";
          }

          break;
        default:
          formatAdapter = formatStyle === "full" || formatStyle === "given" || formatStyle === "family" || formatStyle === "maiden" ? "FULL_NAME" : formatStyle === "familiar" ? "FAMILIAR_NAME" : formatStyle === "list" ? "LIST_VIEW" : DEFAULT_NAME_STYLE;

          if (hasMicroformat) {
            formatAdapter = [formatAdapter, "MICROFORMAT"];
          }

          t8Impl = new t8.NameFormatter();
          output = t8Impl.format(nameAdapter, formatAdapter, locale);
          break;
      }
    }
  }

  return output;
}

"use strict";

var name_placeholder = evaluateNamePlaceholder;
function evaluateNamePlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      output = undefined;

  output = name_formatter(placeholderAst.parameters, resolvedValue, locale);

  if (placeholderAst.parameters.possessive) {
    output += formatPossessive(output, locale);
  }

  output = prefix_suffix_formatter(placeholderAst.parameters, output);

  return output;
}

"use strict";

var list_placeholder = evaluateListPlaceholder;
var TYPE_NAME = "name",
    RULE_START = "start",
    RULE_MIDDLE = "middle",
    RULE_END = "end",
    RULE_2 = "2";

function applyValueFormatting(styles, value, locale) {
  if (styles) {
    if (styles[TYPE_NAME]) {
      value = name_formatter(styles, value, locale);
    }
    // Then apply prefix/suffix
    value = prefix_suffix_formatter(styles, value);
  }
  return value;
}
function evaluateListPlaceholder(placeholderAst, context, locale) {
  var resolvedValue = resolveIndex(placeholderAst, context),
      result = "",
      collectionSize,
      lastIndex,
      rules = getRules(locale, "list") || getRules("default", "list"),
      listRule,
      firstPosition,
      secondPosition;

  // Blank output for falsy and non-list values.

  if (resolvedValue && resolvedValue instanceof Array) {
    collectionSize = resolvedValue.length;
    lastIndex = collectionSize - 1;

    if (collectionSize > 0) {
      switch (collectionSize) {
        case 1:
          result = applyValueFormatting(placeholderAst.parameters, resolvedValue[0], locale);
          break;

        case 2:
          result = stringFormat(rules[RULE_2], applyValueFormatting(placeholderAst.parameters, resolvedValue[0], locale), applyValueFormatting(placeholderAst.parameters, resolvedValue[1], locale));
          break;

        default:
          // 3 or more
          firstPosition = 0;
          secondPosition = 1;

          do {
            listRule = firstPosition === 0 ? RULE_START : secondPosition < lastIndex ? RULE_MIDDLE : RULE_END;

            if (listRule === RULE_START) {
              result = stringFormat(rules[listRule], applyValueFormatting(placeholderAst.parameters, resolvedValue[firstPosition], locale), applyValueFormatting(placeholderAst.parameters, resolvedValue[secondPosition], locale));
            } else {
              result = stringFormat(rules[listRule], result, applyValueFormatting(placeholderAst.parameters, resolvedValue[secondPosition], locale));
            }

            firstPosition++;
            secondPosition++;
          } while (listRule !== RULE_END);
          break;
      }
    } // else { return ''; }
  } // else { return ''; }

  return result;
}

/**
 * Validate if an given AST is well-formed. Returns a string if there was a
 * validation error; otherwise returns undefined.
 * @param  {Object} ast - the AST of a placeholder
 * @return {String|Undefined}
 */
"use strict";

var validatePlaceholder = validatePlaceholder__validate;

function validatePlaceholder__validate(ast) {
  if (!ast) {
    return "Placeholder is invalid.";
  }

  if (!ast.index) {
    return "Placeholder must have an index.";
  }

  return undefined;
}

"use strict";

function allowOnlyTheseStyleKeys(ast, allowedStylesRegexp) {
  var hasValidationError, styles, name;

  if (ast.parameters) {
    styles = ast.parameters;

    for (name in styles) {
      if (styles.hasOwnProperty(name)) {
        if (!allowedStylesRegexp.test(name)) {
          hasValidationError = "Invalid style \"" + name + "\"";
          break;
        }
      }
    }
  }

  return hasValidationError;
}

function mustHaveTheseStyleKeys(ast, requiredStyleKeys) {
  var hasValidationError, styles, name, index, length;

  if (ast.parameters) {
    styles = ast.parameters;

    for (index = 0, length = requiredStyleKeys.length; index < length; ++index) {
      name = requiredStyleKeys[index];
      if (!styles.hasOwnProperty(name)) {
        hasValidationError = "Missing required style key \"" + name + "\"";
      }
    }
  } else {
    hasValidationError = "Placeholder must have styles";
  }

  return hasValidationError;
}

function checkStyleCount(ast, styleLimit, mode) {
  var hasValidationError,
      messageZero = "Placeholder must have styles",
      messageEq = "Placeholder must have exactly " + styleLimit + " style(s)",
      messageLte = "Placeholder must have at least " + styleLimit + " style(s)",
      styleCount = 0,
      key;

  // This would be way nicer if we could just use Object.keys().length here.

  if (ast.parameters) {
    for (key in ast.parameters) {
      if (ast.parameters.hasOwnProperty(key)) {
        styleCount++;
      }
    }

    if (mode === "eq" && styleCount !== styleLimit) {
      hasValidationError = messageEq;
    } else if (mode === "gte" && styleCount < styleLimit) {
      hasValidationError = messageLte;
    }
  } else {
    if (styleLimit > 0) {
      hasValidationError = messageZero;
    }
    // else case: we expect 0 and there are none -- no error here.
  }

  return hasValidationError;
}
function mustHaveAtLeastNStyles(ast, styleLimit) {
  return checkStyleCount(ast, styleLimit, "gte");
}

function mustHaveExactlyNStyles(ast, styleLimit) {
  return checkStyleCount(ast, styleLimit, "eq");
}

function checkStyleValue(ast, styles, mode) {
  var style, index, length, hasValidationError;

  if (ast.parameters) {
    for (index = 0, length = styles.length; index < length; ++index) {
      style = ast.parameters[styles[index]];

      if ("without" === mode) {
        if (style && style.value) {
          hasValidationError = "Invalid value for style \"" + style.key + "\"";
        }
      } else if ("with" === mode) {
        if (style) {
          if (!style.value) {
            hasValidationError = "Style \"" + style.key + "\" must have a value";
          }
        }
      }
    }
  }

  return hasValidationError;
}
function stylesMustHaveAValue(ast, styles) {
  return checkStyleValue(ast, styles, "with");
}

function stylesMustHaveNoValue(ast, styles) {
  return checkStyleValue(ast, styles, "without");
}

"use strict";

var validateAnchor = validateAnchor__validate;
var validateAnchor__STRING_TEXT = "text",
    validateAnchor__STRING_TITLE = "title",
    REQUIRED_STYLES = [validateAnchor__STRING_TEXT],
    validateAnchor__SUPPORTED_STYLES_REGEXP = new RegExp("^(" + [validateAnchor__STRING_TEXT, validateAnchor__STRING_TITLE].join("|") + ")$");
function validateAnchor__validate(ast) {
  var result = mustHaveTheseStyleKeys(ast, REQUIRED_STYLES);

  if (!result) {
    result = allowOnlyTheseStyleKeys(validateAnchor__SUPPORTED_STYLES_REGEXP);
  }

  return result;
}

"use strict";

var validateBoolean = validateBoolean__validate;
var validateBoolean__STRING_TRUE = "true",
    validateBoolean__STRING_FALSE = "false",
    validateBoolean__SUPPORTED_STYLES_REGEXP = new RegExp("^(" + [validateBoolean__STRING_TRUE, validateBoolean__STRING_FALSE].join("|") + ")$");
function validateBoolean__validate(ast) {
  var result = mustHaveAtLeastNStyles(ast, 1);

  if (!result) {
    result = allowOnlyTheseStyleKeys(ast, validateBoolean__SUPPORTED_STYLES_REGEXP);
  }

  return result;
}

"use strict";

var validateChoice = validateChoice__validate;
var validateChoice__REGEX_NUMBERS = /-?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/,
    validateChoice__REGEX_CATEGORIES = /^(zero|singular|dual|few|many|plural|other)$/;
function validateChoice__validate(ast) {
  var result,
      lastNumber,
      currentNumber,
      i,
      length,
      styles,
      params = ast.parameters,
      currentStyle,
      key,
      hasCategories = false;

  styles = [];

  for (key in params) {
    if (params.hasOwnProperty(key)) {
      currentStyle = params[key];
      styles[currentStyle.order] = currentStyle;

      if (validateChoice__REGEX_CATEGORIES.test(key)) {
        hasCategories = true;
      }
    }
  }

  currentStyle = undefined;

  // Loop through the styles and check if numeric ones are ascending.
  for (i = 0, length = styles.length; i < length; ++i) {
    currentStyle = styles[i];

    if (validateChoice__REGEX_NUMBERS.test(currentStyle.key)) {
      currentNumber = parseInt(currentStyle.key, 10);

      if (lastNumber === undefined) {
        lastNumber = currentNumber;
      } else {
        if (lastNumber < currentNumber) {
          lastNumber = currentNumber;
        } else {
          result = "Invalid number order. Cannot list " + currentNumber + " after " + lastNumber + ". Numbers must be ascending.";
          break;
        }
      }
    } else if (validateChoice__REGEX_CATEGORIES.test(currentStyle.key)) {
      hasCategories = true;
    } else {
      result = "Invalid category key \"" + currentStyle.key + "\".";
      break;
    }
  }

  if (hasCategories) {
    // It must have singular and plural categories if it has categories.
    if (!params.singular) {
      result = "Missing required category \"singular\"";
    } else if (!params.plural) {
      result = "Missing required category \"plural\"";
    }
  }

  return result;
}

"use strict";

var validateMap = validateMap__validate;

function validateMap__validate(ast) {
  var result, styles, name, hasStyle;

  if (!ast.parameters) {
    result = "MapPlaceholder must have parameters.";
  } else {
    styles = ast.parameters;

    for (name in styles) {
      if (styles.hasOwnProperty(name)) {
        hasStyle = true; // If there's at least one this should be true.

        if (!styles[name].value) {
          result = "MapPlaceholder cannot have keys without values.";
          break;
        }
      }
    }

    if (!hasStyle) {
      result = "MapPlaceholder must have at least one style argument.";
    }
  }

  return result;
}

"use strict";

var validateName = validateName__validate;
var validateName__STRING_PREFIX = "prefix",
    validateName__STRING_SUFFIX = "suffix",
    validateName__STYLES_THAT_REQUIRE_VALUES = [validateName__STRING_PREFIX, validateName__STRING_SUFFIX],
    validateName__SUPPORTED_STYLES_REGEXP = /^(familiar|family|full|given|list|maiden|micro|possessive|salutation|prefix|suffix)$/;
function validateName__validate(ast) {
  var result = allowOnlyTheseStyleKeys(ast, validateName__SUPPORTED_STYLES_REGEXP);

  if (!result) {
    result = stylesMustHaveAValue(ast, validateName__STYLES_THAT_REQUIRE_VALUES);
  }

  return result;
}

"use strict";

var validateList = validateList__validate;
var validateList__STRING_PREFIX = "prefix",
    validateList__STRING_SUFFIX = "suffix",
    validateList__STYLES_THAT_REQUIRE_VALUES = [validateList__STRING_PREFIX, validateList__STRING_SUFFIX],
    validateList__SUPPORTED_STYLES_REGEXP = /^(text|name|familiar|family|full|given|list|maiden|micro|possessive|salutation|prefix|suffix)$/;
function validateList__validate(ast) {
  var result = allowOnlyTheseStyleKeys(ast, validateList__SUPPORTED_STYLES_REGEXP);

  if (!result) {
    result = stylesMustHaveAValue(ast, validateList__STYLES_THAT_REQUIRE_VALUES);
  }

  return result;
}

"use strict";

var validatePossessive = validatePossessive__validate;
function validatePossessive__validate(ast) {
  return mustHaveExactlyNStyles(ast, 0);
}

"use strict";

var validateSuffix = validateSuffix__validate;
var validateSuffix__STRING_SEP = "sep",
    STRING_NOSEP = "nosep",
    EMPTY_STYLES = [validateSuffix__STRING_SEP, STRING_NOSEP],
    validateSuffix__SUPPORTED_STYLES_REGEXP = new RegExp("^(" + [validateSuffix__STRING_SEP, STRING_NOSEP].join("|") + ")$");
function validateSuffix__validate(ast) {
  var result = mustHaveExactlyNStyles(ast, 1);

  if (!result) {
    result = allowOnlyTheseStyleKeys(ast, validateSuffix__SUPPORTED_STYLES_REGEXP);
  }

  if (!result) {
    result = stylesMustHaveNoValue(ast, EMPTY_STYLES);
  }

  return result;
}

"use strict";

var validateText = validateText__validate;
var validateText__STRING_PREFIX = "prefix",
    validateText__STRING_SUFFIX = "suffix",
    validateText__SUPPORTED_STYLES_REGEXP = new RegExp([validateText__STRING_SUFFIX, validateText__STRING_PREFIX].join("|"));
function validateText__validate(ast) {
  return allowOnlyTheseStyleKeys(ast, validateText__SUPPORTED_STYLES_REGEXP);
}

"use strict";

var Validator = Validator__validate;
var specializedValidators = {
  anchor: validateAnchor,
  boolean: validateBoolean,
  choice: validateChoice,
  list: validateList,
  map: validateMap,
  name: validateName,
  possessive: validatePossessive,
  suffix: validateSuffix,
  text: validateText
};
function Validator__validate(ast) {
  var key, value, hasValidationError;

  if (typeof ast === "string") {
    return;
  }

  hasValidationError = validatePlaceholder(ast);

  if (!hasValidationError) {
    if (specializedValidators.hasOwnProperty(ast.type)) {
      hasValidationError = specializedValidators[ast.type](ast);
    }
  }

  if (hasValidationError) {
    throw new Error(hasValidationError);
  } else {
    // Check styles (if they are also placeholders)
    if (ast && ast.parameters instanceof Object) {
      for (key in ast.parameters) {
        if (ast.parameters.hasOwnProperty(key)) {
          value = ast.parameters[key];

          if (value.value && value.value instanceof Array) {
            // Calls itself recursively.
            value.value.forEach(Validator__validate);
          }
        }
      }
    }
  }
}

/**
 * Converts an XMessage-compliant string into an AST by parsing it.
 *
 * @param  {[type]} xmessage [description]
 * @return {[type]}          [description]
 */
"use strict";

var EVALUATORS = {
  anchor: anchor_placeholder,
  boolean: boolean_placeholder,
  choice: choice_placeholder,
  date: date_placeholder,
  list: list_placeholder,
  map: map_placeholder,
  name: name_placeholder,
  number: number_placeholder,
  possessive: possessive_placeholder,
  simple: simple_placeholder,
  suffix: suffix_placeholder,
  text: text_placeholder,
  time: time_placeholder
};

/**
 * A facade/proxy function that delegates to various placeholder functions.
 * This function maps a placeholder to its resolved value. Passes `context`
 * and `locale` to the underlying implementations.
 *
 * @param  {Object} placeholderAst - a well-formed placeholder AST object
 * @param  {Array} context - an array of values, may be primitives or objects
 * @param  {String} locale - the locale
 * @return {String}
 */
function message_builder__evaluator(placeholderAst, context, locale) {
  var result = "",
      evaluatorFunction;

  if (placeholderAst && context && locale) {
    evaluatorFunction = EVALUATORS[placeholderAst.type];

    if (evaluatorFunction) {
      result = evaluatorFunction(placeholderAst, context, locale);
    }
  }

  return result;
}

// Use the above evaluator function for evaluating placeholders and styles.
setEvaluator(message_builder__evaluator);
function toAst(xmessage) {
  var protoMessage = parser(xmessage);

  // Perform validation. This could be omitted for PROD.
  protoMessage.forEach(Validator);

  return protoMessage;
}

function makeInterpolator(ast, locale) {
  return function evaluate(context) {
    return ast.map(function (placeholder) {
      if (typeof placeholder === "string") {
        return placeholder;
      } else {
        return message_builder__evaluator(placeholder, context, locale);
      }
    }).join("");
  };
}

function fromString(xmessage, locale) {
  return makeInterpolator(toAst(xmessage), locale);
}

exports.toAst = toAst;
exports.makeInterpolator = makeInterpolator;
exports.fromString = fromString;
}exportAsGlobal((this.xmessage = {}), require);})();

/**
 * Override the get method in t8 to support xmsg syntax in dust templates.
 *
 * Created by xxiao on 5/15/15.
 */
(function(play, t8, LI) {

  var oldResourceGet = t8.Resources.prototype.get;

  t8.Resources.prototype.get = function(key, namespace, context, callback) {
    var useNativeXmsg = false;
    var useJavaXmsg = false;
    if (play.hasPageContext()) {
      useNativeXmsg = play.getPageContextValue("useNativeXmsg", true);
      useJavaXmsg = !play.isClient && play.getPageContextValue("useJavaXmsg", false);
    }
    if (useNativeXmsg || useJavaXmsg) {
      play.Utils.assert(callback, "get called with null callback");
      play.Utils.assert(key, "get called with null or empty key");
      play.Utils.assert(namespace, "get called with null or empty namespace");

      //No dynamic string if xmsg syntax is enabled
      evalXmsg(callback, key, namespace, context, useJavaXmsg, this.i18nCacheStatic);
    } else {
      oldResourceGet.call(this, key, namespace, context, callback);
    }

  };

  function evalXmsg(callback, key, namespace, context, useJavaXmsg, i18nCacheStatic) {

    if (useJavaXmsg) {
      try {
        callback(null, com.linkedin.playplugins.dust.plugin.I18nPropertiesRenderer.getProperty(namespace, LI.i18n.getLocale().value, key, context));
      } catch (e) {
        callback(e.getMessage());
      }
    } else {
      if (i18nCacheStatic && i18nCacheStatic.cache && i18nCacheStatic.cache[namespace] && i18nCacheStatic.cache[namespace][key]) {
        var xmsgString = i18nCacheStatic.cache[namespace][key];
        var ast = xmessage.toAst(xmsgString);
        var messageFn = xmessage.makeInterpolator(ast, LI.i18n.getLocale().value);
        callback(null, messageFn(getData(ast, context)));
      } else {
        callback("Could not find xmsg key " + key + " in static i18n cache.");
      }
    }

  }

  // Construct the argument array. Since dust does not use the number based index, the array length should always be 1.
  function getData(ast, context) {
    var result = [];
    var data = {};
    ast.map(function (placeholder) {
      if (typeof placeholder === "object") {
        var argName = placeholder.index.keyword;
        data[argName] = context.get(argName);
      }
    });
    result[0] = data;
    return result;
  }

})(play, t8, LI);