auto-lang 1.1.1 → 2.0.0

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,88 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var __generator = (this && this.__generator) || function (thisArg, body) {
11
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
12
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
13
+ function verb(n) { return function (v) { return step([n, v]); }; }
14
+ function step(op) {
15
+ if (f) throw new TypeError("Generator is already executing.");
16
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
17
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
18
+ if (y = 0, t) op = [op[0] & 2, t.value];
19
+ switch (op[0]) {
20
+ case 0: case 1: t = op; break;
21
+ case 4: _.label++; return { value: op[1], done: false };
22
+ case 5: _.label++; y = op[1]; op = [0]; continue;
23
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
24
+ default:
25
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
26
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
27
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
28
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
29
+ if (t[2]) _.ops.pop();
30
+ _.trys.pop(); continue;
31
+ }
32
+ op = body.call(thisArg, _);
33
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
34
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
+ }
36
+ };
37
+ import { Command } from 'commander';
38
+ import path from 'path';
39
+ import { createDeclarationFile, parseJsonFile, showLangDiff, translateFile, } from './utils/index.js';
40
+ import { validateUserInput } from './utils/validation.js';
41
+ import { store } from './utils/store.js';
42
+ function main() {
43
+ return __awaiter(this, void 0, void 0, function () {
44
+ var pjson, appVersion, program, _a, from, to, genType, diff;
45
+ return __generator(this, function (_b) {
46
+ switch (_b.label) {
47
+ case 0: return [4 /*yield*/, parseJsonFile(path.join(process.cwd(), 'package.json'))];
48
+ case 1:
49
+ pjson = _b.sent();
50
+ appVersion = pjson.version;
51
+ program = new Command();
52
+ program
53
+ .name('auto-lang')
54
+ .description('Generate translation files for multiple languages (i18n)')
55
+ .version(appVersion)
56
+ .option('-f, --from <lang>', 'language to translate from')
57
+ .option('-t, --to <lang...>', 'languages to translate to (seperated by space)')
58
+ .option('-d, --dir <directory>', 'directory containing the language files', 'translations')
59
+ .option('-s, --skip-existing', 'skip existing keys during translation')
60
+ .option('-g, --gen-type <lang>', 'generate types from language file')
61
+ .option('-d, --diff <lang...>', 'show missing keys between two language files')
62
+ .parse();
63
+ validateUserInput(program.opts());
64
+ store.setInputParams(program.opts());
65
+ _a = store.getInputParams(), from = _a.from, to = _a.to, genType = _a.genType, diff = _a.diff;
66
+ if (!(from && to)) return [3 /*break*/, 3];
67
+ return [4 /*yield*/, translateFile()];
68
+ case 2:
69
+ _b.sent();
70
+ _b.label = 3;
71
+ case 3:
72
+ if (!genType) return [3 /*break*/, 5];
73
+ return [4 /*yield*/, createDeclarationFile()];
74
+ case 4:
75
+ _b.sent();
76
+ _b.label = 5;
77
+ case 5:
78
+ if (!diff) return [3 /*break*/, 7];
79
+ return [4 /*yield*/, showLangDiff()];
80
+ case 6:
81
+ _b.sent();
82
+ _b.label = 7;
83
+ case 7: return [2 /*return*/];
84
+ }
85
+ });
86
+ });
87
+ }
88
+ main();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,249 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var __generator = (this && this.__generator) || function (thisArg, body) {
11
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
12
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
13
+ function verb(n) { return function (v) { return step([n, v]); }; }
14
+ function step(op) {
15
+ if (f) throw new TypeError("Generator is already executing.");
16
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
17
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
18
+ if (y = 0, t) op = [op[0] & 2, t.value];
19
+ switch (op[0]) {
20
+ case 0: case 1: t = op; break;
21
+ case 4: _.label++; return { value: op[1], done: false };
22
+ case 5: _.label++; y = op[1]; op = [0]; continue;
23
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
24
+ default:
25
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
26
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
27
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
28
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
29
+ if (t[2]) _.ops.pop();
30
+ _.trys.pop(); continue;
31
+ }
32
+ op = body.call(thisArg, _);
33
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
34
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
+ }
36
+ };
37
+ import path from 'path';
38
+ import { existsSync, promises as fs } from 'fs';
39
+ import { createSpinner } from 'nanospinner';
40
+ import JsonToTS from 'json-to-ts';
41
+ import prettier from 'prettier';
42
+ import { Logger } from './logger.js';
43
+ // @ts-expect-error
44
+ import translate from 'translate';
45
+ import { store } from './store.js';
46
+ function makeTranslatedCopy(source, target, targetLang) {
47
+ return __awaiter(this, void 0, void 0, function () {
48
+ var _a, from, skipExisting, _i, _b, _c, key, value, _d, _e, err_1;
49
+ return __generator(this, function (_f) {
50
+ switch (_f.label) {
51
+ case 0:
52
+ _a = store.getInputParams(), from = _a.from, skipExisting = _a.skipExisting;
53
+ _i = 0, _b = Object.entries(source);
54
+ _f.label = 1;
55
+ case 1:
56
+ if (!(_i < _b.length)) return [3 /*break*/, 8];
57
+ _c = _b[_i], key = _c[0], value = _c[1];
58
+ if (!(typeof value === 'object')) return [3 /*break*/, 3];
59
+ target[key] = target[key] || {};
60
+ return [4 /*yield*/, makeTranslatedCopy(value, target[key], targetLang)];
61
+ case 2:
62
+ _f.sent();
63
+ return [3 /*break*/, 7];
64
+ case 3:
65
+ _f.trys.push([3, 6, , 7]);
66
+ if (!!(target[key] && skipExisting)) return [3 /*break*/, 5];
67
+ _d = target;
68
+ _e = key;
69
+ return [4 /*yield*/, translate(value, {
70
+ from: from,
71
+ to: targetLang,
72
+ })];
73
+ case 4:
74
+ _d[_e] = _f.sent();
75
+ _f.label = 5;
76
+ case 5: return [3 /*break*/, 7];
77
+ case 6:
78
+ err_1 = _f.sent();
79
+ console.log('\n');
80
+ Logger.error(err_1.message);
81
+ process.exit(1);
82
+ return [3 /*break*/, 7];
83
+ case 7:
84
+ _i++;
85
+ return [3 /*break*/, 1];
86
+ case 8: return [2 /*return*/];
87
+ }
88
+ });
89
+ });
90
+ }
91
+ var getTranslationObject = function (lang) {
92
+ var dir = store.getInputParams().dir;
93
+ return new Promise(function (resolve, reject) { return __awaiter(void 0, void 0, void 0, function () {
94
+ var translatedObj, inputLangObj, outputFile;
95
+ return __generator(this, function (_a) {
96
+ switch (_a.label) {
97
+ case 0:
98
+ translatedObj = {};
99
+ return [4 /*yield*/, getInputLangObject()];
100
+ case 1:
101
+ inputLangObj = _a.sent();
102
+ outputFile = path.join(process.cwd(), dir, "".concat(lang, ".json"));
103
+ if (!existsSync(outputFile)) return [3 /*break*/, 3];
104
+ return [4 /*yield*/, parseJsonFile(outputFile)];
105
+ case 2:
106
+ translatedObj = _a.sent();
107
+ _a.label = 3;
108
+ case 3: return [4 /*yield*/, makeTranslatedCopy(inputLangObj, translatedObj, lang)];
109
+ case 4:
110
+ _a.sent();
111
+ resolve(translatedObj);
112
+ return [2 /*return*/];
113
+ }
114
+ });
115
+ }); });
116
+ };
117
+ export function createDeclarationFile() {
118
+ return __awaiter(this, void 0, void 0, function () {
119
+ var _a, dir, genType, spinner, langObject, interfaces, typesDir, declarationFile, result, formattedContent;
120
+ return __generator(this, function (_b) {
121
+ switch (_b.label) {
122
+ case 0:
123
+ _a = store.getInputParams(), dir = _a.dir, genType = _a.genType;
124
+ spinner = createSpinner('Creating language type file').start();
125
+ langObject = parseJsonFile(path.join(process.cwd(), dir, "".concat(genType, ".json")));
126
+ interfaces = JsonToTS(langObject, {
127
+ rootName: 'GlobalTranslationType',
128
+ });
129
+ typesDir = path.join(process.cwd(), dir, 'types');
130
+ if (!!existsSync(typesDir)) return [3 /*break*/, 2];
131
+ return [4 /*yield*/, fs.mkdir(typesDir)];
132
+ case 1:
133
+ _b.sent();
134
+ _b.label = 2;
135
+ case 2:
136
+ declarationFile = path.join(typesDir, 'index');
137
+ result = "\n type NestedKeyOf<ObjectType extends object> = {\n [Key in keyof ObjectType & string]: ObjectType[Key] extends object\n ? // @ts-ignore\n `${Key}.${NestedKeyOf<ObjectType[Key]>}`\n : `${Key}`\n }[keyof ObjectType & string]\n\n export type GlobalTranslation = NestedKeyOf<GlobalTranslationType>;\n\n ".concat(interfaces.join('\n\n'), "\n ");
138
+ return [4 /*yield*/, prettier.format(result, {
139
+ parser: 'typescript',
140
+ })];
141
+ case 3:
142
+ formattedContent = _b.sent();
143
+ return [4 /*yield*/, fs.writeFile(declarationFile, formattedContent)];
144
+ case 4:
145
+ _b.sent();
146
+ spinner.success({ text: 'Language type file created' });
147
+ return [2 /*return*/];
148
+ }
149
+ });
150
+ });
151
+ }
152
+ export function translateFile() {
153
+ return __awaiter(this, void 0, void 0, function () {
154
+ var _a, to, dir, spinner, langFile, translationObject, _i, to_1, lang;
155
+ return __generator(this, function (_b) {
156
+ switch (_b.label) {
157
+ case 0:
158
+ _a = store.getInputParams(), to = _a.to, dir = _a.dir;
159
+ _i = 0, to_1 = to;
160
+ _b.label = 1;
161
+ case 1:
162
+ if (!(_i < to_1.length)) return [3 /*break*/, 5];
163
+ lang = to_1[_i];
164
+ langFile = path.join(process.cwd(), dir, "".concat(lang, ".json"));
165
+ spinner = createSpinner("Translating to ".concat(lang, "...")).start();
166
+ return [4 /*yield*/, getTranslationObject(lang)];
167
+ case 2:
168
+ translationObject = _b.sent();
169
+ return [4 /*yield*/, fs.writeFile(langFile, JSON.stringify(translationObject, null, 2))];
170
+ case 3:
171
+ _b.sent();
172
+ spinner.success({ text: "Complete" });
173
+ _b.label = 4;
174
+ case 4:
175
+ _i++;
176
+ return [3 /*break*/, 1];
177
+ case 5: return [2 /*return*/];
178
+ }
179
+ });
180
+ });
181
+ }
182
+ export function parseJsonFile(filePath) {
183
+ return __awaiter(this, void 0, void 0, function () {
184
+ var _a, _b;
185
+ return __generator(this, function (_c) {
186
+ switch (_c.label) {
187
+ case 0:
188
+ _b = (_a = JSON).parse;
189
+ return [4 /*yield*/, fs.readFile(filePath, { encoding: 'utf-8' })];
190
+ case 1: return [2 /*return*/, _b.apply(_a, [_c.sent()])];
191
+ }
192
+ });
193
+ });
194
+ }
195
+ function getInputLangObject() {
196
+ var _a = store.getInputParams(), dir = _a.dir, from = _a.from;
197
+ var inputFile = path.join(process.cwd(), dir, "".concat(from, ".json"));
198
+ return parseJsonFile(inputFile);
199
+ }
200
+ function getMissingKeys(source, target) {
201
+ var missingKeys = [];
202
+ function loop(source, target, path) {
203
+ if (path === void 0) { path = ''; }
204
+ for (var _i = 0, _a = Object.entries(source); _i < _a.length; _i++) {
205
+ var _b = _a[_i], key = _b[0], value = _b[1];
206
+ var currentPath = path ? "".concat(path, ".").concat(key) : key;
207
+ if (typeof value === 'object') {
208
+ if (target[key]) {
209
+ loop(value, target[key], currentPath);
210
+ }
211
+ else {
212
+ missingKeys.push(currentPath);
213
+ }
214
+ }
215
+ else {
216
+ if (!target[key]) {
217
+ missingKeys.push(currentPath);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ loop(source, target);
223
+ return missingKeys;
224
+ }
225
+ export function showLangDiff() {
226
+ return __awaiter(this, void 0, void 0, function () {
227
+ var spinner, _a, dir, diff, lang1, lang2, lang1Object, lang2Object, missingKeys;
228
+ return __generator(this, function (_b) {
229
+ switch (_b.label) {
230
+ case 0:
231
+ spinner = createSpinner('Comparing language files').start();
232
+ _a = store.getInputParams(), dir = _a.dir, diff = _a.diff;
233
+ lang1 = diff[0];
234
+ lang2 = diff[1];
235
+ return [4 /*yield*/, parseJsonFile(path.join(process.cwd(), dir, "".concat(lang1, ".json")))];
236
+ case 1:
237
+ lang1Object = _b.sent();
238
+ return [4 /*yield*/, parseJsonFile(path.join(process.cwd(), dir, "".concat(lang2, ".json")))];
239
+ case 2:
240
+ lang2Object = _b.sent();
241
+ missingKeys = getMissingKeys(lang1Object, lang2Object);
242
+ Logger.log("\nMissing keys in ".concat(lang2, ".json compared to ").concat(lang1, ".json\n"));
243
+ Logger.log(missingKeys.join('\n') || 'No missing keys');
244
+ spinner.success({ text: 'Comparison complete' });
245
+ return [2 /*return*/];
246
+ }
247
+ });
248
+ });
249
+ }
@@ -0,0 +1,13 @@
1
+ import chalk from 'chalk';
2
+ var Logger = /** @class */ (function () {
3
+ function Logger() {
4
+ }
5
+ Logger.error = function (message) {
6
+ console.log("".concat(chalk.red(message)));
7
+ };
8
+ Logger.log = function (message) {
9
+ console.log(message);
10
+ };
11
+ return Logger;
12
+ }());
13
+ export { Logger };
@@ -0,0 +1,20 @@
1
+ var Store = /** @class */ (function () {
2
+ function Store() {
3
+ this.inputParams = {
4
+ from: '',
5
+ to: [],
6
+ dir: '',
7
+ skipExisting: false,
8
+ genType: '',
9
+ diff: ['', ''],
10
+ };
11
+ }
12
+ Store.prototype.setInputParams = function (params) {
13
+ this.inputParams = params;
14
+ };
15
+ Store.prototype.getInputParams = function () {
16
+ return this.inputParams;
17
+ };
18
+ return Store;
19
+ }());
20
+ export var store = new Store();
@@ -0,0 +1,41 @@
1
+ import { existsSync } from 'fs';
2
+ import path from 'path';
3
+ import { Logger } from './logger.js';
4
+ export function validateUserInput(params) {
5
+ if (!Object.keys(params).length) {
6
+ Logger.error("Invalid arguments. Use \"--help\" for usage");
7
+ process.exit(1);
8
+ }
9
+ var to = params.to, from = params.from, dir = params.dir, genType = params.genType, diff = params.diff;
10
+ if ((from && !to) || (to && !from)) {
11
+ Logger.error("\"--from\" and \"--to\" are dependent options");
12
+ process.exit(1);
13
+ }
14
+ var inputFilePath = path.join(process.cwd(), dir, "".concat(from, ".json"));
15
+ var genTypeFilePath = path.join(process.cwd(), dir, "".concat(genType, ".json"));
16
+ if (!existsSync(inputFilePath) && from) {
17
+ Logger.error("File \"".concat(inputFilePath, "\" not found"));
18
+ process.exit(1);
19
+ }
20
+ if (!existsSync(genTypeFilePath) && genType) {
21
+ Logger.error("File \"".concat(genTypeFilePath, "\" not found"));
22
+ process.exit(1);
23
+ }
24
+ if (diff) {
25
+ if (diff.length !== 2) {
26
+ Logger.error("\"--diff\" option requires two languages");
27
+ process.exit(1);
28
+ }
29
+ var lang1 = diff[0], lang2 = diff[1];
30
+ var lang1File = path.join(process.cwd(), dir, "".concat(lang1, ".json"));
31
+ var lang2File = path.join(process.cwd(), dir, "".concat(lang2, ".json"));
32
+ if (!existsSync(lang1File)) {
33
+ Logger.error("File \"".concat(lang1File, "\" not found"));
34
+ process.exit(1);
35
+ }
36
+ if (!existsSync(lang2File)) {
37
+ Logger.error("File \"".concat(lang2File, "\" not found"));
38
+ process.exit(1);
39
+ }
40
+ }
41
+ }
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.0",
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": "tsc --w",
10
+ "build": "rm -rf dist && tsc"
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,23 @@
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
+ "webpack": "^5.97.1",
43
+ "webpack-cli": "^5.1.4"
28
44
  }
29
45
  }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { Command } from 'commander';
2
+ import path from 'path';
3
+
4
+ import {
5
+ createDeclarationFile,
6
+ parseJsonFile,
7
+ showLangDiff,
8
+ translateFile,
9
+ } from './utils/index.js';
10
+ import { validateUserInput } from './utils/validation.js';
11
+ import { store } from './utils/store.js';
12
+
13
+ async function main() {
14
+ const pjson = await parseJsonFile(path.join(process.cwd(), 'package.json'));
15
+ const appVersion = pjson.version;
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name('auto-lang')
21
+ .description('Generate translation files for multiple languages (i18n)')
22
+ .version(appVersion)
23
+ .option('-f, --from <lang>', 'language to translate from')
24
+ .option(
25
+ '-t, --to <lang...>',
26
+ 'languages to translate to (seperated by space)',
27
+ )
28
+ .option(
29
+ '-d, --dir <directory>',
30
+ 'directory containing the language files',
31
+ 'translations',
32
+ )
33
+ .option('-s, --skip-existing', 'skip existing keys during translation')
34
+ .option('-g, --gen-type <lang>', 'generate types from language file')
35
+ .option(
36
+ '-d, --diff <lang...>',
37
+ 'show missing keys between two language files',
38
+ )
39
+ .parse();
40
+
41
+ validateUserInput(program.opts());
42
+ store.setInputParams(program.opts());
43
+
44
+ const { from, to, genType, diff } = store.getInputParams();
45
+
46
+ if (from && to) {
47
+ await translateFile();
48
+ }
49
+
50
+ if (genType) {
51
+ await createDeclarationFile();
52
+ }
53
+
54
+ if (diff) {
55
+ await showLangDiff();
56
+ }
57
+ }
58
+
59
+ 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,186 @@
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.js';
12
+ import { store } from './store.js';
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
+ // @ts-expect-error
71
+ const interfaces = JsonToTS(langObject, {
72
+ rootName: 'GlobalTranslationType',
73
+ });
74
+ const typesDir = path.join(process.cwd(), dir, 'types');
75
+
76
+ if (!existsSync(typesDir)) {
77
+ await fs.mkdir(typesDir);
78
+ }
79
+
80
+ const declarationFile = path.join(typesDir, 'index');
81
+
82
+ const result = `
83
+ type NestedKeyOf<ObjectType extends object> = {
84
+ [Key in keyof ObjectType & string]: ObjectType[Key] extends object
85
+ ? // @ts-ignore
86
+ \`$\{Key}.$\{NestedKeyOf<ObjectType[Key]>}\`
87
+ : \`$\{Key}\`
88
+ }[keyof ObjectType & string]
89
+
90
+ export type GlobalTranslation = NestedKeyOf<GlobalTranslationType>;
91
+
92
+ ${interfaces.join('\n\n')}
93
+ `;
94
+
95
+ const formattedContent = await prettier.format(result, {
96
+ parser: 'typescript',
97
+ });
98
+
99
+ await fs.writeFile(declarationFile, formattedContent);
100
+
101
+ spinner.success({ text: 'Language type file created' });
102
+ }
103
+
104
+ export async function translateFile() {
105
+ const { to, dir } = store.getInputParams();
106
+
107
+ let spinner: Spinner;
108
+ let langFile: string;
109
+ let translationObject: TranslationObject;
110
+
111
+ for (let lang of to) {
112
+ langFile = path.join(process.cwd(), dir, `${lang}.json`);
113
+ spinner = createSpinner(`Translating to ${lang}...`).start();
114
+
115
+ translationObject = await getTranslationObject(lang);
116
+
117
+ await fs.writeFile(langFile, JSON.stringify(translationObject, null, 2));
118
+
119
+ spinner.success({ text: `Complete` });
120
+ }
121
+ }
122
+
123
+ export async function parseJsonFile<T = Record<string, any>>(filePath: string) {
124
+ return JSON.parse(await fs.readFile(filePath, { encoding: 'utf-8' })) as T;
125
+ }
126
+
127
+ function getInputLangObject() {
128
+ const { dir, from } = store.getInputParams();
129
+
130
+ const inputFile = path.join(process.cwd(), dir, `${from}.json`);
131
+
132
+ return parseJsonFile(inputFile);
133
+ }
134
+
135
+ function getMissingKeys(source: TranslationObject, target: TranslationObject) {
136
+ const missingKeys: string[] = [];
137
+
138
+ function loop(
139
+ source: TranslationObject,
140
+ target: TranslationObject,
141
+ path = '',
142
+ ) {
143
+ for (let [key, value] of Object.entries(source)) {
144
+ const currentPath = path ? `${path}.${key}` : key;
145
+
146
+ if (typeof value === 'object') {
147
+ if (target[key]) {
148
+ loop(value, target[key], currentPath);
149
+ } else {
150
+ missingKeys.push(currentPath);
151
+ }
152
+ } else {
153
+ if (!target[key]) {
154
+ missingKeys.push(currentPath);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ loop(source, target);
161
+
162
+ return missingKeys;
163
+ }
164
+
165
+ export async function showLangDiff() {
166
+ const spinner = createSpinner('Comparing language files').start();
167
+
168
+ const { dir, diff } = store.getInputParams();
169
+
170
+ const lang1 = diff[0];
171
+ const lang2 = diff[1];
172
+
173
+ const lang1Object = await parseJsonFile(
174
+ path.join(process.cwd(), dir, `${lang1}.json`),
175
+ );
176
+ const lang2Object = await parseJsonFile(
177
+ path.join(process.cwd(), dir, `${lang2}.json`),
178
+ );
179
+
180
+ const missingKeys = getMissingKeys(lang1Object, lang2Object);
181
+
182
+ Logger.log(`\nMissing keys in ${lang2}.json compared to ${lang1}.json\n`);
183
+ Logger.log(missingKeys.join('\n') || 'No missing keys');
184
+
185
+ spinner.success({ text: 'Comparison complete' });
186
+ }
@@ -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.js';
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.js';
5
+ import { Logger } from './logger.js';
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,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "module": "NodeNext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "skipLibCheck": true
11
+ // "moduleResolution": "bundler"
12
+
13
+ },
14
+ "exclude": ["node_modules"],
15
+ "include": ["src/**/*"]
16
+ }
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
- }