├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── bin ├── cli-debug.js └── cli.js ├── package.json └── src ├── context.js ├── plugin.js ├── plugins ├── fetchLocal.js ├── google.js ├── gugu.js ├── metaToResult.js ├── reduce.js ├── save.js ├── summary.js └── youdao.js ├── resolve.js ├── static.js ├── translate.js └── util.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "parserOptions": { 5 | "ecmaFeatures": { 6 | "experimentalObjectRestSpread": true, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | lib 4 | __site 5 | .vscode 6 | .DS_Store 7 | /npm-debug.log 8 | .editorconfig 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "7" 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | 3 | - fix: replace google google-translate-api with @vitalets/google-translate-api 4 | 5 | ## 0.3.1 6 | 7 | - fix: google plugin dependencies, use npm instead of github 8 | 9 | ## 0.3.0 10 | 11 | - fix: make dir fail if parent path not exist, [#9](https://github.com/ant-tool/atool-l10n/pull/9) 12 | - feat: add google plugin, [#8](https://github.com/ant-tool/atool-l10n/pull/8) 13 | 14 | ## 0.2.2 15 | 16 | - fix: typo 17 | 18 | ## 0.2.1 19 | 20 | - feat:summary warn if not find locale files 21 | 22 | ## 0.2.0 23 | 24 | - feat:summary support `sourcePattern` array supported 25 | 26 | ## 0.1.1 27 | 28 | - [break:fetchLocal] change params `skip` to true by default 29 | - [feat:metaToResult] warn if local value diff with meta value 30 | - [feat] auto generating a config file if program.config not exists 31 | - [fix:context] return {} if store.local, store.meta undefined 32 | - [fix:save] if dest not exists, mkdir 33 | - [updates] docs 34 | 35 | ## 0.1.0 36 | 37 | - let there be atool-l10n -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atool-l10n 2 | 3 | - collection all meta data generated by babel 4 | - fetch translate result from machine translation center 5 | - save the translation into local 6 | 7 | automatical solution for generating localization resource using middlewares 8 | 9 | [Demo in React](https://github.com/ant-design/intl-example) 10 | 11 | ![](https://zos.alipayobjects.com/rmsportal/JoGKhgfuFXaJNzK.gif) 12 | 13 | ## Usage 14 | 15 | 16 | - setup 17 | 18 | ```bash 19 | $ npm i atool-l10n --save-dev 20 | ``` 21 | - add `l10n.config.js` into the same root dir of your project 22 | 23 | ```js 24 | // default config 25 | module.exports = { 26 | middlewares: { 27 | summary: ['summary?sourcePattern=i18n-messages/**/*.json'], 28 | process: [ 29 | 'fetchLocal?source=locales,skip', 30 | 'metaToResult?from=defaultMessage,to=zh', 31 | 'youdao?apiname=iamatestmanx,apikey=2137553564', 32 | 'reduce?-autoPick,autoReduce[]=local,autoReduce[]=meta', 33 | ], 34 | emit: ['save?dest=locales'], 35 | }, 36 | }; 37 | ``` 38 | - run 39 | 40 | ```bash 41 | $ node_modules/.bin/atool-l10n 42 | ``` 43 | 44 | ## Options 45 | 46 | ```bash 47 | 48 | Usage: atool-l10n [options] 49 | 50 | Options: 51 | 52 | -h, --help output usage information 53 | -v, --version output the version number 54 | --config where is the config file, default is l10n.config.js 55 | 56 | ``` 57 | 58 | ## Middleware 59 | 60 | `atool-l10n` middlewares will execute one by one, with parameter `query` and shared context 61 | 62 | A middleware may looks like this: 63 | 64 | ``` 65 | export default function something(query) { 66 | this.getMeta(); 67 | this.setResult(); 68 | this.setOption(); 69 | ... 70 | } 71 | ``` 72 | 73 | - query: parameters passed to current middleware 74 | - parse from `option.config(default is l10n.config.js)` 75 | - parsed by [loader-utils](https://github.com/webpack/loader-utils) 76 | 77 | #### Context 78 | 79 | There are necessary operation and usefull methods on `this` context in the function 80 | 81 | You can check the detail API via [file](https://github.com/ant-tool/atool-l10n/tree/master/src/context.js) 82 | 83 | #### Built-in middlewares 84 | 85 | - `summary`: collect origin data generated from `babel-plugin-react-intl` 86 | 87 | |parameter|default|description| 88 | |:-------:|:-----:|:---------:| 89 | |`sourcePattern`|`'i18n-messages/**/*.json'`|where the messages json files is, specified in `babel-plugin-react-intl`, array supported| 90 | 91 | - `fetchLocal`: add local locales messages as an option of translation result 92 | 93 | |parameter|default|description| 94 | |:-------:|:-----:|:---------:| 95 | |`source`|`'locales'`|where the local locales messages file is, file name is same as language name, eg: `zh`| 96 | |`skip`|`true`|if add the id into translating skip array when all local locales messages for it is not empty| 97 | 98 | - `metaToResult`: take defaultMessage or other key of meta into an option of translation result 99 | 100 | |parameter|default|description| 101 | |:-------:|:-----:|:---------:| 102 | |`from`|`'defaultMessage'`|using which key of meta, eg: `defaultMessage`, `id`, `description`...| 103 | |`to`|`'zh'`|the language you want to save as| 104 | 105 | - `youdao`: fetch translate result from zh to en from youdao 106 | 107 | |parameter|default|description| 108 | |:-------:|:-----:|:---------:| 109 | |`apiname`|`'iamatestmanx'`|apiname you applied for machine translation from youdao| 110 | |`apikey`|`'2137553564'`|apikey you applied for machine translation from youdao| 111 | 112 | you can easily apply the apiname and apikey from [youdao](http://fanyi.youdao.com/openapi?path=data-mode) 113 | 114 | - `google`: fetch translate result from zh to en from google translate 115 | 116 | |parameter|default|description| 117 | |:-------:|:-----:|:---------:| 118 | |`from`|`'zh-cn'`|string, what languages you want to translate from| 119 | |`to`|`'en'`|string, what languages you want to translate to| 120 | |`tld`|`'cn'`|string, which TLD of Google Translate you want to use, form: `translate.google.${tld}`| 121 | 122 | use [google-translate-api](https://github.com/matheuss/google-translate-api) 123 | 124 | - `gugu`: automatic contextualized translate for multi languages of each id from gugu, only available in alibaba-network 125 | 126 | |parameter|default|description| 127 | |:-------:|:-----:|:---------:| 128 | |`from`|`'zh'`|can be an array, what languages you want to translate from| 129 | |`to`|`'en'`|can be an array, what languages you want to translate to| 130 | 131 | - `reduce`: pick the best translation, among all translation options in terminal 132 | 133 | |parameter|default|description| 134 | |:-------:|:-----:|:---------:| 135 | |`autoPick`|`false`|auto pick the value of index `[autoPick]`| 136 | |`autoReduce`|`['local', 'meta']`|auto reduce some options, the smallest index wins, values of the other index in the array will be delete| 137 | 138 | 139 | - `save`: save translation result into local locale files, which are required directly by source code 140 | 141 | |parameter|default|description| 142 | |:-------:|:-----:|:---------:| 143 | |`dest`|`'locales'`|save locales messages into where, message file named by language name| -------------------------------------------------------------------------------- /bin/cli-debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | 4 | require('devtool/bin/spawn')([ 5 | path.join(__dirname, './cli.js'), 6 | ].concat(process.argv.slice(2))); 7 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const isAli = require('is-ali-env'); 7 | const log = require('spm-log'); 8 | 9 | program 10 | .version(require(path.join(__dirname, '../package.json')).version, '-v, --version') 11 | .option('--config ', 'where is the config file, default is l10n.config.js', 'l10n.config.js') 12 | .parse(process.argv); 13 | 14 | const configPath = path.join(process.cwd(), program.config); 15 | 16 | isAli().then(function(flag) { 17 | if (program.config && !fs.existsSync(configPath)) { 18 | const defaultOptions = { 19 | middlewares: { 20 | summary: ['summary?sourcePattern=i18n-messages/**/*.json'], 21 | process: [ 22 | 'fetchLocal?source=locales,skip', 23 | 'metaToResult?from=defaultMessage,to=zh', 24 | flag ? 'gugu?from[]=zh,to[]=en' : 'youdao?apiname=iamatestmanx,apikey=2137553564', 25 | 'reduce?-autoPick,autoReduce[]=local,autoReduce[]=meta', 26 | ], 27 | emit: ['save?dest=locales'], 28 | }, 29 | }; 30 | log.info('initial', `generating a config file ${program.config} in cwd...`); 31 | fs.writeFileSync( 32 | configPath, 33 | `module.exports = ${JSON.stringify(defaultOptions, null, 2)}` 34 | ); 35 | } 36 | require('../lib/translate')(Object.assign({}, 37 | require(configPath), 38 | { cwd: process.cwd() } 39 | )); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atool-l10n", 3 | "version": "0.4.0", 4 | "description": "automatical solution for generating localization resource using middlewares", 5 | "author": "jaredleechn ", 6 | "homepage": "https://github.com/ant-tool/atool-l10n", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ant-tool/atool-l10n" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/ant-tool/atool-l10n/issues" 13 | }, 14 | "license": "MIT", 15 | "main": "index.js", 16 | "files": [ 17 | "lib", 18 | "bin" 19 | ], 20 | "bin": { 21 | "atool-l10n": "./bin/cli.js", 22 | "atool-l10n-debug": "./bin/cli-debug.js" 23 | }, 24 | "scripts": { 25 | "start": "concurrently 'npm run dev' 'npm run doc'", 26 | "build": "rm -rf lib && babel src --out-dir lib --ignore __tests__", 27 | "dev": "./node_modules/.bin/babel src --out-dir lib --watch --ignore __tests__ -s", 28 | "lint": "eslint --ext .js src", 29 | "pub": "npm run build && npm publish", 30 | "test": "npm run lint", 31 | "doc": "atool-doc" 32 | }, 33 | "dependencies": { 34 | "@vitalets/google-translate-api": "^2.8.0", 35 | "babel-polyfill": "^6.9.1", 36 | "co": "~4.6.0", 37 | "co-request": "~1.0.0", 38 | "commander": "~2.9.0", 39 | "ensure-dir": "^0.1.0", 40 | "glob": "~7.0.3", 41 | "inquirer": "~1.0.3", 42 | "is-ali-env": "^0.1.2", 43 | "loader-utils": "~0.2.15", 44 | "query-string": "^4.2.2", 45 | "resolve": "~1.1.7", 46 | "spm-log": "~0.1.3" 47 | }, 48 | "devDependencies": { 49 | "atool-build": "^0.7.8", 50 | "atool-doc": "0.x", 51 | "babel-cli": "~6.6.5", 52 | "babel-eslint": "^6.0.4", 53 | "babel-plugin-add-module-exports": "~0.1.2", 54 | "babel-preset-es2015": "~6.6.0", 55 | "babel-preset-stage-0": "~6.5.0", 56 | "concurrently": "^2.1.0", 57 | "devtool": "^2.0.2", 58 | "dora": "^0.3.2", 59 | "eslint": "^2.7.0", 60 | "eslint-config-airbnb": "^6.2.0", 61 | "eslint-plugin-react": "^4.3.0", 62 | "pre-commit": "~1.1.2" 63 | }, 64 | "babel": { 65 | "presets": [ 66 | "es2015", 67 | "stage-0" 68 | ], 69 | "plugins": [ 70 | "add-module-exports" 71 | ] 72 | }, 73 | "pre-commit": [ 74 | "lint" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import { LANGS } from './static'; 2 | import * as utils from './util'; 3 | import log from 'spm-log'; 4 | 5 | export default function createContext(context, others) { 6 | return { 7 | context, 8 | LANGS, 9 | _store: { 10 | meta: {}, 11 | local: {}, 12 | list: [], 13 | skip: [], 14 | result: {}, 15 | }, 16 | getMeta(id) { 17 | return id ? this._store.meta[id] || {} : this._store.meta; 18 | }, 19 | getLocal(lang) { 20 | return lang ? this._store.local[lang] || {} : this._store.local; 21 | }, 22 | getList() { 23 | return this._store.list; 24 | }, 25 | getSkip() { 26 | return this._store.skip; 27 | }, 28 | addSkip(id) { 29 | if (this._store.skip.indexOf(id) === -1) { 30 | this._store.skip.push(id); 31 | } 32 | }, 33 | getTodo() { 34 | return this._store.list.filter(id => this._store.skip.indexOf(id) === -1); 35 | }, 36 | getResult(id) { 37 | return id ? this._store.result[id] : Object.keys(this._store.result).map(each => ({ 38 | id: each, 39 | ...this._store.result[each], 40 | })); 41 | }, 42 | setResult(id, result) { 43 | if (id) { 44 | this._store.result[id] = { 45 | ...this._store.result[id], 46 | ...result, 47 | }; 48 | } else { 49 | log.error('set Result', 'set result failed'); 50 | } 51 | }, 52 | removeOption(id, lang, key) { 53 | delete this._store.result[id][lang][key]; 54 | }, 55 | setOption(id, lang, option) { 56 | this._store.result[id] = this._store.result[id] || {}; 57 | const record = this._store.result[id]; 58 | 59 | switch (this.typeOf(record[lang])) { 60 | case 'object': 61 | record[lang] = { 62 | ...record[lang], 63 | ...option, 64 | }; 65 | break; 66 | case 'array': 67 | log.error('setOptionForLang', 'type error'); 68 | break; 69 | case 'string': 70 | record[lang] = { 71 | [record[lang]]: record[lang], 72 | ...option, 73 | }; 74 | break; 75 | case 'undefined': 76 | record[lang] = { 77 | ...option, 78 | }; 79 | break; 80 | default: 81 | log.error('setOptionForLang', 'unSupported type'); 82 | } 83 | }, 84 | getOptionValues(id, lang) { 85 | const record = this._store.result[id][lang]; 86 | switch (this.typeOf(record)) { 87 | case 'object': 88 | return Object.keys(record).map(key => record[key]); 89 | case 'string': 90 | return [record]; 91 | case 'undefined': 92 | return []; 93 | default: 94 | log.error('getResultValues', 'unSupported type'); 95 | return false; 96 | } 97 | }, 98 | ...utils, 99 | ...others, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import resolve from './resolve'; 3 | import { parseQuery } from 'loader-utils'; 4 | import { isAbsolute, isRelative, isProvided } from './util'; 5 | 6 | export function resolvePlugin(_pluginName, resolveDir, cwd = process.cwd()) { 7 | let plugin; 8 | let name = false; 9 | let query = {}; 10 | 11 | if (typeof _pluginName === 'string') { 12 | const [pluginName, _query] = _pluginName.split('?'); 13 | name = pluginName; 14 | if (_query) { 15 | query = parseQuery(`?${_query}`); 16 | } 17 | if (isRelative(pluginName)) { 18 | plugin = require(join(cwd, pluginName)); 19 | } else if (isAbsolute(pluginName)) { 20 | plugin = require(pluginName); 21 | } else if (isProvided(pluginName)) { 22 | plugin = require(join(__dirname, 'plugins', pluginName)); 23 | } else { 24 | const pluginPath = resolve(pluginName, resolveDir); 25 | if (!pluginPath) { 26 | throw new Error(`[Error] ${pluginName} not found in ${resolveDir}`); 27 | } 28 | plugin = require(pluginPath); 29 | } 30 | } 31 | 32 | return { 33 | query, 34 | plugin, 35 | name, 36 | }; 37 | } 38 | 39 | export function resolvePlugins(pluginNames, resolveDir, cwd) { 40 | return pluginNames.map(pluginName => resolvePlugin(pluginName, resolveDir, cwd)); 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/fetchLocal.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | import { join } from 'path'; 3 | 4 | export default function skipLocal(query) { 5 | const { source, skip } = { 6 | source: 'locales', 7 | skip: true, 8 | ...query, 9 | }; 10 | 11 | const langs = Object.keys(this.LANGS) 12 | .filter(lang => this.existsResolve(join(this.context.cwd, source, lang))); 13 | 14 | if (!(langs.length)) { 15 | log.info('fetchLocal', 'no local files need to be processed'); 16 | return; 17 | } 18 | 19 | log.info('fetchLocal', `from ${source}, language: ${langs}, skip ${skip}`); 20 | 21 | const localCollect = langs 22 | .reduce((collect, lang) => { 23 | const content = require(join(this.context.cwd, source, lang)); 24 | this.getList().forEach(id => { 25 | if (content[id]) { 26 | this.setOption(id, lang, { 27 | local: content[id], 28 | }); 29 | } 30 | }); 31 | return { 32 | ...collect, 33 | [lang]: content, 34 | }; 35 | }, {}); 36 | this._store.local = localCollect; 37 | 38 | if (skip) { 39 | this.getTodo().forEach(id => { 40 | const needSkip = langs.every(lang => this.getLocal(lang)[id]); 41 | if (needSkip) { 42 | this.addSkip(id); 43 | this.setResult(id, langs.reduce((collect, lang) => ({ 44 | ...collect, 45 | [lang]: this.getLocal(lang)[id], 46 | }), {})); 47 | log.warn('add to skip', id); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/plugins/google.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | import translate from '@vitalets/google-translate-api'; 3 | 4 | // https://github.com/matheuss/google-translate-response-spec 5 | 6 | async function words(q, params) { 7 | return translate(q, { ...params }) 8 | .then(res => res.text) 9 | .catch(err => log.error(err)); 10 | } 11 | 12 | export default async function google(query) { 13 | const { from = 'zh-cn', to = 'en', tld = 'cn' } = query; 14 | 15 | const todo = this.getTodo(); 16 | if (!(todo.length)) { 17 | log.info('google', 'no element need to be processed'); 18 | return; 19 | } 20 | 21 | log.info('google translate starts'); 22 | 23 | for (const id of todo) { 24 | const texts = this.getOptionValues(id, 'zh'); 25 | if (!texts.length) { 26 | log.warn('google', `skip ${id} from zh to en`); 27 | } else { 28 | for (const q of texts) { 29 | const result = await words(q, { from, to, tld }); 30 | log.info('google: zh -> en', `${q} -> ${result}`); 31 | 32 | this.setOption(id, 'en', { 33 | [`google, ${q}`]: result, 34 | }); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/plugins/gugu.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | import request from 'co-request'; 3 | import { stringify } from 'query-string'; 4 | 5 | 6 | async function words(url, text, from = 'zh', to = 'en') { 7 | const res = await request(`${url}?${stringify({ from, to, text })}`); 8 | return JSON.parse(res.body).data; 9 | } 10 | 11 | export default async function gugu(query) { 12 | const { from, to } = { 13 | from: 'zh', 14 | to: 'en', 15 | ...query, 16 | }; 17 | 18 | const todo = this.getTodo(); 19 | if (!(todo.length)) { 20 | log.info('gugu', 'no element need to be processed'); 21 | return; 22 | } 23 | 24 | log.info('gugu', `from ${from} to ${to}`); 25 | 26 | const url = 'http://gugu.alipay.net/suggestion'; 27 | 28 | for (const id of todo) { 29 | const text = this.getOptionValues(id, from); 30 | if (!text.length) { 31 | log.warn('gugu', `skip ${id} from ${to} to ${to}`); 32 | } else { 33 | const result = await words(url, text, from, to); 34 | result.forEach(each => { 35 | log.info(`gugu:${from} -> ${each.to}`, `${each.text} -> ${each.translate}`); 36 | this.setOption(id, each.to, { 37 | [`gugu, ${each.text}`]: each.translate, 38 | }); 39 | }); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/plugins/metaToResult.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | export default function metaToResult(query) { 3 | const { from, to } = { 4 | from: 'defaultMessage', 5 | to: 'zh', 6 | ...query, 7 | }; 8 | 9 | const todo = this.getTodo(); 10 | if (!(todo.length)) { 11 | log.info('metaToResult', 'no element need to be processed'); 12 | return; 13 | } 14 | 15 | log.info('metaToResult', `from meta.${from} to result.${to}`); 16 | 17 | todo.forEach(id => { 18 | if (this.getLocal(to)[id] && this.getMeta(id)[from] !== this.getLocal(to)[id]) { 19 | log.warn(`multiple ${to}@${id}`, 20 | `${this.getMeta(id)[from]}(meta) !== ${this.getLocal(to)[id]}(local)`); 21 | } 22 | this.setOption(id, to, { 23 | meta: this.getMeta(id)[from], 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/plugins/reduce.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | import inquirer from 'inquirer'; 3 | 4 | async function select(target, message = 'pick the best one') { 5 | const optionKeys = Object.keys(target); 6 | if (optionKeys.length > 1) { 7 | const answer = await (inquirer.prompt({ 8 | name: 'value', 9 | type: 'list', 10 | message, 11 | choices: Object.keys(target).map(each => ({ 12 | value: target[each], 13 | name: `${target[each]} - from ${each}`, 14 | })), 15 | })); 16 | return answer.value; 17 | } else if (optionKeys.length === 1) { 18 | log.info('only one', `pick ${target[optionKeys[0]]}`); 19 | return target[optionKeys[0]]; 20 | } 21 | return false; 22 | } 23 | 24 | export default async function reduce(query) { 25 | const { autoPick, autoReduce } = { 26 | autoPick: false, 27 | autoReduce: ['local', 'meta'], 28 | ...query, 29 | }; 30 | 31 | const todo = this.getTodo(); 32 | if (!(todo.length)) { 33 | log.info('reduce', 'no element need to be processed'); 34 | return; 35 | } 36 | 37 | log.info('reduce', `autoPick ${autoPick} autoReduce ${autoReduce}`); 38 | 39 | for (const id of todo) { 40 | const record = this.getResult(id); 41 | 42 | const langs = Object.keys(record); 43 | for (const lang of langs) { 44 | if (autoPick && record[lang][autoPick]) { 45 | log.warn(`autoPick ${lang}@${id} with ${autoPick}`, record[lang][autoPick]); 46 | this.setResult(id, { 47 | [lang]: record[lang][autoPick], 48 | }); 49 | } else { 50 | if (autoReduce && autoReduce.length) { 51 | autoReduce.reduceRight((end, start) => { 52 | if (record[lang][start]) { 53 | // refactor to parse split 54 | this.removeOption(id, lang, end); 55 | return start; 56 | } 57 | return end; 58 | }); 59 | } 60 | if (this.typeOf(record[lang]) === 'object') { 61 | const success = await select(record[lang], `pinking ${lang} for ${id}`); 62 | this.setResult(id, { 63 | [lang]: success, 64 | }); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/plugins/save.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import log from 'spm-log'; 3 | import { join } from 'path'; 4 | import ensureDir from 'ensure-dir'; 5 | 6 | function sortKey(obj) { 7 | return Object.keys(obj) 8 | .sort((a, b) => a.toString().toLowerCase() > b.toString().toLowerCase()) 9 | .reduce((collect, current) => ({ 10 | ...collect, 11 | [current]: obj[current], 12 | }), {}); 13 | } 14 | 15 | 16 | export default function save(query) { 17 | const { dest } = { 18 | dest: 'locales', 19 | ...query, 20 | }; 21 | log.info('save task', `dest is ${dest}`); 22 | 23 | const results = this.getResult(); 24 | 25 | const saveResult = {}; 26 | 27 | const dir = join(this.context.cwd, dest); 28 | 29 | results.forEach(result => { 30 | const langs = Object.keys(result).filter(lang => lang !== 'id'); 31 | langs.forEach(lang => { 32 | saveResult[lang] = saveResult[lang] || { 33 | lang, 34 | content: {}, 35 | file: `${join(dir, lang)}.json`, 36 | }; 37 | 38 | saveResult[lang].content[result.id] = result[lang]; 39 | }); 40 | }); 41 | 42 | ensureDir(dir).then(() => { 43 | Object.keys(saveResult).forEach(item => { 44 | writeFileSync( 45 | saveResult[item].file, 46 | JSON.stringify(sortKey(saveResult[item].content), null, 2) 47 | ); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/plugins/summary.js: -------------------------------------------------------------------------------- 1 | import { sync } from 'glob'; 2 | import { join } from 'path'; 3 | import log from 'spm-log'; 4 | 5 | function parseMeta(cwd, sourcePattern) { 6 | const patternArray = Array.isArray(sourcePattern) ? sourcePattern : [sourcePattern]; 7 | try { 8 | return patternArray.map(pattern => sync(join(cwd, pattern))) 9 | .reduce((a, b) => a.concat(b), []) 10 | .map(file => require(file)) 11 | .reduce((a, b) => a.concat(b), []); 12 | } catch (e) { 13 | log.error('summary', e); 14 | return false; 15 | } 16 | } 17 | 18 | export default function summary(query) { 19 | const { sourcePattern } = { 20 | sourcePattern: 'i18n-messages/**/*.json', 21 | ...query, 22 | }; 23 | log.info('summary', sourcePattern); 24 | 25 | const metaArray = parseMeta(this.context.cwd, sourcePattern); 26 | 27 | if (metaArray.length === 0) { 28 | log.warn('summary', 'no local files find, run webpack with babel-plugin-react-intl first'); 29 | } 30 | 31 | const meta = this.arrayToObject(metaArray, 'id'); 32 | this._store.meta = meta; 33 | this._store.list = Object.keys(meta); 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/youdao.js: -------------------------------------------------------------------------------- 1 | import log from 'spm-log'; 2 | import request from 'co-request'; 3 | import { stringify } from 'query-string'; 4 | 5 | 6 | async function words(url, params) { 7 | let times = 5; 8 | while (times > 0) { 9 | times --; 10 | try { 11 | const { body } = await request(`${url}?${stringify(params)}`); 12 | return JSON.parse(body).translation; 13 | } catch (e) { 14 | continue; 15 | } 16 | } 17 | log.error('youdao', 'apikey is forbidden, apply another in http://fanyi.youdao.com/openapi?path=data-mode'); 18 | return false; 19 | } 20 | 21 | export default async function youdao(query) { 22 | const { apiname: keyfrom, apikey: key } = { 23 | apiname: 'iamatestmanx', 24 | apikey: '2137553564', 25 | ...query, 26 | }; 27 | 28 | const todo = this.getTodo(); 29 | if (!(todo.length)) { 30 | log.info('youdao', 'no element need to be processed'); 31 | return; 32 | } 33 | 34 | log.info('youdao', `using apiname: ${keyfrom}, apikey: ${key}`); 35 | const url = 'http://fanyi.youdao.com/openapi.do'; 36 | 37 | for (const id of todo) { 38 | const texts = this.getOptionValues(id, 'zh'); 39 | if (!texts.length) { 40 | log.warn('youdao', `skip ${id} from zh to en`); 41 | } else { 42 | for (const q of texts) { 43 | const result = await words(url, { 44 | keyfrom, 45 | key, 46 | type: 'data', 47 | doctype: 'json', 48 | version: '1.1', 49 | q, 50 | }); 51 | result.forEach(each => { 52 | log.info('youdao: zh -> en', `${q} -> ${each}`); 53 | this.setOption(id, 'en', { 54 | [`youdao, ${q}`]: each, 55 | }); 56 | }); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/resolve.js: -------------------------------------------------------------------------------- 1 | import resolve from 'resolve'; 2 | 3 | function tryResolve(name, dirname) { 4 | let result; 5 | try { 6 | result = resolve.sync(name, { 7 | basedir: dirname, 8 | }); 9 | } catch (e) {} // eslint-disable-line no-empty 10 | return result; 11 | } 12 | 13 | export default function (name, _resolveDir) { 14 | const resolveDir = Array.isArray(_resolveDir) ? _resolveDir : [_resolveDir]; 15 | 16 | let result; 17 | resolveDir.some(dirname => { 18 | result = tryResolve(`intl-plugin-${name}`, dirname) || tryResolve(name, dirname); 19 | return result; 20 | }); 21 | return result; 22 | } 23 | -------------------------------------------------------------------------------- /src/static.js: -------------------------------------------------------------------------------- 1 | exports.LANGS = { 2 | en: { full: 'english', name: '英语' }, 3 | zh: { full: 'Simplified Chinese', name: '简体中文' }, 4 | zt: { full: 'Traditional Chinese', name: '繁体中文' }, 5 | es: { full: 'spanish', name: '西班牙语' }, 6 | pt: { full: 'portuguese', name: '葡萄牙语' }, 7 | fr: { full: 'french', name: '法语' }, 8 | de: { full: 'german', name: '德语' }, 9 | it: { full: 'italian', name: '意大利语' }, 10 | ru: { full: 'russian', name: '俄语' }, 11 | ja: { full: 'japanese', name: '日语' }, 12 | ko: { full: 'korean', name: '韩语' }, 13 | ar: { full: 'arabic', name: '阿拉伯语' }, 14 | tr: { full: 'turkish', name: '土耳其语' }, 15 | th: { full: 'thai', name: '泰语' }, 16 | vi: { full: 'vietnamese', name: '越南语' }, 17 | nl: { full: 'dutch', name: '荷兰语' }, 18 | he: { full: 'hebrew', name: '希伯来语' }, 19 | id: { full: 'indonesian', name: '印尼语' }, 20 | pl: { full: 'polish', name: '波兰语' }, 21 | hi: { full: 'hindi', name: '印地语' }, 22 | uk: { full: 'urainian', name: '乌克兰语' }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/translate.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { resolvePlugins } from './plugin'; 3 | import createContext from './context'; 4 | import log from 'spm-log'; 5 | 6 | export default function translate(options) { 7 | const { cwd, middlewares } = { 8 | cwd: process.cwd(), 9 | ...options, 10 | }; 11 | 12 | const resolveDir = [cwd]; 13 | const pluginNames = Array.isArray(middlewares) 14 | ? middlewares 15 | : Object.keys(middlewares).reduce((a, b) => a.concat(middlewares[b]), []); 16 | 17 | const plugins = resolvePlugins(pluginNames, resolveDir, cwd); 18 | 19 | const pluginThis = createContext({ 20 | cwd, 21 | }); 22 | 23 | log.info('atool-l10n', plugins.map(plugin => plugin.name).join(', ')); 24 | plugins.reduce((a, b) => { 25 | if (a instanceof Promise) { 26 | return a.then(result => b.plugin.call(pluginThis, b.query, result)); 27 | } 28 | return b.plugin.call(pluginThis, b.query, a); 29 | }, Promise.resolve()); 30 | } 31 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export function arrayToObject(arr, key) { 5 | return arr.reduce((collection, current) => ({ 6 | ...collection, 7 | [current[key]]: { 8 | ...current, 9 | }, 10 | }), {}); 11 | } 12 | 13 | export function isRelative(filepath) { 14 | return filepath.charAt(0) === '.'; 15 | } 16 | 17 | export function isAbsolute(filepath) { 18 | return filepath.charAt(0) === '/'; 19 | } 20 | 21 | export function isProvided(filepath) { 22 | return existsSync(join(__dirname, 'plugins', `${filepath}.js`)); 23 | } 24 | 25 | export function existsResolve(path) { 26 | try { 27 | require.resolve(path); 28 | } catch (e) { 29 | return false; 30 | } 31 | return true; 32 | } 33 | 34 | export function isObject(something) { 35 | return something && something.constructor === Object; 36 | } 37 | 38 | export function format(target, prefix) { 39 | const before = prefix || ''; 40 | if (Array.isArray(target)) { 41 | return target.reduce((collection, item) => ({ 42 | ...collection, 43 | [`${item} - ${before}`]: item, 44 | }), {}); 45 | } else if (isObject(target)) { 46 | return Object.keys(target).reduce((collection, key) => ({ 47 | ...collection, 48 | [`${target[key]} - ${before} ${key}`]: target[key], 49 | }), {}); 50 | } 51 | return { 52 | [`${target} - ${before}`]: target, 53 | }; 54 | } 55 | 56 | 57 | export function maxKeys(arr) { 58 | return Object.keys(arr[0]); 59 | } 60 | 61 | 62 | export function typeOf(thing) { 63 | if (thing && thing.constructor === Object) { 64 | return 'object'; 65 | } else if (Array.isArray(thing)) { 66 | return 'array'; 67 | } 68 | return typeof thing; 69 | } 70 | --------------------------------------------------------------------------------