DemocracyOS/democracyos

View on GitHub
bin/dos-translate

Summary

Maintainability
Test Coverage
#!/usr/bin/env node

var path = require('path')
var fs = require('fs')
var arrDiff = require('arr-diff')
var jsonFile = require('json-file-plus')
var Batch = require('batch')
var nomnom = require('nomnom')
var config = require('../lib/config')

// Load the list of available languages.
var LANGS_FOLDER = path.resolve(__dirname, '../lib/translations/lib')
var languages = fs.readdirSync(LANGS_FOLDER).map(function (v) {
  return v.replace('.json', '')
})

/*
 * Extend proto of JSONFile object
 */

;(function () {
  function sort (old) {
    if (typeof old !== 'object' && old !== null) {
      return old
    }
    var copy = Array.isArray(old) ? [] : {}
    var keys = Object.keys(old).sort()
    keys.forEach(function (key) {
      copy[key] = sort(old[key])
    })
    return copy
  }

  jsonFile.JSONFile.prototype.sort = function () {
    this.data = sort(this.data)
  }
})()

/*
 * Utility Functions
 */

function getLangPath (lang) {
  return path.join(LANGS_FOLDER, lang + '.json')
}

function getLang (lang) {
  return require(getLangPath(lang))
}

function getLangKeys (lang) {
  return Object.keys(getLang(lang))
}

function forEachLanguageFile (langs, cb) {
  return langs.forEach(function (lang) {
    jsonFile(getLangPath(lang), function (err, file) {
      if (err) throw err
      cb.call(file, lang, file)
    })
  })
}

/*
 * Commands
 */

function commandListMissing (opts) {
  if (!opts.on || !opts.on.length) {
    opts.on = languages.filter(function (v) { return v !== opts.base })
  }

  var baseKeys = getLangKeys(opts.base)

  opts.on.forEach(function (lang) {
    var onKeys = getLangKeys(lang)
    var missing = arrDiff(baseKeys, onKeys)

    if (!missing.length) return

    console.log('Missing ' + missing.length + ' keys on `' + lang + '`:')

    missing.forEach(function (k) {
      console.log('  - ' + k)
    })
  })
}

function commandTranslateMissing (opts) {
  if (!opts.on || !opts.on.length) {
    opts.on = languages.filter(function (v) { return v !== opts.base })
  }

  var googleTranslate = require('google-translate')(config.googleAPIKey)
  var baseLang = getLang(opts.base)
  var baseKeys = getLangKeys(opts.base)

  var missings = {}
  var langs = opts.on.filter(function (lang) {
    var onKeys = getLangKeys(lang)
    var missing = arrDiff(baseKeys, onKeys)

    if (missing.length) missings[lang] = missing

    return !!missing.length
  })

  forEachLanguageFile(langs, function (lang, file) {
    console.log('Translating ' + missings[lang].length + ' missing keys on `' + lang + '`:')

    var formatRegex = /%[a-z]|{[a-z-]+}/
    var batch = new Batch()

    batch.concurrency(3)

    missings[lang].forEach(function (k) {
      if (formatRegex.test(baseLang[k])) {
        console.warn('  Couldnt translate `' + k + '`, it has a variable defined with {var} format.')
        return
      }

      batch.push(function (done) {
        googleTranslate.translate(baseLang[k], opts.base, lang, function (err, translation) {
          if (err) {
            console.error(err)
            return done()
          }
          var t = {}
          t[k] = translation.translatedText
          file.set(t)
          console.log('  "' + k + '": "' + translation.translatedText + '"')
          done()
        })
      })
    })

    batch.end(function (err) {
      if (err) throw err
      if (opts.test) {
        console.log('Done!')
      } else {
        file.sort()
        file.save().then(function () {
          console.log('Done!')
        }).catch(function (err) {
          throw err
        })
      }
    })
  })
}

function commandDeleteKey (opts) {
  var langs = opts.on || languages

  forEachLanguageFile(langs, function (lang, file) {
    console.log('Deleting keys from `' + lang + '`:')

    opts.key.forEach(function (k) {
      if (!file.data[k]) return
      file.data[k] = undefined
      console.log('  "' + k + '"')
    })

    file.save().then(function () {
      console.log('Done!')
    }).catch(function (err) {
      throw err
    })
  })
}

function commandRenameKey (opts) {
  var langs = opts.on || languages

  forEachLanguageFile(langs, function (lang, file) {
    console.log('Renaming keys from `' + lang + '`:')

    if (opts.key.length !== opts.name.length) {
      throw new Error('Have to provide the same amount of keys and names.')
    }

    var edited = false

    opts.key.forEach(function (k, i) {
      if (!file.data[k]) return

      var name = opts.name[i]
      var value = file.data[k]

      if (k === name) return

      file.data[k] = undefined
      file.data[name] = value

      edited = true

      console.log('  "' + k + '" => "' + name + '"')
    })

    if (edited) {
      file.sort()
      file.save().then(function () {
        console.log('Done!')
      }).catch(function (err) {
        throw err
      })
    }
  })
}

