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 ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "bracketSpacing": true,
3
+ "semi": true,
4
+ "singleQuote": true,
5
+ }
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
- -V, --version output the version number
40
- -f, --from <lang> language to translate from
41
- -t, --to <lang...> languages to translate to (seperated by space)
42
- -d, --dir <directory> directory containing the language files (default: "translations")
43
- -s, --skip-existing skip existing keys during translation
44
- -g, --gen-type <lang> generate types from language file
45
- -h, --help display help for command
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
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Webpack App</title>
6
+ </head>
7
+ <body>
8
+ <h1>Hello world!</h1>
9
+ <h2>Tip: Check your console</h2>
10
+ </body>
11
+
12
+ </html>
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "auto-lang",
3
- "version": "1.1.1",
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": ["Internationalization", "node", "i18n", "translate"],
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": "./src/index.js"
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,10 @@
1
+ export interface IInputParams {
2
+ from: string;
3
+ to: string[];
4
+ dir: string;
5
+ genType: string;
6
+ skipExisting: boolean;
7
+ diff: [string, string];
8
+ }
9
+
10
+ export type TranslationObject = Record<string, any>;
@@ -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,11 @@
1
+ import chalk from 'chalk';
2
+
3
+ export class Logger {
4
+ static error(message: string) {
5
+ console.log(`${chalk.red(message)}`);
6
+ }
7
+
8
+ static log(message: string) {
9
+ console.log(message);
10
+ }
11
+ }
@@ -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
- }
@@ -1,7 +0,0 @@
1
- import chalk from 'chalk';
2
-
3
- export class Logger {
4
- static error(message) {
5
- console.log(`${chalk.bgRed(' ERROR ')} ${message}`);
6
- }
7
- }
@@ -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
- }