├── .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 | 
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 |
--------------------------------------------------------------------------------