text: /**
   * @author      Created by Marcus Spiegel <spiegel@uscreen.de> on 2011-03-25.
   * @link        https://github.com/mashpie/i18n-node
   * @license     http://opensource.org/licenses/MIT
   */
  
  'use strict';
  
  // dependencies
  var vsprintf = require('sprintf-js').vsprintf,
    pkgVersion = require('./package.json').version,
    fs = require('fs'),
    url = require('url'),
    path = require('path'),
    debug = require('debug')('i18n:debug'),
    warn = require('debug')('i18n:warn'),
    error = require('debug')('i18n:error'),
    Mustache = require('mustache'),
    Messageformat = require('messageformat'),
    MakePlural = require('make-plural'),
    parseInterval = require('math-interval-parser').default;
  
  // utils
  var escapeRegExp = function(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
  };
  
  // exports an instance
  module.exports = (function() {
  
    var MessageformatInstanceForLocale = {},
      PluralsForLocale = {},
      locales = {},
      api = {
        '__': '__',
        '__n': '__n',
        '__l': '__l',
        '__h': '__h',
        '__mf': '__mf',
        'getLocale': 'getLocale',
        'setLocale': 'setLocale',
        'getCatalog': 'getCatalog',
        'getLocales': 'getLocales',
        'addLocale': 'addLocale',
        'removeLocale': 'removeLocale'
      },
      mustacheConfig = {
        tags: [ '{{', '}}' ],
        disable: false
      },
      mustacheRegex,
      pathsep = path.sep, // ---> means win support will be available in node 0.8.x and above
      autoReload,
      cookiename,
      defaultLocale,
      directory,
      directoryPermissions,
      extension,
      fallbacks,
      indent,
      logDebugFn,
      logErrorFn,
      logWarnFn,
      preserveLegacyCase,
      objectNotation,
      prefix,
      queryParameter,
      register,
      updateFiles,
      syncFiles,
      missingKeyFn;
  
    // public exports
    var i18n = {};
  
    i18n.version = pkgVersion;
  
    i18n.configure = function i18nConfigure(opt) {
  
      // reset locales
      locales = {};
  
      // Provide custom API method aliases if desired
      // This needs to be processed before the first call to applyAPItoObject()
      if (opt.api && typeof opt.api === 'object') {
        for (var method in opt.api) {
          if (opt.api.hasOwnProperty(method)) {
            var alias = opt.api[method];
            if (typeof api[method] !== 'undefined') {
              api[method] = alias;
            }
          }
        }
      }
  
      // you may register i18n in global scope, up to you
      if (typeof opt.register === 'object') {
        register = opt.register;
        // or give an array objects to register to
        if (Array.isArray(opt.register)) {
          register = opt.register;
          register.forEach(function(r) {
            applyAPItoObject(r);
          });
        } else {
          applyAPItoObject(opt.register);
        }
      }
  
      // sets a custom cookie name to parse locale settings from
      cookiename = (typeof opt.cookie === 'string') ? opt.cookie : null;
  
      // query-string parameter to be watched - @todo: add test & doc
      queryParameter = (typeof opt.queryParameter === 'string') ? opt.queryParameter : null;
  
      // where to store json files
      directory = (typeof opt.directory === 'string') ?
        opt.directory : path.join(__dirname, 'locales');
  
      // permissions when creating new directories
      directoryPermissions = (typeof opt.directoryPermissions === 'string') ?
        parseInt(opt.directoryPermissions, 8) : null;
  
      // write new locale information to disk
      updateFiles = (typeof opt.updateFiles === 'boolean') ? opt.updateFiles : true;
  
      // sync locale information accros all files
      syncFiles = (typeof opt.syncFiles === 'boolean') ? opt.syncFiles : false;
  
      // what to use as the indentation unit (ex: "\t", "  ")
      indent = (typeof opt.indent === 'string') ? opt.indent : '\t';
  
      // json files prefix
      prefix = (typeof opt.prefix === 'string') ? opt.prefix : '';
  
      // where to store json files
      extension = (typeof opt.extension === 'string') ? opt.extension : '.json';
  
      // setting defaultLocale
      defaultLocale = (typeof opt.defaultLocale === 'string') ? opt.defaultLocale : 'en';
  
      // auto reload locale files when changed
      autoReload = (typeof opt.autoReload === 'boolean') ? opt.autoReload : false;
  
      // enable object notation?
      objectNotation = (typeof opt.objectNotation !== 'undefined') ? opt.objectNotation : false;
      if (objectNotation === true) objectNotation = '.';
  
      // read language fallback map
      fallbacks = (typeof opt.fallbacks === 'object') ? opt.fallbacks : {};
  
      // setting custom logger functions
      logDebugFn = (typeof opt.logDebugFn === 'function') ? opt.logDebugFn : debug;
      logWarnFn = (typeof opt.logWarnFn === 'function') ? opt.logWarnFn : warn;
      logErrorFn = (typeof opt.logErrorFn === 'function') ? opt.logErrorFn : error;
  
      preserveLegacyCase = (typeof opt.preserveLegacyCase === 'undefined') ?
        true : opt.preserveLegacyCase;
  
      // setting custom missing key function
      missingKeyFn = (typeof opt.missingKeyFn === 'function') ? opt.missingKeyFn : missingKey;
  
      // when missing locales we try to guess that from directory
      opt.locales = opt.staticCatalog ? Object.keys(opt.staticCatalog) : opt.locales || guessLocales(directory);
  
      // some options should be disabled when using staticCatalog
      if(opt.staticCatalog){
        updateFiles = false;
        autoReload = false;
        syncFiles = false;
      }
  
      // customize mustache parsing
      if(opt.mustacheConfig) {
        if(Array.isArray(opt.mustacheConfig.tags)){
          mustacheConfig.tags = opt.mustacheConfig.tags;
        }
        if(opt.mustacheConfig.disable === true){
          mustacheConfig.disable = true;
        }
      }
  
      const [ start, end ] = mustacheConfig.tags;
      mustacheRegex = new RegExp(escapeRegExp(start) + '.*' + escapeRegExp(end));
  
      // implicitly read all locales
      if (Array.isArray(opt.locales)) {
  
        if (opt.staticCatalog) {
          locales = opt.staticCatalog;
        } else {
          opt.locales.forEach(function(l) {
            read(l);
          });
        }
  
        // auto reload locale files when changed
        if (autoReload) {
  
          // watch changes of locale files (it's called twice because fs.watch is still unstable)
          fs.watch(directory, function(event, filename) {
            var localeFromFile = guessLocaleFromFile(filename);
  
            if (localeFromFile && opt.locales.indexOf(localeFromFile) > -1) {
              logDebug('Auto reloading locale file "' + filename + '".');
              read(localeFromFile);
            }
  
          });
        }
      }
    };
  
    i18n.init = function i18nInit(request, response, next) {
      if (typeof request === 'object') {
  
        // guess requested language/locale
        guessLanguage(request);
  
        // bind api to req
        applyAPItoObject(request);
  
        // looks double but will ensure schema on api refactor
        i18n.setLocale(request, request.locale);
      } else {
        return logError('i18n.init must be called with one parameter minimum, ie. i18n.init(req)');
      }
  
      if (typeof response === 'object') {
        applyAPItoObject(response);
  
        // and set that locale to response too
        i18n.setLocale(response, request.locale);
      }
  
      // head over to next callback when bound as middleware
      if (typeof next === 'function') {
        return next();
      }
    };
  
    i18n.__ = function i18nTranslate(phrase) {
      var msg;
      var argv = parseArgv(arguments);
      var namedValues = argv[0];
      var args = argv[1];
  
      // called like __({phrase: "Hello", locale: "en"})
      if (typeof phrase === 'object') {
        if (typeof phrase.locale === 'string' && typeof phrase.phrase === 'string') {
          msg = translate(phrase.locale, phrase.phrase);
        }
      }
      // called like __("Hello")
      else {
        // get translated message with locale from scope (deprecated) or object
        msg = translate(getLocaleFromObject(this), phrase);
      }
  
      // postprocess to get compatible to plurals
      if (typeof msg === 'object' && msg.one) {
        msg = msg.one;
      }
  
      // in case there is no 'one' but an 'other' rule
      if (typeof msg === 'object' && msg.other) {
        msg = msg.other;
      }
  
      // head over to postProcessing
      return postProcess(msg, namedValues, args);
    };
  
    i18n.__mf = function i18nMessageformat(phrase) {
      var msg, mf, f;
      var targetLocale = defaultLocale;
      var argv = parseArgv(arguments);
      var namedValues = argv[0];
      var args = argv[1];
  
      // called like __({phrase: "Hello", locale: "en"})
      if (typeof phrase === 'object') {
        if (typeof phrase.locale === 'string' && typeof phrase.phrase === 'string') {
          msg = phrase.phrase;
          targetLocale = phrase.locale;
        }
      }
      // called like __("Hello")
      else {
        // get translated message with locale from scope (deprecated) or object
        msg = phrase;
        targetLocale = getLocaleFromObject(this);
      }
  
      msg = translate(targetLocale, msg);
      // --- end get msg
  
      // now head over to Messageformat
      // and try to cache instance
      if (MessageformatInstanceForLocale[targetLocale]) {
        mf = MessageformatInstanceForLocale[targetLocale];
      } else {
        mf = new Messageformat(targetLocale);
  
        // @see https://messageformat.github.io/messageformat/MessageFormat#disablePluralKeyChecks__anchor
        mf.disablePluralKeyChecks();
        mf.compiledFunctions = {};
        MessageformatInstanceForLocale[targetLocale] = mf;
      }
  
      // let's try to cache that function
      if (mf.compiledFunctions[msg]) {
        f = mf.compiledFunctions[msg];
      } else {
        f = mf.compile(msg);
        mf.compiledFunctions[msg] = f;
      }
  
      return postProcess(f(namedValues), namedValues, args);
    };
  
    i18n.__l = function i18nTranslationList(phrase) {
      var translations = [];
      Object.keys(locales).sort().forEach(function(l) {
        translations.push(i18n.__({ phrase: phrase, locale: l }));
      });
      return translations;
    };
  
    i18n.__h = function i18nTranslationHash(phrase) {
      var translations = [];
      Object.keys(locales).sort().forEach(function(l) {
        var hash = {};
        hash[l] = i18n.__({ phrase: phrase, locale: l });
        translations.push(hash);
      });
      return translations;
    };
  
    i18n.__n = function i18nTranslatePlural(singular, plural, count) {
      var msg, namedValues, targetLocale, args = [];
  
      // Accept an object with named values as the last parameter
      if (argsEndWithNamedObject(arguments)) {
        namedValues = arguments[arguments.length - 1];
        args = arguments.length >= 5 ? Array.prototype.slice.call(arguments, 3, -1) : [];
      } else {
        namedValues = {};
        args = arguments.length >= 4 ? Array.prototype.slice.call(arguments, 3) : [];
      }
  
      // called like __n({singular: "%s cat", plural: "%s cats", locale: "en"}, 3)
      if (typeof singular === 'object') {
        if (
          typeof singular.locale === 'string' &&
          typeof singular.singular === 'string' &&
          typeof singular.plural === 'string'
        ) {
          targetLocale = singular.locale;
          msg = translate(singular.locale, singular.singular, singular.plural);
        }
        args.unshift(count);
  
        // some template engines pass all values as strings -> so we try to convert them to numbers
        if (typeof plural === 'number' || Number(plural) + '' === plural) {
          count = plural;
        }
  
        // called like __n({singular: "%s cat", plural: "%s cats", locale: "en", count: 3})
        if (typeof singular.count === 'number' || typeof singular.count === 'string') {
          count = singular.count;
          args.unshift(plural);
        }
      } else {
        // called like  __n('cat', 3)
        if (typeof plural === 'number' || Number(plural) + '' === plural) {
          count = plural;
  
          // we add same string as default
          // which efectivly copies the key to the plural.value
          // this is for initialization of new empty translations
          plural = singular;
  
          args.unshift(count);
          args.unshift(plural);
        }
        // called like __n('%s cat', '%s cats', 3)
        // get translated message with locale from scope (deprecated) or object
        msg = translate(getLocaleFromObject(this), singular, plural);
        targetLocale = getLocaleFromObject(this);
      }
  
      if (count === null) count = namedValues.count;
  
      // enforce number
      count = Number(count);
  
      // find the correct plural rule for given locale
      if (typeof msg === 'object') {
        var p;
        // create a new Plural for locale
        // and try to cache instance
        if (PluralsForLocale[targetLocale]) {
          p = PluralsForLocale[targetLocale];
        } else {
          // split locales with a region code
          var lc = targetLocale.toLowerCase().split(/[_-\s]+/)
            .filter(function(el){ return true && el; });
          // take the first part of locale, fallback to full locale
          p = MakePlural[lc[0] || targetLocale];
          PluralsForLocale[targetLocale] = p;
        }
  
        // fallback to 'other' on case of missing translations
        msg = msg[p(count)] || msg.other;
      }
  
      // head over to postProcessing
      return postProcess(msg, namedValues, args, count);
    };
  
    i18n.setLocale = function i18nSetLocale(object, locale, skipImplicitObjects) {
  
      // when given an array of objects => setLocale on each
      if (Array.isArray(object) && typeof locale === 'string') {
        for (var i = object.length - 1; i >= 0; i--) {
          i18n.setLocale(object[i], locale, true);
        }
        return i18n.getLocale(object[0]);
      }
  
      // defaults to called like i18n.setLocale(req, 'en')
      var targetObject = object;
      var targetLocale = locale;
  
      // called like req.setLocale('en') or i18n.setLocale('en')
      if (locale === undefined && typeof object === 'string') {
        targetObject = this;
        targetLocale = object;
      }
  
      // consider a fallback
      if (!locales[targetLocale]) {
        targetLocale = getFallback(targetLocale, fallbacks) || targetLocale;
      }
  
      // now set locale on object
      targetObject.locale = locales[targetLocale] ? targetLocale : defaultLocale;
  
      // consider any extra registered objects
      if (typeof register === 'object') {
        if (Array.isArray(register) && !skipImplicitObjects) {
          register.forEach(function(r) {
            r.locale = targetObject.locale;
          });
        } else {
          register.locale = targetObject.locale;
        }
      }
  
      // consider res
      if (targetObject.res && !skipImplicitObjects) {
  
        // escape recursion
        // @see  - https://github.com/balderdashy/sails/pull/3631
        //       - https://github.com/mashpie/i18n-node/pull/218
        if (targetObject.res.locals) {
          i18n.setLocale(targetObject.res, targetObject.locale, true);
          i18n.setLocale(targetObject.res.locals, targetObject.locale, true);
        } else {
          i18n.setLocale(targetObject.res, targetObject.locale);
        }
      }
  
      // consider locals
      if (targetObject.locals && !skipImplicitObjects) {
  
        // escape recursion
        // @see  - https://github.com/balderdashy/sails/pull/3631
        //       - https://github.com/mashpie/i18n-node/pull/218
        if (targetObject.locals.res) {
          i18n.setLocale(targetObject.locals, targetObject.locale, true);
          i18n.setLocale(targetObject.locals.res, targetObject.locale, true);
        } else {
          i18n.setLocale(targetObject.locals, targetObject.locale);
        }
      }
  
      return i18n.getLocale(targetObject);
    };
  
    i18n.getLocale = function i18nGetLocale(request) {
  
      // called like i18n.getLocale(req)
      if (request && request.locale) {
        return request.locale;
      }
  
      // called like req.getLocale()
      return this.locale || defaultLocale;
    };
  
    i18n.getCatalog = function i18nGetCatalog(object, locale) {
      var targetLocale;
  
      // called like i18n.getCatalog(req)
      if (typeof object === 'object' && typeof object.locale === 'string' && locale === undefined) {
        targetLocale = object.locale;
      }
  
      // called like i18n.getCatalog(req, 'en')
      if (!targetLocale && typeof object === 'object' && typeof locale === 'string') {
        targetLocale = locale;
      }
  
      // called like req.getCatalog('en')
      if (!targetLocale && locale === undefined && typeof object === 'string') {
        targetLocale = object;
      }
  
      // called like req.getCatalog()
      if (!targetLocale &&
        object === undefined &&
        locale === undefined &&
        typeof this.locale === 'string'
      ) {
        if (register && register.global) {
          targetLocale = '';
        } else {
          targetLocale = this.locale;
        }
      }
  
      // called like i18n.getCatalog()
      if (targetLocale === undefined || targetLocale === '') {
        return locales;
      }
  
      if (!locales[targetLocale]) {
        targetLocale = getFallback(targetLocale, fallbacks) || targetLocale;
      }
  
      if (locales[targetLocale]) {
        return locales[targetLocale];
      } else {
        logWarn('No catalog found for "' + targetLocale + '"');
        return false;
      }
    };
  
    i18n.getLocales = function i18nGetLocales() {
      return Object.keys(locales);
    };
  
    i18n.addLocale = function i18nAddLocale(locale) {
      read(locale);
    };
  
    i18n.removeLocale = function i18nRemoveLocale(locale) {
      delete locales[locale];
    };
  
    // ===================
    // = private methods =
    // ===================
  
    var postProcess = function(msg, namedValues, args, count) {
  
      // test for parsable interval string
      if ((/\|/).test(msg)) {
        msg = parsePluralInterval(msg, count);
      }
  
      // replace the counter
      if (typeof count === 'number') {
        msg = vsprintf(msg, [Number(count)]);
      }
  
      // if the msg string contains {{Mustache}} patterns we render it as a mini tempalate
      if (!mustacheConfig.disable && mustacheRegex.test(msg)) {
        msg = Mustache.render(msg, namedValues, {}, mustacheConfig.tags);
      }
  
      // if we have extra arguments with values to get replaced,
      // an additional substition injects those strings afterwards
      if ((/%/).test(msg) && args && args.length > 0) {
        msg = vsprintf(msg, args);
      }
  
      return msg;
    };
  
    var argsEndWithNamedObject = function(args) {
      return (args.length > 1 &&
        args[args.length - 1] !== null &&
        typeof args[args.length - 1] === 'object');
    };
  
    var parseArgv = function(args) {
      var namedValues, returnArgs;
  
      if (argsEndWithNamedObject(args)) {
        namedValues = args[args.length - 1];
        returnArgs = Array.prototype.slice.call(args, 1, -1);
      } else {
        namedValues = {};
        returnArgs = args.length >= 2 ? Array.prototype.slice.call(args, 1) : [];
      }
  
      return [namedValues, returnArgs];
    };
  
    /**
     * registers all public API methods to a given response object when not already declared
     */
    var applyAPItoObject = function(object) {
  
      var alreadySetted = true;
  
      // attach to itself if not provided
      for (var method in api) {
        if (api.hasOwnProperty(method)) {
          var alias = api[method];
  
          // be kind rewind, or better not touch anything already existing
          if (!object[alias]) {
            alreadySetted = false;
            object[alias] = i18n[method].bind(object);
          }
        }
      }
  
      // set initial locale if not set
      if (!object.locale) {
        object.locale = defaultLocale;
      }
  
      // escape recursion
      if (alreadySetted) {
        return;
      }
  
      // attach to response if present (ie. in express)
      if (object.res) {
        applyAPItoObject(object.res);
      }
  
      // attach to locals if present (ie. in express)
      if (object.locals) {
        applyAPItoObject(object.locals);
      }
    };
  
    /**
     * tries to guess locales by scanning the given directory
     */
    var guessLocales = function(directory) {
      var entries = fs.readdirSync(directory);
      var localesFound = [];
  
      for (var i = entries.length - 1; i >= 0; i--) {
        if (entries[i].match(/^\./)) continue;
        var localeFromFile = guessLocaleFromFile(entries[i]);
        if (localeFromFile) localesFound.push(localeFromFile);
      }
  
      return localesFound.sort();
    };
  
    /**
     * tries to guess locales from a given filename
     */
    var guessLocaleFromFile = function(filename) {
      var extensionRegex = new RegExp(extension + '$', 'g');
      var prefixRegex = new RegExp('^' + prefix, 'g');
  
      if (!filename) return false;
      if (prefix && !filename.match(prefixRegex)) return false;
      if (extension && !filename.match(extensionRegex)) return false;
      return filename.replace(prefix, '').replace(extensionRegex, '');
    };
  
    /**
     * @param queryLanguage - language query parameter, either an array or a string.
     * @return the first non-empty language query parameter found, null otherwise.
     */
    var extractQueryLanguage = function(queryLanguage) {
      if (Array.isArray(queryLanguage)) {
        return queryLanguage.find(lang => lang !== '' && lang);
      } 
      return typeof queryLanguage === 'string' && queryLanguage;
    };
  
    /**
     * guess language setting based on http headers
     */
  
    var guessLanguage = function(request) {
      if (typeof request === 'object') {
        var languageHeader = request.headers? request.headers['accept-language'] : undefined,
          languages = [],
          regions = [];
  
        request.languages = [defaultLocale];
        request.regions = [defaultLocale];
        request.language = defaultLocale;
        request.region = defaultLocale;
  
        // a query parameter overwrites all
        if (queryParameter && request.url) {
          var urlAsString = typeof request.url === 'string' ? request.url : request.url.toString();
          var urlObj = url.parse(urlAsString, true);
          var languageQueryParameter = urlObj.query[queryParameter];
          if (languageQueryParameter) {
            let queryLanguage = extractQueryLanguage(languageQueryParameter);
            if(queryLanguage){
              logDebug('Overriding locale from query: ' + queryLanguage);
              if (preserveLegacyCase) {
                queryLanguage = queryLanguage.toLowerCase();
              }
              return i18n.setLocale(request, queryLanguage);
            }
          }
        }
  
        // a cookie overwrites headers
        if (cookiename && request.cookies && request.cookies[cookiename]) {
          request.language = request.cookies[cookiename];
          return i18n.setLocale(request, request.language);
        }
  
        // 'accept-language' is the most common source
        if (languageHeader) {
          var acceptedLanguages = getAcceptedLanguagesFromHeader(languageHeader),
            match, fallbackMatch, fallback;
          for (var i = 0; i < acceptedLanguages.length; i++) {
            var lang = acceptedLanguages[i],
              lr = lang.split('-', 2),
              parentLang = lr[0],
              region = lr[1];
  
            // Check if we have a configured fallback set for this language.
            var fallbackLang = getFallback(lang, fallbacks);
            if (fallbackLang) {
              fallback = fallbackLang;
              // Fallbacks for languages should be inserted
              // where the original, unsupported language existed.
              var acceptedLanguageIndex = acceptedLanguages.indexOf(lang);
              var fallbackIndex = acceptedLanguages.indexOf(fallback);
              if(fallbackIndex > -1) {
                acceptedLanguages.splice(fallbackIndex, 1);
              }
              acceptedLanguages.splice(acceptedLanguageIndex + 1, 0, fallback);
            }
  
            // Check if we have a configured fallback set for the parent language of the locale.
            var fallbackParentLang = getFallback(parentLang, fallbacks);
            if (fallbackParentLang) {
              fallback = fallbackParentLang;
              // Fallbacks for a parent language should be inserted
              // to the end of the list, so they're only picked
              // if there is no better match.
              if (acceptedLanguages.indexOf(fallback) < 0) {
                acceptedLanguages.push(fallback);
              }
            }
  
            if (languages.indexOf(parentLang) < 0) {
              languages.push(parentLang.toLowerCase());
            }
            if (region) {
              regions.push(region.toLowerCase());
            }
  
            if (!match && locales[lang]) {
              match = lang;
              break;
            }
  
            if (!fallbackMatch && locales[parentLang]) {
              fallbackMatch = parentLang;
            }
          }
  
          request.language = match || fallbackMatch || request.language;
          request.region = regions[0] || request.region;
          return i18n.setLocale(request, request.language);
        }
      }
  
      // last resort: defaultLocale
      return i18n.setLocale(request, defaultLocale);
    };
  
    /**
     * Get a sorted list of accepted languages from the HTTP Accept-Language header
     */
    var getAcceptedLanguagesFromHeader = function(header) {
      var languages = header.split(','),
        preferences = {};
      return languages.map(function parseLanguagePreference(item) {
        var preferenceParts = item.trim().split(';q=');
        if (preferenceParts.length < 2) {
          preferenceParts[1] = 1.0;
        } else {
          var quality = parseFloat(preferenceParts[1]);
          preferenceParts[1] = quality ? quality : 0.0;
        }
        preferences[preferenceParts[0]] = preferenceParts[1];
  
        return preferenceParts[0];
      }).filter(function(lang) {
        return preferences[lang] > 0;
      }).sort(function sortLanguages(a, b) {
        return preferences[b] - preferences[a];
      });
    };
  
    /**
     * searches for locale in given object
     */
  
    var getLocaleFromObject = function(obj) {
      var locale;
      if (obj && obj.scope) {
        locale = obj.scope.locale;
      }
      if (obj && obj.locale) {
        locale = obj.locale;
      }
      return locale;
    };
  
    /**
     * splits and parses a phrase for mathematical interval expressions
     */
    var parsePluralInterval = function(phrase, count) {
      var returnPhrase = phrase;
      var phrases = phrase.split(/\|/);
      var intervalRuleExists = false;
  
      // some() breaks on 1st true
      phrases.some(function(p) {
        var matches = p.match(/^\s*([\(\)\[\]]+[\d,]+[\(\)\[\]]+)?\s*(.*)$/);
  
        // not the same as in combined condition
        if (matches != null && matches[1]) {
          intervalRuleExists = true;
          if (matchInterval(count, matches[1]) === true) {
            returnPhrase = matches[2];
            return true;
          }
        } else {
          // this is a other or catch all case, this only is taken into account if there is actually another rule
          if (intervalRuleExists) {
            returnPhrase = p;
          }
        }
  
      });
      return returnPhrase;
    };
  
    /**
     * test a number to match mathematical interval expressions
     * [0,2] - 0 to 2 (including, matches: 0, 1, 2)
     * ]0,3[ - 0 to 3 (excluding, matches: 1, 2)
     * [1]   - 1 (matches: 1)
     * [20,] - all numbers ≥20 (matches: 20, 21, 22, ...)
     * [,20] - all numbers ≤20 (matches: 20, 21, 22, ...)
     */
    var matchInterval = function(number, interval) {
      interval = parseInterval(interval);
      if (interval && typeof number === 'number') {
        if (interval.from.value === number) {
          return interval.from.included;
        }
        if (interval.to.value === number) {
          return interval.to.included;
        }
  
        return (Math.min(interval.from.value, number) === interval.from.value &&
          Math.max(interval.to.value, number) === interval.to.value);
      }
      return false;
    };
  
    /**
     * read locale file, translate a msg and write to fs if new
     */
    var translate = function(locale, singular, plural, skipSyncToAllFiles) {
  
      // add same key to all translations
      if (!skipSyncToAllFiles && syncFiles) {
        syncToAllFiles(singular, plural);
      }
  
      if (locale === undefined) {
        logWarn('WARN: No locale found - check the context of the call to __(). Using ' +
          defaultLocale + ' as current locale');
        locale = defaultLocale;
      }
  
      if (!locales[locale]) {
        locale = getFallback(locale, fallbacks) || locale;
      }
  
      // attempt to read when defined as valid locale
      if (!locales[locale]) {
        read(locale);
      }
  
      // fallback to default when missed
      if (!locales[locale]) {
  
        logWarn('WARN: Locale ' + locale +
          ' couldn\'t be read - check the context of the call to $__. Using ' +
          defaultLocale + ' (default) as current locale');
  
        locale = defaultLocale;
        read(locale);
      }
  
      // dotnotaction add on, @todo: factor out
      var defaultSingular = singular;
      var defaultPlural = plural;
      if (objectNotation) {
        var indexOfColon = singular.indexOf(':');
        // We compare against 0 instead of -1 because
        // we don't really expect the string to start with ':'.
        if (0 < indexOfColon) {
          defaultSingular = singular.substring(indexOfColon + 1);
          singular = singular.substring(0, indexOfColon);
        }
        if (plural && typeof plural !== 'number') {
          indexOfColon = plural.indexOf(':');
          if (0 < indexOfColon) {
            defaultPlural = plural.substring(indexOfColon + 1);
            plural = plural.substring(0, indexOfColon);
          }
        }
      }
  
      var accessor = localeAccessor(locale, singular);
      var mutator = localeMutator(locale, singular);
  
      if (plural) {
        if (accessor() == null) {
          mutator({
            'one': defaultSingular || singular,
            'other': defaultPlural || plural
          });
          write(locale);
        }
      }
  
      if (accessor() == null) {
        mutator(defaultSingular || singular);
        write(locale);
      }
  
      return accessor();
    };
  
    /**
     * initialize the same key in all locales
     * when not already existing, checked via translate
     */
    var syncToAllFiles = function(singular, plural) {
      // iterate over locales and translate again
      // this will implicitly write/sync missing keys
      // to the rest of locales
      for (var l in locales) {
        translate(l, singular, plural, true);
      }
    };
  
    /**
     * Allows delayed access to translations nested inside objects.
     * @param {String} locale The locale to use.
     * @param {String} singular The singular term to look up.
     * @param {Boolean} [allowDelayedTraversal=true] Is delayed traversal of the tree allowed?
     * This parameter is used internally. It allows to signal the accessor that
     * a translation was not found in the initial lookup and that an invocation
     * of the accessor may trigger another traversal of the tree.
     * @returns {Function} A function that, when invoked, returns the current value stored
     * in the object at the requested location.
     */
    var localeAccessor = function(locale, singular, allowDelayedTraversal) {
      // Bail out on non-existent locales to defend against internal errors.
      if (!locales[locale]) return Function.prototype;
  
      // Handle object lookup notation
      var indexOfDot = objectNotation && singular.lastIndexOf(objectNotation);
      if (objectNotation && (0 < indexOfDot && indexOfDot < singular.length - 1)) {
        // If delayed traversal wasn't specifically forbidden, it is allowed.
        if (typeof allowDelayedTraversal === 'undefined') allowDelayedTraversal = true;
        // The accessor we're trying to find and which we want to return.
        var accessor = null;
        // An accessor that returns null.
        var nullAccessor = function() {
          return null;
        };
        // Do we need to re-traverse the tree upon invocation of the accessor?
        var reTraverse = false;
        // Split the provided term and run the callback for each subterm.
        singular.split(objectNotation).reduce(function(object, index) {
          // Make the accessor return null.
          accessor = nullAccessor;
          // If our current target object (in the locale tree) doesn't exist or
          // it doesn't have the next subterm as a member...
          if (null === object || !object.hasOwnProperty(index)) {
            // ...remember that we need retraversal (because we didn't find our target).
            reTraverse = allowDelayedTraversal;
            // Return null to avoid deeper iterations.
            return null;
          }
          // We can traverse deeper, so we generate an accessor for this current level.
          accessor = function() {
            return object[index];
          };
          // Return a reference to the next deeper level in the locale tree.
          return object[index];
  
        }, locales[locale]);
        // Return the requested accessor.
        return function() {
          // If we need to re-traverse (because we didn't find our target term)
          // traverse again and return the new result (but don't allow further iterations)
          // or return the previously found accessor if it was already valid.
          return (reTraverse) ? localeAccessor(locale, singular, false)() : accessor();
        };
  
      } else {
        // No object notation, just return an accessor that performs array lookup.
        return function() {
          return locales[locale][singular];
        };
      }
    };
  
    /**
     * Allows delayed mutation of a translation nested inside objects.
     * @description Construction of the mutator will attempt to locate the requested term
     * inside the object, but if part of the branch does not exist yet, it will not be
     * created until the mutator is actually invoked. At that point, re-traversal of the
     * tree is performed and missing parts along the branch will be created.
     * @param {String} locale The locale to use.
     * @param {String} singular The singular term to look up.
     * @param [Boolean} [allowBranching=false] Is the mutator allowed to create previously
     * non-existent branches along the requested locale path?
     * @returns {Function} A function that takes one argument. When the function is
     * invoked, the targeted translation term will be set to the given value inside the locale table.
     */
    var localeMutator = function(locale, singular, allowBranching) {
      // Bail out on non-existent locales to defend against internal errors.
      if (!locales[locale]) return Function.prototype;
  
      // Handle object lookup notation
      var indexOfDot = objectNotation && singular.lastIndexOf(objectNotation);
      if (objectNotation && (0 < indexOfDot && indexOfDot < singular.length - 1)) {
        // If branching wasn't specifically allowed, disable it.
        if (typeof allowBranching === 'undefined') allowBranching = false;
        // This will become the function we want to return.
        var accessor = null;
        // An accessor that takes one argument and returns null.
        var nullAccessor = function() {
          return null;
        };
        // Fix object path.
        var fixObject = function() {
          return {};
        };
        // Are we going to need to re-traverse the tree when the mutator is invoked?
        var reTraverse = false;
        // Split the provided term and run the callback for each subterm.
        singular.split(objectNotation).reduce(function(object, index) {
          // Make the mutator do nothing.
          accessor = nullAccessor;
          // If our current target object (in the locale tree) doesn't exist or
          // it doesn't have the next subterm as a member...
          if (null === object || !object.hasOwnProperty(index)) {
            // ...check if we're allowed to create new branches.
            if (allowBranching) {
              // Fix `object` if `object` is not Object.
              if (null === object || typeof object !== 'object') {
                object = fixObject();
              }
              // If we are allowed to, create a new object along the path.
              object[index] = {};
            } else {
              // If we aren't allowed, remember that we need to re-traverse later on and...
              reTraverse = true;
              // ...return null to make the next iteration bail our early on.
              return null;
            }
          }
          // Generate a mutator for the current level.
          accessor = function(value) {
            object[index] = value;
            return value;
          };
          // Generate a fixer for the current level.
          fixObject = function() {
            object[index] = {};
            return object[index];
          };
  
          // Return a reference to the next deeper level in the locale tree.
          return object[index];
  
        }, locales[locale]);
  
        // Return the final mutator.
        return function(value) {
          // If we need to re-traverse the tree
          // invoke the search again, but allow branching
          // this time (because here the mutator is being invoked)
          // otherwise, just change the value directly.
          value = missingKeyFn(locale, value);
          return (reTraverse) ? localeMutator(locale, singular, true)(value) : accessor(value);
        };
  
      } else {
        // No object notation, just return a mutator that performs array lookup and changes the value.
        return function(value) {
          value = missingKeyFn(locale, value);
          locales[locale][singular] = value;
          return value;
        };
      }
    };
  
    /**
     * try reading a file
     */
    var read = function(locale) {
      var localeFile = {},
        file = getStorageFilePath(locale);
      try {
        logDebug('read ' + file + ' for locale: ' + locale);
        localeFile = fs.readFileSync(file);
        try {
          // parsing filecontents to locales[locale]
          locales[locale] = JSON.parse(localeFile);
        } catch (parseError) {
          logError('unable to parse locales from file (maybe ' +
            file + ' is empty or invalid json?): ', parseError);
        }
      } catch (readError) {
        // unable to read, so intialize that file
        // locales[locale] are already set in memory, so no extra read required
        // or locales[locale] are empty, which initializes an empty locale.json file
  
        // since the current invalid locale could exist, we should back it up
        if (fs.existsSync(file)) {
          logDebug('backing up invalid locale ' + locale + ' to ' + file + '.invalid');
          fs.renameSync(file, file + '.invalid');
        }
  
        logDebug('initializing ' + file);
        write(locale);
      }
    };
  
    /**
     * try writing a file in a created directory
     */
    var write = function(locale) {
      var stats, target, tmp;
  
      // don't write new locale information to disk if updateFiles isn't true
      if (!updateFiles) {
        return;
      }
  
      // creating directory if necessary
      try {
        stats = fs.lstatSync(directory);
      } catch (e) {
        logDebug('creating locales dir in: ' + directory);
        try {
          fs.mkdirSync(directory, directoryPermissions);
        } catch (e) {
          // in case of parallel tasks utilizing in same dir
          if (e.code !== 'EEXIST') throw e;
        }
      }
  
      // first time init has an empty file
      if (!locales[locale]) {
        locales[locale] = {};
      }
  
      // writing to tmp and rename on success
      try {
        target = getStorageFilePath(locale);
        tmp = target + '.tmp';
        fs.writeFileSync(tmp, JSON.stringify(locales[locale], null, indent), 'utf8');
        stats = fs.statSync(tmp);
        if (stats.isFile()) {
          fs.renameSync(tmp, target);
        } else {
          logError('unable to write locales to file (either ' +
            tmp + ' or ' + target + ' are not writeable?): ');
        }
      } catch (e) {
        logError('unexpected error writing files (either ' +
          tmp + ' or ' + target + ' are not writeable?): ', e);
      }
    };
  
    /**
     * basic normalization of filepath
     */
    var getStorageFilePath = function(locale) {
      // changed API to use .json as default, #16
      var ext = extension || '.json',
        filepath = path.normalize(directory + pathsep + prefix + locale + ext),
        filepathJS = path.normalize(directory + pathsep + prefix + locale + '.js');
      // use .js as fallback if already existing
      try {
        if (fs.statSync(filepathJS)) {
          logDebug('using existing file ' + filepathJS);
          extension = '.js';
          return filepathJS;
        }
      } catch (e) {
        logDebug('will use ' + filepath);
      }
      return filepath;
    };
  
    /**
     * Get locales with wildcard support
     */
    var getFallback = function(targetLocale, fallbacks) {
      fallbacks = fallbacks || {};
      if (fallbacks[targetLocale]) return fallbacks[targetLocale];
      var fallBackLocale = null;
      for (var key in fallbacks) {
        if(targetLocale.match(new RegExp('^' + key.replace('*', '.*') + '$'))) {
          fallBackLocale = fallbacks[key];
          break;
        }
      }
      return fallBackLocale;
    };
  
    /**
     * Logging proxies
     */
    function logDebug(msg) {
      logDebugFn(msg);
    }
  
    function logWarn(msg) {
      logWarnFn(msg);
    }
  
    function logError(msg) {
      logErrorFn(msg);
    }
  
    /**
     * Missing key function
     */
    function missingKey(locale, value) {
      return value;
    }
  
    return i18n;
  
  }());
  