├── .travis.yml ├── README.md ├── analytics.js ├── test ├── interactive.js ├── test.js └── common.js ├── .gitignore ├── LICENSE ├── package.json ├── common.js ├── interactive.js ├── languages.js └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '5' 5 | - '4' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vertaler [![Build Status](https://travis-ci.org/matheuss/vertaler.svg?branch=master)](https://travis-ci.org/matheuss/vertaler) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 2 | 3 | Translate texts in your command line with Google Translate -------------------------------------------------------------------------------- /analytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by matheus on 5/9/16. 3 | */ 4 | 5 | const Insight = require('insight'); 6 | const pkg = require('./package.json'); 7 | 8 | const insight = new Insight({ 9 | trackingCode: 'UA-75832795-5', 10 | pkg 11 | }); 12 | module.exports.init = callback => { 13 | if (insight.optOut === undefined) { 14 | insight.track('downloaded'); 15 | insight.askPermission(null, callback); 16 | } else { 17 | callback(); 18 | } 19 | }; 20 | 21 | module.exports.track = () => { 22 | insight.track(Array.from(arguments).join('/')); 23 | }; 24 | -------------------------------------------------------------------------------- /test/interactive.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | const interactive = require('../interactive')({from: 'en', to: 'pt'}); 4 | 5 | test('translate some text from english to portuguese', async t => { 6 | await interactive.exec('en:pt'); 7 | const translation = await interactive.exec('this is a text in english'); 8 | t.is(translation, 'este é um texto em Inglês'); 9 | }); 10 | 11 | test('clear the screen with the \'clear\' command', async t => { 12 | await interactive.exec('clear'); 13 | t.pass(); 14 | }); 15 | 16 | // no ideas on how to to this 17 | // already tried: RobotJS 18 | test.todo('clear the screen by pressing ctrl+l'); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .idea 40 | 41 | links.txt 42 | tmp 43 | 44 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matheus Fernandes – http://matheus.top – hi@matheus.top 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import execa from 'execa'; 2 | import test from 'ava'; 3 | 4 | test('show error when called w/o any arguments', async t => { 5 | t.throws(execa('../index.js'), /Missing arguments/); 6 | }); 7 | 8 | test('translate from english to dutch using options', async t => { 9 | const ret = await execa('../index.js', ['-f', 'en', '-t', 'nl', 'translator']); 10 | 11 | t.is(ret.stdout, 'vertaler'); 12 | }); 13 | 14 | test('translate from auto to dutch using options', async t => { 15 | const ret = await execa('../index.js', ['-f', 'auto', '-t', 'nl', 'translator']); 16 | 17 | t.is(ret.stdout, 'vertaler'); 18 | }); 19 | 20 | test('translate from english to dutch using colon notation', async t => { 21 | const ret = await execa('../index.js', ['en:nl', 'translator']); 22 | 23 | t.is(ret.stdout, 'vertaler'); 24 | }); 25 | 26 | test('translate from auto to dutch using colon notation', async t => { 27 | const ret = await execa('../index.js', [':nl', 'translator']); 28 | 29 | t.is(ret.stdout, 'vertaler'); 30 | }); 31 | 32 | test('show help when trying to translate from english to ?', async t => { 33 | t.throws(execa('../index.js', ['en:', 'translator']), /Missing\/invalid target language/); 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vertaler", 3 | "version": "1.0.0-alpha.2", 4 | "description": "Translate texts in your command line with Google Translate", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Matheus Fernandes", 8 | "email": "npm@matheus.top", 9 | "url": "http://matheus.top" 10 | }, 11 | "engines": { 12 | "node": ">=4.0.0" 13 | }, 14 | "main": "index.js", 15 | "bin": "index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/matheuss/vertaler.git" 19 | }, 20 | "scripts": { 21 | "test": "xo && ava" 22 | }, 23 | "dependencies": { 24 | "ansi-styles": "^2.2.1", 25 | "chalk": "^1.1.3", 26 | "commander": "^2.9.0", 27 | "configstore": "^2.0.0", 28 | "google-translate-api": "^2.2.2", 29 | "insight": "^0.8.1", 30 | "ora": "^0.2.3", 31 | "request": "^2.72.0", 32 | "update-notifier": "^1.0.2", 33 | "vorpal": "^1.11.2" 34 | }, 35 | "keywords": [ 36 | "translate", 37 | "translator", 38 | "google", 39 | "translate", 40 | "cli" 41 | ], 42 | "devDependencies": { 43 | "ava": "^0.15.2", 44 | "execa": "^0.4.0", 45 | "xo": "^0.16.0" 46 | }, 47 | "xo": { 48 | "space": 4, 49 | "esnext": true, 50 | "ignores": [ 51 | "languages.js" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | const translate = require('google-translate-api'); 2 | /** 3 | * The parsed languages 4 | * @typedef ParsedLanguages 5 | * @property {string|undefined} from – the 'from' language 6 | * @property {string|undefined} to – the 'to' language 7 | */ 8 | 9 | /** 10 | * parseLanguages - parses languages from a 11 | * 12 | * @param {string} str – the string that will be parsed 13 | * @return {ParsedLanguages} 14 | */ 15 | function parseLanguages(str) { 16 | const langColonLang = /([a-z]{2,})?(-)?([a-z]{2})?:([a-z]{2,})?(-)?([a-z]{2})?/i; 17 | const result = {}; 18 | 19 | const match = langColonLang.exec(str); 20 | if (match && match.index === 0) { // index == 0 is needed to avoid a match on, e.g., 'testing this: regex' 21 | result.from = match[1]; 22 | if (match[2]) { // e.g. 'zh-cn' 23 | result.from += match[2] + match[3]; 24 | } 25 | 26 | result.to = match[4]; 27 | if (match[5]) { 28 | result.to += match[5] + match[6]; 29 | } 30 | } 31 | if (result.from) { 32 | result.from = translate.languages.getCode(result.from); 33 | } 34 | if (result.to) { 35 | result.to = translate.languages.getCode(result.to); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | module.exports.parseLanguages = parseLanguages; 42 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import {parseLanguages} from '../common'; 4 | 5 | test('parse langs from \'en:nl\'', t => { 6 | const result = parseLanguages('en:nl'); 7 | t.is(result.from, 'en'); 8 | t.is(result.to, 'nl'); 9 | }); 10 | 11 | test('parse langs from \'zh-cn:en\'', t => { 12 | const result = parseLanguages('zh-cn:en'); 13 | t.is(result.from, 'zh-cn'); 14 | t.is(result.to, 'en'); 15 | }); 16 | 17 | test('parse langs from \'en:zh-tw\'', t => { 18 | const result = parseLanguages('en:zh-tw'); 19 | t.is(result.from, 'en'); 20 | t.is(result.to, 'zh-tw'); 21 | }); 22 | 23 | test('parse langs from \'en:nl some text here\'', t => { 24 | const result = parseLanguages('en:nl some text here'); 25 | t.is(result.from, 'en'); 26 | t.is(result.to, 'nl'); 27 | }); 28 | 29 | test('parse langs from \'en:nl :some:text:here:\'', t => { 30 | const result = parseLanguages('en:nl :some:text:here:'); 31 | t.is(result.from, 'en'); 32 | t.is(result.to, 'nl'); 33 | }); 34 | 35 | test('parse lang from \'en:\'', t => { 36 | const result = parseLanguages('en:'); 37 | t.is(result.from, 'en'); 38 | t.is(result.to, undefined); 39 | }); 40 | 41 | test('parse lang from \'en: some text\'', t => { 42 | const result = parseLanguages('en: some text'); 43 | t.is(result.from, 'en'); 44 | t.is(result.to, undefined); 45 | }); 46 | 47 | test('parse lang from \':nl\'', t => { 48 | const result = parseLanguages(':nl'); 49 | t.is(result.from, undefined); 50 | t.is(result.to, 'nl'); 51 | }); 52 | 53 | test('parse lang from \':nl some text\'', t => { 54 | const result = parseLanguages(':nl some text'); 55 | t.is(result.from, undefined); 56 | t.is(result.to, 'nl'); 57 | }); 58 | -------------------------------------------------------------------------------- /interactive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const translate = require('google-translate-api'); 4 | const vorpal = require('vorpal')(); 5 | 6 | const languages = require('./languages'); 7 | 8 | /** 9 | * Clears the screen 10 | */ 11 | const clearScreen = () => { 12 | process.stdout.write('\u001B[2J\u001B[0;0f'); 13 | vorpal.ui.refresh(); 14 | }; 15 | 16 | const onKeyPress = obj => { 17 | const key = obj.e.key; 18 | 19 | // If ctrl + l 20 | if (key.ctrl === true && key.shift === false && key.meta === false && ['l', 'L'].indexOf(key.name) > -1) { 21 | clearScreen(); 22 | } 23 | }; 24 | 25 | /** 26 | * 27 | * @param lang the ISO 639-1 code of the desired language 28 | * @returns {Vorpal} 29 | */ 30 | const setDelimiterLang = lang => vorpal.delimiter(`${languages[lang]}>`); 31 | 32 | /** 33 | * 34 | * @param program the commander.js program 35 | */ 36 | const interactive = program => { 37 | vorpal.command('clear', 'Clears the screen') 38 | .action((args, callback) => { 39 | clearScreen(); 40 | callback(); 41 | }); 42 | 43 | vorpal 44 | .catch('[text...]') 45 | .action((command, callback) => { 46 | let text = command.text.join(' '); 47 | if (text.charAt(2) === ':') { // 'en:pt' or 'en:pt test' 48 | let langs = text.substring(0, 5); 49 | langs = langs.split(':'); 50 | 51 | program.from = langs[0]; 52 | program.to = langs[1]; 53 | 54 | setDelimiterLang(program.from); 55 | 56 | text = text.substring(6); 57 | } 58 | 59 | if (text) { 60 | return new Promise((resolve, reject) => { 61 | translate(command.text.join(' '), {from: program.from, to: program.to}).then(res => { 62 | vorpal.activeCommand.log(res.text); 63 | resolve(res.text); 64 | }).catch(err => { 65 | if (err.code === 'BAD_NETWORK') { 66 | vorpal.activeCommand.log('Please check your internet connection'); 67 | reject('Please check your internet connection'); 68 | } else { // TODO 69 | vorpal.activeCommand.log('Unknown error'); 70 | reject('Unknown error'); 71 | } 72 | }); 73 | }); 74 | } 75 | callback(); 76 | }); 77 | 78 | vorpal.on('keypress', onKeyPress); 79 | setDelimiterLang(program.from); 80 | vorpal.show(); 81 | 82 | return vorpal; 83 | }; 84 | 85 | module.exports = interactive; 86 | -------------------------------------------------------------------------------- /languages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by matheus on 5/15/16. 3 | * 4 | * Generated from https://translate.google.com 5 | * 6 | * The languages that Google Translate supports (as of 5/15/16) alongside with their ISO 639-1 codes 7 | * See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 8 | */ 9 | 10 | var langs = { 11 | af: 'Afrikaans', 12 | sq: 'Albanian', 13 | ar: 'Arabic', 14 | hy: 'Armenian', 15 | az: 'Azerbaijani', 16 | eu: 'Basque', 17 | be: 'Belarusian', 18 | bn: 'Bengali', 19 | bs: 'Bosnian', 20 | bg: 'Bulgarian', 21 | ca: 'Catalan', 22 | ceb: 'Cebuano', 23 | ny: 'Chichewa', 24 | 'zh-CN': 'Chinese Simplified', 25 | 'zh-TW': 'Chinese Traditional', 26 | co: 'Corsican', 27 | hr: 'Croatian', 28 | cs: 'Czech', 29 | da: 'Danish', 30 | nl: 'Dutch', 31 | en: 'English', 32 | eo: 'Esperanto', 33 | et: 'Estonian', 34 | tl: 'Filipino', 35 | fi: 'Finnish', 36 | fr: 'French', 37 | fy: 'Frisian', 38 | gl: 'Galician', 39 | ka: 'Georgian', 40 | de: 'German', 41 | el: 'Greek', 42 | gu: 'Gujarati', 43 | ht: 'Haitian Creole', 44 | ha: 'Hausa', 45 | haw: 'Hawaiian', 46 | iw: 'Hebrew', 47 | hi: 'Hindi', 48 | hmn: 'Hmong', 49 | hu: 'Hungarian', 50 | is: 'Icelandic', 51 | ig: 'Igbo', 52 | id: 'Indonesian', 53 | ga: 'Irish', 54 | it: 'Italian', 55 | ja: 'Japanese', 56 | jw: 'Javanese', 57 | kn: 'Kannada', 58 | kk: 'Kazakh', 59 | km: 'Khmer', 60 | ko: 'Korean', 61 | ku: 'Kurdish (Kurmanji)', 62 | ky: 'Kyrgyz', 63 | lo: 'Lao', 64 | la: 'Latin', 65 | lv: 'Latvian', 66 | lt: 'Lithuanian', 67 | lb: 'Luxembourgish', 68 | mk: 'Macedonian', 69 | mg: 'Malagasy', 70 | ms: 'Malay', 71 | ml: 'Malayalam', 72 | mt: 'Maltese', 73 | mi: 'Maori', 74 | mr: 'Marathi', 75 | mn: 'Mongolian', 76 | my: 'Myanmar (Burmese)', 77 | ne: 'Nepali', 78 | no: 'Norwegian', 79 | ps: 'Pashto', 80 | fa: 'Persian', 81 | pl: 'Polish', 82 | pt: 'Portuguese', 83 | ma: 'Punjabi', 84 | ro: 'Romanian', 85 | ru: 'Russian', 86 | sm: 'Samoan', 87 | gd: 'Scots Gaelic', 88 | sr: 'Serbian', 89 | st: 'Sesotho', 90 | sn: 'Shona', 91 | sd: 'Sindhi', 92 | si: 'Sinhala', 93 | sk: 'Slovak', 94 | sl: 'Slovenian', 95 | so: 'Somali', 96 | es: 'Spanish', 97 | su: 'Sudanese', 98 | sw: 'Swahili', 99 | sv: 'Swedish', 100 | tg: 'Tajik', 101 | ta: 'Tamil', 102 | te: 'Telugu', 103 | th: 'Thai', 104 | tr: 'Turkish', 105 | uk: 'Ukrainian', 106 | ur: 'Urdu', 107 | uz: 'Uzbek', 108 | vi: 'Vietnamese', 109 | cy: 'Welsh', 110 | xh: 'Xhosa', 111 | yi: 'Yiddish', 112 | yo: 'Yoruba', 113 | zu: 'Zulu' 114 | }; 115 | 116 | module.exports = langs; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const chalk = require('chalk'); 4 | const ora = require('ora'); 5 | const program = require('commander'); 6 | const styles = require('ansi-styles'); 7 | const translate = require('google-translate-api'); 8 | const updateNotifier = require('update-notifier'); 9 | 10 | const common = require('./common'); 11 | const interactive = require('./interactive'); 12 | 13 | const pkg = require('./package.json'); 14 | const analytics = require('./analytics'); 15 | const languages = require('./languages'); 16 | 17 | const notifier = updateNotifier({pkg}); 18 | const spinner = ora('Translating...'); 19 | 20 | if (notifier.update) { 21 | const update = notifier.update; 22 | let message = `Update available! ${chalk.red(update.current)} → ${chalk.green(update.latest)} \n`; 23 | message += `Run ${chalk.magenta('npm i -g vertaler')} to update :)`; 24 | const boxenOpts = { 25 | padding: 1, 26 | margin: 1, 27 | borderColor: 'green', 28 | borderStyle: { 29 | topLeft: '.', 30 | topRight: '.', 31 | bottomLeft: '\'', 32 | bottomRight: '\'', 33 | horizontal: '-', 34 | vertical: '|' 35 | } 36 | }; 37 | notifier.notify({message, boxenOpts}); 38 | } 39 | 40 | analytics.init(() => { 41 | program 42 | .version(pkg.version) 43 | .usage(':targetLang> ') 44 | .option('-f, --from ', 'Source language', 'auto') 45 | .option('-t, --to ', 'Target language', undefined) 46 | .option('-i , --interactive', 'Interactive mode', false); 47 | 48 | program.on('--help', () => { 49 | console.log(' Examples:'); 50 | console.log(''); 51 | console.log(' $ vertaler en:nl Hi'); 52 | console.log(' $ vertaler :en Hoi'); 53 | console.log(' └── automatic source lang detection'); 54 | console.log(' $ vertaler -f en -t nl Hi'); 55 | console.log(' $ vertaler --from en --to nl Hi'); 56 | console.log(''); 57 | }); 58 | 59 | program.parse(process.argv); 60 | 61 | if (program.interactive) { 62 | return interactive(program); 63 | } 64 | if (!program.args.length) { // called w/o any arguments 65 | console.log(chalk.red('Missing arguments. See \'vertaler --help\'')); 66 | process.exit(1); 67 | } 68 | if (!program.to) { // program.from is 'auto' by default, so does not need to be checked 69 | Object.assign(program, common.parseLanguages(program.args.shift())); 70 | if (!program.to) { 71 | console.log(chalk.red('Missing/invalid target language. See \'vertaler --help\'')); 72 | process.exit(1); 73 | } 74 | } 75 | if (!program.args.length) { 76 | console.log(chalk.red('There\'s nothing to translate. See \'vertaler --help\'')); 77 | process.exit(1); 78 | } 79 | 80 | spinner.start(); 81 | analytics.track('translate', program.from, program.to); 82 | translate(program.args.join(' '), {from: program.from, to: program.to}).then(res => { 83 | let msg = ''; 84 | 85 | spinner.stop(); 86 | 87 | if (res.from.language.didYouMean) { 88 | msg = `${chalk.bold('Did you mean to translate from ')}`; 89 | msg += `${chalk.green.bold(languages[res.from.language.iso])}${chalk.bold('?')}`; 90 | } 91 | 92 | let correctedSpelling = ''; 93 | if (res.from.text.value !== '') { 94 | if (chalk.supportsColor) { 95 | correctedSpelling = res.from.text.value.replace(/\[/g, styles.bold.open + styles.green.open); 96 | correctedSpelling = correctedSpelling.replace(/]/g, styles.bold.close + styles.green.close); 97 | } else { 98 | correctedSpelling = res.from.text.value.replace(/\[/g, ''); 99 | correctedSpelling = correctedSpelling.replace(/]/g, ''); 100 | } 101 | } 102 | 103 | if (res.from.text.autoCorrected) { 104 | if (msg !== '') { 105 | msg += '\n'; 106 | } 107 | msg += `${chalk.bold('Auto corrected to:')} ${correctedSpelling}`; 108 | } else if (res.from.text.didYouMean) { 109 | if (msg !== '') { 110 | msg += '\n'; 111 | } 112 | msg += `${chalk.bold('Did you mean:')} ${correctedSpelling}`; 113 | } 114 | 115 | if (msg !== '') { 116 | msg += '\n'; 117 | } 118 | 119 | msg += res.text; 120 | 121 | console.log(msg); 122 | }).catch(err => { 123 | let msg = ''; 124 | if (err.code === 'BAD_REQUEST') { 125 | msg = chalk.red('Ops. Our code is no longer working – Google servers are rejecting our requests.\n' + 126 | 'Feel free to open an issue @ https://git.io/g-trans-api'); 127 | } else if (err.code === 'BAD_NETWORK') { 128 | msg = chalk.red('Please check your internet connection.'); 129 | } 130 | 131 | spinner.stop(); 132 | console.error(msg); 133 | process.exit(1); 134 | }); 135 | }); 136 | --------------------------------------------------------------------------------