auto-lang 1.1.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +5 -0
- package/README.md +13 -7
- package/dist/index.js +145 -0
- package/index.html +12 -0
- package/package.json +24 -6
- package/src/index.ts +61 -0
- package/src/interfaces/input-params.interface.ts +10 -0
- package/src/utils/index.ts +185 -0
- package/src/utils/logger.ts +11 -0
- package/src/utils/store.ts +22 -0
- package/src/utils/validation.ts +55 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +41 -0
- package/src/index.js +0 -138
- package/src/utils/Logger.mjs +0 -7
- package/src/utils/validation.mjs +0 -37
package/.prettierrc
ADDED
package/README.md
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
Generate translation files for multiple languages.
|
4
4
|
|
5
5
|
Write once for a single language and automatically get translated json files for others.
|
6
|
+
|
7
|
+
**NEW**: Show the difference between two translation files.
|
8
|
+
|
6
9
|
## Installation
|
7
10
|
### Using npm
|
8
11
|
$ npm install auto-lang
|
@@ -36,13 +39,16 @@ Or, using yarn:
|
|
36
39
|
|
37
40
|
#### Options
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
```
|
43
|
+
-V, --version output the version number
|
44
|
+
-f, --from <lang> language to translate from
|
45
|
+
-t, --to <lang...> languages to translate to (seperated by space)
|
46
|
+
-d, --dir <directory> directory containing the language files (default: "translations")
|
47
|
+
-s, --skip-existing skip existing keys during translation
|
48
|
+
-g, --gen-type <lang> generate types from language file
|
49
|
+
-d, --diff <lang...> show missing keys between two language files
|
50
|
+
-h, --help display help for command
|
51
|
+
```
|
46
52
|
|
47
53
|
**Note:** `<lang>` must be a valid [ISO 639-1 language code](https://localizely.com/iso-639-1-list/).
|
48
54
|
|
package/dist/index.js
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import { Command as $ } from "commander";
|
3
|
+
import c from "path";
|
4
|
+
import { existsSync as g, promises as d } from "fs";
|
5
|
+
import { createSpinner as y } from "nanospinner";
|
6
|
+
import O from "json-to-ts";
|
7
|
+
import x from "prettier";
|
8
|
+
import T from "chalk";
|
9
|
+
import F from "translate";
|
10
|
+
class p {
|
11
|
+
static error(e) {
|
12
|
+
console.log(`${T.red(e)}`);
|
13
|
+
}
|
14
|
+
static log(e) {
|
15
|
+
console.log(e);
|
16
|
+
}
|
17
|
+
}
|
18
|
+
class k {
|
19
|
+
inputParams = {
|
20
|
+
from: "",
|
21
|
+
to: [],
|
22
|
+
dir: "",
|
23
|
+
skipExisting: !1,
|
24
|
+
genType: "",
|
25
|
+
diff: ["", ""]
|
26
|
+
};
|
27
|
+
setInputParams(e) {
|
28
|
+
this.inputParams = e;
|
29
|
+
}
|
30
|
+
getInputParams() {
|
31
|
+
return this.inputParams;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
const l = new k();
|
35
|
+
async function b(o, e, t) {
|
36
|
+
const { from: s, skipExisting: i } = l.getInputParams();
|
37
|
+
for (let [n, r] of Object.entries(o))
|
38
|
+
if (typeof r == "object")
|
39
|
+
e[n] = e[n] || {}, await b(r, e[n], t);
|
40
|
+
else
|
41
|
+
try {
|
42
|
+
e[n] && i || (e[n] = await F(r, {
|
43
|
+
from: s,
|
44
|
+
to: t
|
45
|
+
}));
|
46
|
+
} catch (a) {
|
47
|
+
console.log(`
|
48
|
+
`), p.error(a.message), process.exit(1);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
const P = (o) => {
|
52
|
+
const { dir: e } = l.getInputParams();
|
53
|
+
return new Promise(async (t, s) => {
|
54
|
+
let i = {};
|
55
|
+
const n = await K(), r = c.join(process.cwd(), e, `${o}.json`);
|
56
|
+
g(r) && (i = await u(r)), await b(n, i, o), t(i);
|
57
|
+
});
|
58
|
+
};
|
59
|
+
async function h() {
|
60
|
+
const { dir: o, genType: e } = l.getInputParams(), t = y("Creating language type file").start(), s = u(
|
61
|
+
c.join(process.cwd(), o, `${e}.json`)
|
62
|
+
), i = O(s, {
|
63
|
+
rootName: "GlobalTranslationType"
|
64
|
+
}), n = c.join(process.cwd(), o, "types");
|
65
|
+
g(n) || await d.mkdir(n);
|
66
|
+
const r = c.join(n, "index"), a = `
|
67
|
+
type NestedKeyOf<ObjectType extends object> = {
|
68
|
+
[Key in keyof ObjectType & string]: ObjectType[Key] extends object
|
69
|
+
? // @ts-ignore
|
70
|
+
\`\${Key}.\${NestedKeyOf<ObjectType[Key]>}\`
|
71
|
+
: \`\${Key}\`
|
72
|
+
}[keyof ObjectType & string]
|
73
|
+
|
74
|
+
export type GlobalTranslation = NestedKeyOf<GlobalTranslationType>;
|
75
|
+
|
76
|
+
${i.join(`
|
77
|
+
|
78
|
+
`)}
|
79
|
+
`, f = await x.format(a, {
|
80
|
+
parser: "typescript"
|
81
|
+
});
|
82
|
+
await d.writeFile(r, f), t.success({ text: "Language type file created" });
|
83
|
+
}
|
84
|
+
async function I() {
|
85
|
+
const { to: o, dir: e } = l.getInputParams();
|
86
|
+
let t, s, i;
|
87
|
+
for (let n of o)
|
88
|
+
s = c.join(process.cwd(), e, `${n}.json`), t = y(`Translating to ${n}...`).start(), i = await P(n), await d.writeFile(s, JSON.stringify(i, null, 2)), t.success({ text: "Complete" });
|
89
|
+
}
|
90
|
+
async function u(o) {
|
91
|
+
return JSON.parse(await d.readFile(o, { encoding: "utf-8" }));
|
92
|
+
}
|
93
|
+
function K() {
|
94
|
+
const { dir: o, from: e } = l.getInputParams(), t = c.join(process.cwd(), o, `${e}.json`);
|
95
|
+
return u(t);
|
96
|
+
}
|
97
|
+
function v(o, e) {
|
98
|
+
const t = [];
|
99
|
+
function s(i, n, r = "") {
|
100
|
+
for (let [a, f] of Object.entries(i)) {
|
101
|
+
const m = r ? `${r}.${a}` : a;
|
102
|
+
typeof f == "object" ? n[a] ? s(f, n[a], m) : t.push(m) : n[a] || t.push(m);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
return s(o, e), t;
|
106
|
+
}
|
107
|
+
async function C() {
|
108
|
+
const o = y("Comparing language files").start(), { dir: e, diff: t } = l.getInputParams(), s = t[0], i = t[1], n = await u(
|
109
|
+
c.join(process.cwd(), e, `${s}.json`)
|
110
|
+
), r = await u(
|
111
|
+
c.join(process.cwd(), e, `${i}.json`)
|
112
|
+
), a = v(n, r);
|
113
|
+
p.log(`
|
114
|
+
Missing keys in ${i}.json compared to ${s}.json
|
115
|
+
`), p.log(a.join(`
|
116
|
+
`) || "No missing keys"), o.success({ text: "Comparison complete" });
|
117
|
+
}
|
118
|
+
function N(o) {
|
119
|
+
Object.keys(o).length || (p.error('Invalid arguments. Use "--help" for usage'), process.exit(1));
|
120
|
+
const { to: e, from: t, dir: s, genType: i, diff: n } = o;
|
121
|
+
(t && !e || e && !t) && (p.error('"--from" and "--to" are dependent options'), process.exit(1));
|
122
|
+
const r = c.join(process.cwd(), s, `${t}.json`), a = c.join(process.cwd(), s, `${i}.json`);
|
123
|
+
if (!g(r) && t && (p.error(`File "${r}" not found`), process.exit(1)), !g(a) && i && (p.error(`File "${a}" not found`), process.exit(1)), n) {
|
124
|
+
n.length !== 2 && (p.error('"--diff" option requires two languages'), process.exit(1));
|
125
|
+
const [f, m] = n, j = c.join(process.cwd(), s, `${f}.json`), w = c.join(process.cwd(), s, `${m}.json`);
|
126
|
+
g(j) || (p.error(`File "${j}" not found`), process.exit(1)), g(w) || (p.error(`File "${w}" not found`), process.exit(1));
|
127
|
+
}
|
128
|
+
}
|
129
|
+
async function S() {
|
130
|
+
const e = (await u(c.join(process.cwd(), "package.json"))).version, t = new $();
|
131
|
+
t.name("auto-lang").description("Generate translation files for multiple languages (i18n)").version(e).option("-f, --from <lang>", "language to translate from").option(
|
132
|
+
"-t, --to <lang...>",
|
133
|
+
"languages to translate to (seperated by space)"
|
134
|
+
).option(
|
135
|
+
"-d, --dir <directory>",
|
136
|
+
"directory containing the language files",
|
137
|
+
"translations"
|
138
|
+
).option("-s, --skip-existing", "skip existing keys during translation").option("-g, --gen-type <lang>", "generate types from language file").option(
|
139
|
+
"-d, --diff <lang...>",
|
140
|
+
"show missing keys between two language files"
|
141
|
+
).parse(), N(t.opts()), l.setInputParams(t.opts());
|
142
|
+
const { from: s, to: i, genType: n, diff: r } = l.getInputParams();
|
143
|
+
s && i && await I(), n && await h(), r && await C();
|
144
|
+
}
|
145
|
+
S();
|
package/index.html
ADDED
package/package.json
CHANGED
@@ -1,12 +1,20 @@
|
|
1
1
|
{
|
2
2
|
"name": "auto-lang",
|
3
|
-
"version": "
|
3
|
+
"version": "2.0.1",
|
4
4
|
"description": "Automatically create language json files for internationalization",
|
5
5
|
"main": "./src/index.js",
|
6
|
+
"type": "module",
|
6
7
|
"scripts": {
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
9
|
+
"dev": "rm -rf dist && vite build --mode watch",
|
10
|
+
"build": "rm -rf dist && vite build"
|
8
11
|
},
|
9
|
-
"keywords": [
|
12
|
+
"keywords": [
|
13
|
+
"Internationalization",
|
14
|
+
"node",
|
15
|
+
"i18n",
|
16
|
+
"translate"
|
17
|
+
],
|
10
18
|
"author": "Lafen Lesley <lesleytech6@gmail.com> (https://github.com/lesleytech/)",
|
11
19
|
"homepage": "https://github.com/lesleytech/auto-lang#readme",
|
12
20
|
"repository": {
|
@@ -15,15 +23,25 @@
|
|
15
23
|
},
|
16
24
|
"license": "ISC",
|
17
25
|
"bin": {
|
18
|
-
"auto-lang": "./
|
26
|
+
"auto-lang": "./dist/index.js"
|
19
27
|
},
|
20
|
-
"type": "module",
|
21
28
|
"dependencies": {
|
22
29
|
"chalk": "^5.0.1",
|
23
30
|
"commander": "^9.4.0",
|
24
31
|
"json-to-ts": "^1.7.0",
|
25
32
|
"nanospinner": "^1.1.0",
|
26
|
-
"prettier": "^2.7.1",
|
27
33
|
"translate": "^1.4.1"
|
34
|
+
},
|
35
|
+
"devDependencies": {
|
36
|
+
"@types/chalk": "^2.2.4",
|
37
|
+
"@types/node": "^22.10.1",
|
38
|
+
"@webpack-cli/generators": "^3.0.7",
|
39
|
+
"prettier": "^3.4.2",
|
40
|
+
"ts-loader": "^9.5.1",
|
41
|
+
"typescript": "^5.7.2",
|
42
|
+
"vite": "^6.0.3",
|
43
|
+
"vite-plugin-checker": "^0.8.0",
|
44
|
+
"webpack": "^5.97.1",
|
45
|
+
"webpack-cli": "^5.1.4"
|
28
46
|
}
|
29
47
|
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import { Command } from 'commander';
|
4
|
+
import path from 'path';
|
5
|
+
|
6
|
+
import {
|
7
|
+
createDeclarationFile,
|
8
|
+
parseJsonFile,
|
9
|
+
showLangDiff,
|
10
|
+
translateFile,
|
11
|
+
} from './utils';
|
12
|
+
import { validateUserInput } from './utils/validation';
|
13
|
+
import { store } from './utils/store';
|
14
|
+
|
15
|
+
async function main() {
|
16
|
+
const pjson = await parseJsonFile(path.join(process.cwd(), 'package.json'));
|
17
|
+
const appVersion = pjson.version;
|
18
|
+
|
19
|
+
const program = new Command();
|
20
|
+
|
21
|
+
program
|
22
|
+
.name('auto-lang')
|
23
|
+
.description('Generate translation files for multiple languages (i18n)')
|
24
|
+
.version(appVersion)
|
25
|
+
.option('-f, --from <lang>', 'language to translate from')
|
26
|
+
.option(
|
27
|
+
'-t, --to <lang...>',
|
28
|
+
'languages to translate to (seperated by space)',
|
29
|
+
)
|
30
|
+
.option(
|
31
|
+
'-d, --dir <directory>',
|
32
|
+
'directory containing the language files',
|
33
|
+
'translations',
|
34
|
+
)
|
35
|
+
.option('-s, --skip-existing', 'skip existing keys during translation')
|
36
|
+
.option('-g, --gen-type <lang>', 'generate types from language file')
|
37
|
+
.option(
|
38
|
+
'-d, --diff <lang...>',
|
39
|
+
'show missing keys between two language files',
|
40
|
+
)
|
41
|
+
.parse();
|
42
|
+
|
43
|
+
validateUserInput(program.opts());
|
44
|
+
store.setInputParams(program.opts());
|
45
|
+
|
46
|
+
const { from, to, genType, diff } = store.getInputParams();
|
47
|
+
|
48
|
+
if (from && to) {
|
49
|
+
await translateFile();
|
50
|
+
}
|
51
|
+
|
52
|
+
if (genType) {
|
53
|
+
await createDeclarationFile();
|
54
|
+
}
|
55
|
+
|
56
|
+
if (diff) {
|
57
|
+
await showLangDiff();
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
main();
|
@@ -0,0 +1,185 @@
|
|
1
|
+
import path from 'path';
|
2
|
+
import { existsSync, promises as fs } from 'fs';
|
3
|
+
import { createSpinner, Spinner } from 'nanospinner';
|
4
|
+
import JsonToTS from 'json-to-ts';
|
5
|
+
import prettier from 'prettier';
|
6
|
+
|
7
|
+
import { Logger } from './logger.js';
|
8
|
+
|
9
|
+
// @ts-expect-error
|
10
|
+
import translate from 'translate';
|
11
|
+
import { TranslationObject } from '../interfaces/input-params.interface';
|
12
|
+
import { store } from './store';
|
13
|
+
|
14
|
+
async function makeTranslatedCopy(
|
15
|
+
source: TranslationObject,
|
16
|
+
target: TranslationObject,
|
17
|
+
targetLang: string,
|
18
|
+
) {
|
19
|
+
const { from, skipExisting } = store.getInputParams();
|
20
|
+
|
21
|
+
for (let [key, value] of Object.entries(source)) {
|
22
|
+
if (typeof value === 'object') {
|
23
|
+
target[key] = target[key] || {};
|
24
|
+
|
25
|
+
await makeTranslatedCopy(value, target[key], targetLang);
|
26
|
+
} else {
|
27
|
+
try {
|
28
|
+
if (!(target[key] && skipExisting)) {
|
29
|
+
target[key] = await translate(value, {
|
30
|
+
from,
|
31
|
+
to: targetLang,
|
32
|
+
});
|
33
|
+
}
|
34
|
+
} catch (err: any) {
|
35
|
+
console.log('\n');
|
36
|
+
Logger.error(err.message);
|
37
|
+
process.exit(1);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
const getTranslationObject = (lang: string): Promise<TranslationObject> => {
|
44
|
+
const { dir } = store.getInputParams();
|
45
|
+
|
46
|
+
return new Promise(async (resolve, reject) => {
|
47
|
+
let translatedObj: TranslationObject = {};
|
48
|
+
const inputLangObj = await getInputLangObject();
|
49
|
+
|
50
|
+
const outputFile = path.join(process.cwd(), dir, `${lang}.json`);
|
51
|
+
|
52
|
+
if (existsSync(outputFile)) {
|
53
|
+
translatedObj = await parseJsonFile(outputFile);
|
54
|
+
}
|
55
|
+
|
56
|
+
await makeTranslatedCopy(inputLangObj, translatedObj, lang);
|
57
|
+
|
58
|
+
resolve(translatedObj);
|
59
|
+
});
|
60
|
+
};
|
61
|
+
|
62
|
+
export async function createDeclarationFile() {
|
63
|
+
const { dir, genType } = store.getInputParams();
|
64
|
+
const spinner = createSpinner('Creating language type file').start();
|
65
|
+
|
66
|
+
const langObject = parseJsonFile(
|
67
|
+
path.join(process.cwd(), dir, `${genType}.json`),
|
68
|
+
);
|
69
|
+
|
70
|
+
const interfaces = JsonToTS(langObject, {
|
71
|
+
rootName: 'GlobalTranslationType',
|
72
|
+
});
|
73
|
+
const typesDir = path.join(process.cwd(), dir, 'types');
|
74
|
+
|
75
|
+
if (!existsSync(typesDir)) {
|
76
|
+
await fs.mkdir(typesDir);
|
77
|
+
}
|
78
|
+
|
79
|
+
const declarationFile = path.join(typesDir, 'index');
|
80
|
+
|
81
|
+
const result = `
|
82
|
+
type NestedKeyOf<ObjectType extends object> = {
|
83
|
+
[Key in keyof ObjectType & string]: ObjectType[Key] extends object
|
84
|
+
? // @ts-ignore
|
85
|
+
\`$\{Key}.$\{NestedKeyOf<ObjectType[Key]>}\`
|
86
|
+
: \`$\{Key}\`
|
87
|
+
}[keyof ObjectType & string]
|
88
|
+
|
89
|
+
export type GlobalTranslation = NestedKeyOf<GlobalTranslationType>;
|
90
|
+
|
91
|
+
${interfaces.join('\n\n')}
|
92
|
+
`;
|
93
|
+
|
94
|
+
const formattedContent = await prettier.format(result, {
|
95
|
+
parser: 'typescript',
|
96
|
+
});
|
97
|
+
|
98
|
+
await fs.writeFile(declarationFile, formattedContent);
|
99
|
+
|
100
|
+
spinner.success({ text: 'Language type file created' });
|
101
|
+
}
|
102
|
+
|
103
|
+
export async function translateFile() {
|
104
|
+
const { to, dir } = store.getInputParams();
|
105
|
+
|
106
|
+
let spinner: Spinner;
|
107
|
+
let langFile: string;
|
108
|
+
let translationObject: TranslationObject;
|
109
|
+
|
110
|
+
for (let lang of to) {
|
111
|
+
langFile = path.join(process.cwd(), dir, `${lang}.json`);
|
112
|
+
spinner = createSpinner(`Translating to ${lang}...`).start();
|
113
|
+
|
114
|
+
translationObject = await getTranslationObject(lang);
|
115
|
+
|
116
|
+
await fs.writeFile(langFile, JSON.stringify(translationObject, null, 2));
|
117
|
+
|
118
|
+
spinner.success({ text: `Complete` });
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
export async function parseJsonFile<T = Record<string, any>>(filePath: string) {
|
123
|
+
return JSON.parse(await fs.readFile(filePath, { encoding: 'utf-8' })) as T;
|
124
|
+
}
|
125
|
+
|
126
|
+
function getInputLangObject() {
|
127
|
+
const { dir, from } = store.getInputParams();
|
128
|
+
|
129
|
+
const inputFile = path.join(process.cwd(), dir, `${from}.json`);
|
130
|
+
|
131
|
+
return parseJsonFile(inputFile);
|
132
|
+
}
|
133
|
+
|
134
|
+
function getMissingKeys(source: TranslationObject, target: TranslationObject) {
|
135
|
+
const missingKeys: string[] = [];
|
136
|
+
|
137
|
+
function loop(
|
138
|
+
source: TranslationObject,
|
139
|
+
target: TranslationObject,
|
140
|
+
path = '',
|
141
|
+
) {
|
142
|
+
for (let [key, value] of Object.entries(source)) {
|
143
|
+
const currentPath = path ? `${path}.${key}` : key;
|
144
|
+
|
145
|
+
if (typeof value === 'object') {
|
146
|
+
if (target[key]) {
|
147
|
+
loop(value, target[key], currentPath);
|
148
|
+
} else {
|
149
|
+
missingKeys.push(currentPath);
|
150
|
+
}
|
151
|
+
} else {
|
152
|
+
if (!target[key]) {
|
153
|
+
missingKeys.push(currentPath);
|
154
|
+
}
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
loop(source, target);
|
160
|
+
|
161
|
+
return missingKeys;
|
162
|
+
}
|
163
|
+
|
164
|
+
export async function showLangDiff() {
|
165
|
+
const spinner = createSpinner('Comparing language files').start();
|
166
|
+
|
167
|
+
const { dir, diff } = store.getInputParams();
|
168
|
+
|
169
|
+
const lang1 = diff[0];
|
170
|
+
const lang2 = diff[1];
|
171
|
+
|
172
|
+
const lang1Object = await parseJsonFile(
|
173
|
+
path.join(process.cwd(), dir, `${lang1}.json`),
|
174
|
+
);
|
175
|
+
const lang2Object = await parseJsonFile(
|
176
|
+
path.join(process.cwd(), dir, `${lang2}.json`),
|
177
|
+
);
|
178
|
+
|
179
|
+
const missingKeys = getMissingKeys(lang1Object, lang2Object);
|
180
|
+
|
181
|
+
Logger.log(`\nMissing keys in ${lang2}.json compared to ${lang1}.json\n`);
|
182
|
+
Logger.log(missingKeys.join('\n') || 'No missing keys');
|
183
|
+
|
184
|
+
spinner.success({ text: 'Comparison complete' });
|
185
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { IInputParams } from '../interfaces/input-params.interface';
|
2
|
+
|
3
|
+
class Store {
|
4
|
+
private inputParams: IInputParams = {
|
5
|
+
from: '',
|
6
|
+
to: [],
|
7
|
+
dir: '',
|
8
|
+
skipExisting: false,
|
9
|
+
genType: '',
|
10
|
+
diff: ['', ''],
|
11
|
+
};
|
12
|
+
|
13
|
+
public setInputParams(params: IInputParams) {
|
14
|
+
this.inputParams = params;
|
15
|
+
}
|
16
|
+
|
17
|
+
getInputParams() {
|
18
|
+
return this.inputParams;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export const store = new Store();
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { existsSync } from 'fs';
|
2
|
+
import path from 'path';
|
3
|
+
|
4
|
+
import { IInputParams } from '../interfaces/input-params.interface';
|
5
|
+
import { Logger } from './logger';
|
6
|
+
|
7
|
+
export function validateUserInput(params: IInputParams) {
|
8
|
+
if (!Object.keys(params).length) {
|
9
|
+
Logger.error(`Invalid arguments. Use "--help" for usage`);
|
10
|
+
|
11
|
+
process.exit(1);
|
12
|
+
}
|
13
|
+
|
14
|
+
const { to, from, dir, genType, diff } = params;
|
15
|
+
|
16
|
+
if ((from && !to) || (to && !from)) {
|
17
|
+
Logger.error(`"--from" and "--to" are dependent options`);
|
18
|
+
process.exit(1);
|
19
|
+
}
|
20
|
+
|
21
|
+
const inputFilePath = path.join(process.cwd(), dir, `${from}.json`);
|
22
|
+
const genTypeFilePath = path.join(process.cwd(), dir, `${genType}.json`);
|
23
|
+
|
24
|
+
if (!existsSync(inputFilePath) && from) {
|
25
|
+
Logger.error(`File "${inputFilePath}" not found`);
|
26
|
+
process.exit(1);
|
27
|
+
}
|
28
|
+
|
29
|
+
if (!existsSync(genTypeFilePath) && genType) {
|
30
|
+
Logger.error(`File "${genTypeFilePath}" not found`);
|
31
|
+
process.exit(1);
|
32
|
+
}
|
33
|
+
|
34
|
+
if (diff) {
|
35
|
+
if (diff.length !== 2) {
|
36
|
+
Logger.error(`"--diff" option requires two languages`);
|
37
|
+
process.exit(1);
|
38
|
+
}
|
39
|
+
|
40
|
+
const [lang1, lang2] = diff;
|
41
|
+
|
42
|
+
const lang1File = path.join(process.cwd(), dir, `${lang1}.json`);
|
43
|
+
const lang2File = path.join(process.cwd(), dir, `${lang2}.json`);
|
44
|
+
|
45
|
+
if (!existsSync(lang1File)) {
|
46
|
+
Logger.error(`File "${lang1File}" not found`);
|
47
|
+
process.exit(1);
|
48
|
+
}
|
49
|
+
|
50
|
+
if (!existsSync(lang2File)) {
|
51
|
+
Logger.error(`File "${lang2File}" not found`);
|
52
|
+
process.exit(1);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ESNext",
|
4
|
+
"module": "ESNext",
|
5
|
+
"outDir": "./dist",
|
6
|
+
"rootDir": "./src",
|
7
|
+
"strict": true,
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
10
|
+
"skipLibCheck": true,
|
11
|
+
"allowJs": true,
|
12
|
+
"moduleResolution": "bundler",
|
13
|
+
"resolveJsonModule": true,
|
14
|
+
"noEmit": true
|
15
|
+
},
|
16
|
+
"exclude": ["node_modules"],
|
17
|
+
"include": ["src/**/*"]
|
18
|
+
}
|
package/vite.config.ts
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import { defineConfig } from 'vite';
|
2
|
+
import { builtinModules } from 'module';
|
3
|
+
|
4
|
+
import packageJson from './package.json' assert { type: 'json' };
|
5
|
+
import Checker from 'vite-plugin-checker';
|
6
|
+
|
7
|
+
const dependencies = Object.keys(packageJson.dependencies);
|
8
|
+
const devDependencies = Object.keys(packageJson.devDependencies);
|
9
|
+
|
10
|
+
export default defineConfig(({ mode }) => {
|
11
|
+
const isWatch = mode === 'watch';
|
12
|
+
|
13
|
+
return {
|
14
|
+
build: {
|
15
|
+
minify: true,
|
16
|
+
lib: {
|
17
|
+
entry: 'src/index.ts',
|
18
|
+
formats: ['es'],
|
19
|
+
fileName: () => 'index.js',
|
20
|
+
},
|
21
|
+
rollupOptions: {
|
22
|
+
external: [
|
23
|
+
...builtinModules,
|
24
|
+
...dependencies,
|
25
|
+
...devDependencies,
|
26
|
+
/^node:/,
|
27
|
+
],
|
28
|
+
},
|
29
|
+
target: 'node16',
|
30
|
+
outDir: 'dist',
|
31
|
+
watch: isWatch
|
32
|
+
? {
|
33
|
+
include: ['src/**/*'],
|
34
|
+
exclude: ['node_modules', 'dist'],
|
35
|
+
clearScreen: true,
|
36
|
+
}
|
37
|
+
: null,
|
38
|
+
},
|
39
|
+
plugins: [Checker({ typescript: true })],
|
40
|
+
};
|
41
|
+
});
|
package/src/index.js
DELETED
@@ -1,138 +0,0 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
|
3
|
-
import path from 'path';
|
4
|
-
import {existsSync, promises as fs} from 'fs';
|
5
|
-
import {Command} from 'commander';
|
6
|
-
import {createSpinner} from 'nanospinner';
|
7
|
-
import translate from 'translate';
|
8
|
-
import JsonToTS from 'json-to-ts';
|
9
|
-
import prettier from 'prettier';
|
10
|
-
|
11
|
-
import pJson from '../package.json' assert { type: "json" };
|
12
|
-
import {Logger} from './utils/Logger.mjs';
|
13
|
-
import {validateOptions} from './utils/validation.mjs';
|
14
|
-
|
15
|
-
const {version: appVersion} = pJson;
|
16
|
-
|
17
|
-
|
18
|
-
const program = new Command();
|
19
|
-
const nodeMajVer = parseInt(process.version.substring(1).split('.')[0]);
|
20
|
-
|
21
|
-
if (nodeMajVer < 14) {
|
22
|
-
Logger.error(`Node version >= 14.x.x is required`);
|
23
|
-
|
24
|
-
process.exit(1);
|
25
|
-
}
|
26
|
-
|
27
|
-
program
|
28
|
-
.name('auto-lang')
|
29
|
-
.description('Generate translation files for multiple languages (i18n)')
|
30
|
-
.version(appVersion)
|
31
|
-
.option('-f, --from <lang>', 'language to translate from')
|
32
|
-
.option(
|
33
|
-
'-t, --to <lang...>',
|
34
|
-
'languages to translate to (seperated by space)'
|
35
|
-
)
|
36
|
-
.option(
|
37
|
-
'-d, --dir <directory>',
|
38
|
-
'directory containing the language files',
|
39
|
-
'translations'
|
40
|
-
)
|
41
|
-
.option('-s, --skip-existing', 'skip existing keys during translation')
|
42
|
-
.option('-g, --gen-type <lang>', 'generate types from language file')
|
43
|
-
.parse();
|
44
|
-
|
45
|
-
const { from, to, genType, inputFile, genTypeFile, dir, skipExisting } = validateOptions(
|
46
|
-
program.opts()
|
47
|
-
);
|
48
|
-
|
49
|
-
const inputJson = JSON.parse(
|
50
|
-
await fs.readFile(from ? inputFile : genTypeFile, { encoding: 'utf-8' })
|
51
|
-
);
|
52
|
-
|
53
|
-
async function makeTranslatedCopy(obj1, obj2, options) {
|
54
|
-
for (let [key, value] of Object.entries(obj1)) {
|
55
|
-
if (typeof value === 'object') {
|
56
|
-
obj2[key] = obj2[key] || {};
|
57
|
-
await makeTranslatedCopy(value, obj2[key], options);
|
58
|
-
} else {
|
59
|
-
try {
|
60
|
-
if(!(obj2[key] && skipExisting)) obj2[key] = await translate(value, { from, to: options.to });
|
61
|
-
} catch (err) {
|
62
|
-
console.log('\n');
|
63
|
-
Logger.error(err.message);
|
64
|
-
process.exit(1);
|
65
|
-
}
|
66
|
-
}
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
const getTranslation = (language) =>
|
71
|
-
new Promise(async (resolve, reject) => {
|
72
|
-
let translatedObj = {};
|
73
|
-
const outputFile = path.join(process.cwd(), dir, `${language}.json`);
|
74
|
-
|
75
|
-
if(existsSync(outputFile)) {
|
76
|
-
translatedObj = JSON.parse(
|
77
|
-
await fs.readFile(outputFile, { encoding: 'utf-8' })
|
78
|
-
);
|
79
|
-
}
|
80
|
-
|
81
|
-
await makeTranslatedCopy(inputJson, translatedObj, { to: language });
|
82
|
-
|
83
|
-
resolve(JSON.stringify(translatedObj, null, 4));
|
84
|
-
});
|
85
|
-
|
86
|
-
async function createDeclarationFile() {
|
87
|
-
const spinner = createSpinner('Creating language type file').start();
|
88
|
-
|
89
|
-
const interfaces = JsonToTS(inputJson, { rootName: 'GlobalTranslationType' });
|
90
|
-
const typesDir = path.join(process.cwd(), dir, 'types');
|
91
|
-
|
92
|
-
if (!existsSync(typesDir)) {
|
93
|
-
fs.mkdir(typesDir);
|
94
|
-
}
|
95
|
-
|
96
|
-
const declarationFile = path.join(typesDir, 'index.ts');
|
97
|
-
|
98
|
-
const result = `
|
99
|
-
type NestedKeyOf<ObjectType extends object> = {
|
100
|
-
[Key in keyof ObjectType & string]: ObjectType[Key] extends object
|
101
|
-
? // @ts-ignore
|
102
|
-
\`$\{Key}.$\{NestedKeyOf<ObjectType[Key]>}\`
|
103
|
-
: \`$\{Key}\`
|
104
|
-
}[keyof ObjectType & string]
|
105
|
-
|
106
|
-
export type GlobalTranslation = NestedKeyOf<GlobalTranslationType>;
|
107
|
-
|
108
|
-
${interfaces.join('\n\n')}
|
109
|
-
`;
|
110
|
-
|
111
|
-
const formattedResult = prettier.format(result, { parser: 'typescript' });
|
112
|
-
|
113
|
-
fs.writeFile(declarationFile, formattedResult);
|
114
|
-
spinner.success({ text: 'Language type file created' });
|
115
|
-
}
|
116
|
-
|
117
|
-
async function translateFile() {
|
118
|
-
let spinner, langFile, tranlatedJson;
|
119
|
-
|
120
|
-
for (let lang of to) {
|
121
|
-
langFile = path.join(process.cwd(), dir, `${lang}.json`);
|
122
|
-
spinner = createSpinner(`Translating to ${lang}...`).start();
|
123
|
-
|
124
|
-
tranlatedJson = await getTranslation(lang);
|
125
|
-
// await sleep(1000);
|
126
|
-
await fs.writeFile(langFile, tranlatedJson);
|
127
|
-
|
128
|
-
spinner.success({ text: `Complete` });
|
129
|
-
}
|
130
|
-
}
|
131
|
-
|
132
|
-
if (from && to) {
|
133
|
-
await translateFile();
|
134
|
-
}
|
135
|
-
|
136
|
-
if (genType) {
|
137
|
-
await createDeclarationFile();
|
138
|
-
}
|
package/src/utils/Logger.mjs
DELETED
package/src/utils/validation.mjs
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
import chalk from 'chalk';
|
2
|
-
import { existsSync } from 'fs';
|
3
|
-
import path from 'path';
|
4
|
-
|
5
|
-
import { Logger } from './Logger.mjs';
|
6
|
-
|
7
|
-
export function validateOptions(opts) {
|
8
|
-
if (!Object.keys(opts).length) {
|
9
|
-
Logger.error(`Invalid arguments. Use ${chalk.gray('--help')} for usage`);
|
10
|
-
|
11
|
-
process.exit(1);
|
12
|
-
}
|
13
|
-
|
14
|
-
const { to, from, dir, genType } = opts;
|
15
|
-
|
16
|
-
if ((from && !to) || (to && !from)) {
|
17
|
-
Logger.error(
|
18
|
-
`${chalk.gray('--from')} and ${chalk.gray('--to')} are dependent options`
|
19
|
-
);
|
20
|
-
process.exit(1);
|
21
|
-
}
|
22
|
-
|
23
|
-
const inputFile = path.join(process.cwd(), dir, `${from}.json`);
|
24
|
-
const genTypeFile = path.join(process.cwd(), dir, `${genType}.json`);
|
25
|
-
|
26
|
-
if (!existsSync(inputFile) && from) {
|
27
|
-
Logger.error(`File "${inputFile}" not found`);
|
28
|
-
process.exit(1);
|
29
|
-
}
|
30
|
-
|
31
|
-
if (!existsSync(genTypeFile) && genType) {
|
32
|
-
Logger.error(`File "${genTypeFile}" not found`);
|
33
|
-
process.exit(1);
|
34
|
-
}
|
35
|
-
|
36
|
-
return { ...opts, inputFile, genTypeFile };
|
37
|
-
}
|