core/Interpreter.js

const path = require('path')
const fs = require('fs')

const { requireAll, isDir, Collection } = require('../util')

/** Locale manager and string parser */
class Interpreter extends Collection {
  /**
   * Creates a new Localisations instance
   * @arg {Client} Client instance
   */
  constructor (client) {
    super()
    this._client = client
    this._cached = []
  }

  /**
   * Registers a string table for a locale
   * @arg {String|Object|Array} strings An object, array or relative path to a folder or file to load strings from
   * @arg {String} [locale] The name of the locale. If none is supplied, `strings` will be treated as an object mapping locale names to string tables
   */
  register (strings, loc) {
    switch (typeof strings) {
      case 'string': {
        const filepath = path.isAbsolute(strings) ? strings : path.join(process.cwd(), strings)
        if (!fs.existsSync(filepath)) {
          throw new Error(`Folder path ${filepath} does not exist`)
        }
        this._cached.push(filepath)
        const stringMap = isDir(filepath) ? requireAll(filepath) : require(filepath)
        return this.register(stringMap)
      }
      case 'object': {
        if (Array.isArray(strings)) {
          for (const pair of strings) {
            if (typeof pair[0] !== 'string') continue
            this.set(pair[0], Object.assign(this.get(pair[0]) || {}, pair[1]))
          }
          return this
        }
        if (!loc) {
          for (const lang in strings) {
            this.set(lang, Object.assign(this.get(lang) || {}, strings[lang]))
          }
        } else {
          this.set(loc, Object.assign(this.get(loc) || {}, strings))
        }
        return this
      }
      default: {
        throw new Error('Path supplied is not an object or string')
      }
    }
  }

  /**
   * Reloads locale files (only those that have been added from by file path)
   * @returns {Client}
   */
  reload () {
    for (const filepath of this._cached) {
      this._client.unload(filepath)
      this._cached.shift()
      this.register(filepath)
    }
    return this
  }

  /**
   * Locate a nested key within an object
   * @arg {String} key The key to find
   * @arg {Object} obj The object to search from
   * @returns {?String}
   */
  locate (fullkey, obj) {
    let keys = fullkey.split('.')
    let val = obj[keys.shift()]
    if (!val) return null
    for (let key of keys) {
      if (!val[key]) return val
      val = val[key]
      if (Array.isArray(val)) return val.join('\n')
    }
    return val || null
  }

  /**
   * Gets strings under a group key from a locale
   * @arg {String} [key='common'] The string group to find
   * @arg {String} [locale='en'] The locale to find
   * @returns {?String}
   */
  getStrings (key = 'common', locale = 'en') {
    if (!this.has(locale)) locale = 'en'
    return this.locate(key, this.get(locale))
  }

  /**
   * Parses a string and interpolating tags from a supplied object
   * @arg {String} string String to do interpolation
   * @arg {Object} options Object containing tags to interpolate into the string
   * @returns {String}
   */
  shift (string, options) {
    if (!string) return string
    return string.split(' ').map(str => (
      str.replace(/\{\{(.+)\}\}/gi, (matched, key) => (
        this.locate(key, options) || matched
      ))
    )).join(' ')
  }

  /**
   * Parses a string, converting keys to the matching locale string, with interpolation
   * @arg {String} string The string to parse
   * @arg {String} [group='default'] The string group to use
   * @arg {String} [locale='en'] The locale to use
   * @arg {Object} [options] Object containing tags to interpolate into the string
   * @returns {String}
   */
  parse (string, group = 'default', locale = 'en', options = {}) {
    if (!string) return string
    return string.split(' ').map(str => (
      str.replace(/\{\{(.+)\}\}/gi, (matched, key) => {
        const g = key.startsWith('%') ? 'default.' : group + '.'
        key = key.startsWith('%') ? key.substr(1) : key
        let val = this.getStrings(`${g}${key}`, locale)
        return typeof val === 'string' ? this.shift(val, options) : matched
      })
    )).join(' ')
  }
}

module.exports = Interpreter