function commandListExtra (opts) {
  var on = opts.on || languages

  var baseKeys = getLangKeys(opts.base)

  on.forEach(function (lang) {
    var keys = getLangKeys(lang)
    var extra = arrDiff(keys, baseKeys)

    if (!extra.length) return

    console.log('Extra ' + extra.length + ' keys on `' + lang + '`:')

    extra.forEach(function (k) {
      console.log('  - ' + k)
    })
  })
}

function commandDeleteExtra (opts) {
  var on = opts.on || languages

  var baseKeys = getLangKeys(opts.base)

  on.forEach(function (lang) {
    var keys = getLangKeys(lang)
    var extra = arrDiff(keys, baseKeys)

    if (!extra.length) return

    commandDeleteKey({
      on: [lang],
      key: extra
    })
  })
}

function commandSort (opts) {
  var langs = opts.lang || languages

  forEachLanguageFile(langs, function (lang, file) {
    file.sort()
    file.save().then(function () {
      console.log('`' + lang + '` sorted.')
    }).catch(function (err) {
      throw err
    })
  })
}

function commandDeleteRepeatedValues (opts) {
  var on = opts.on || languages

  var baseLanguage = getLang(opts.base)

  on.forEach(function (lang) {
    var language = getLang(lang)
    var toDelete = []

    Object.keys(language).forEach(function (key) {
      if (!baseLanguage[key]) return
      if (baseLanguage[key] === language[key]) toDelete.push(key)
    })

    if (toDelete.length) {
      commandDeleteKey({
        on: [lang],
        key: toDelete
      })
    }
  })
}

/*
 * Definition of terminal interface
 */

nomnom.command('list-missing')
  .option('base', {
    abbr: 'b',
    help: 'Base language to use the keys as reference.',
    choices: languages,
    required: true
  })
  .option('on', {
    abbr: 'o',
    help: 'Target language, will default to all other languages.',
    choices: languages,
    required: false,
    list: true
  })
  .callback(commandListMissing)
  .help('Find all the missing keys.')

nomnom.command('translate-missing')
  .option('base', {
    abbr: 'b',
    help: 'Base language to use as reference.',
    choices: languages,
    required: true
  })
  .option('on', {
    abbr: 'o',
    help: 'Target language, will default to all other languages.',
    choices: languages,
    required: false,
    list: true
  })
  .option('test', {
    abbr: 't',
    flag: true,
    help: 'Print translations but not save them.',
    required: false
  })
  .callback(commandTranslateMissing)
  .help('Translate missing keys.')

nomnom.command('delete-key')
  .option('key', {
    abbr: 'k',
    help: 'Key(s) to delete.',
    required: true,
    list: true
  })
  .option('on', {
    abbr: 'o',
    help: 'Target language(s), will default to all languages.',
    choices: languages,
    required: false,
    list: true
  })
  .callback(commandDeleteKey)
  .help('Delete a given key from selected language files.')

nomnom.command('rename-key')
  .option('key', {
    abbr: 'k',
    help: 'Key(s) to rename.',
    required: true,
    list: true
  })
  .option('name', {
    abbr: 'n',
    help: 'New name to set the key(s), keep in mind to have the same order as `--key` param.',
    required: true,
    list: true
  })
  .option('on', {
    abbr: 'o',
    help: 'Target language(s), will default to all languages.',
    choices: languages,
    required: false,
    list: true
  })
  .callback(commandRenameKey)
  .help('Rename a given key from selected language files.')

nomnom.command('list-extra')
  .option('base', {
    abbr: 'b',
    help: 'Language to use as the base for the comparisons.',
    required: true
  })
  .option('on', {
    abbr: 'n',
    help: 'Language(s) you want to list extra keys from. Defaults to all.',
    list: true
  })
  .callback(commandListExtra)
  .help('Prints a list of the keys of `--on` that are not present on `--base`.')

nomnom.command('delete-extra')
  .option('base', {
    abbr: 'b',
    help: 'Language to use as the base for the comparisons.',
    required: true
  })
  .option('on', {
    abbr: 'n',
    help: 'Language(s) you want to delete extra keys. Defaults to all.',
    list: true
  })
  .callback(commandDeleteExtra)
  .help('Delete extra keys from `--on` that are not present on `--base`.')

nomnom.command('delete-repeated-values')
  .option('base', {
    abbr: 'b',
    help: 'Language to use as the base for the comparisons.',
    required: true
  })
  .option('on', {
    abbr: 'o',
    help: 'Language(s) you want to delete keys from. Defaults to all.',
    choices: languages,
    required: false,
    list: true
  })
  .callback(commandDeleteRepeatedValues)
  .help('Delete keys from `--on` with the same values as `--base`.')

nomnom.command('sort')
  .option('lang', {
    abbr: 'l',
    help: 'Languages to sort. Defaults to all.',
    list: true
  })
  .callback(commandSort)
  .help('Sort the file keys from a language(s).')

nomnom.nom()