locale-tool 1.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.
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
+ <excludeFolder url="file://$MODULE_DIR$/temp" />
7
+ <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
+ </content>
9
+ <orderEntry type="inheritedJdk" />
10
+ <orderEntry type="sourceFolder" forTests="false" />
11
+ </component>
12
+ </module>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MaterialThemeProjectNewConfig">
4
+ <option name="metadata">
5
+ <MTProjectMetadataState>
6
+ <option name="migrated" value="true" />
7
+ <option name="pristineConfig" value="false" />
8
+ <option name="userId" value="6dbcab2b:1923e02f15b:-7ff1" />
9
+ </MTProjectMetadataState>
10
+ </option>
11
+ <option name="titleBarState">
12
+ <MTProjectTitleBarConfigState>
13
+ <option name="overrideColor" value="false" />
14
+ </MTProjectTitleBarConfigState>
15
+ </option>
16
+ </component>
17
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/locale-tool.iml" filepath="$PROJECT_DIR$/.idea/locale-tool.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
package/dist/index.js ADDED
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ const commander_1 = require("commander");
47
+ const fs_1 = require("fs");
48
+ const path_1 = require("path");
49
+ const url_1 = require("url");
50
+ const LOCALES_DIR = './src/lang/locale';
51
+ const TYPES_DIR = (0, path_1.join)(LOCALES_DIR, '_types');
52
+ const INTERFACES_FILE = (0, path_1.join)(TYPES_DIR, 'interfaces.ts');
53
+ function flattenLocale(nested) {
54
+ const result = {};
55
+ function traverse(obj, path = '') {
56
+ for (const [key, value] of Object.entries(obj)) {
57
+ const currentPath = path ? `${path}.${key}` : key;
58
+ if (Array.isArray(value)) {
59
+ result[currentPath] = value;
60
+ }
61
+ else if (typeof value === 'object' && value !== null) {
62
+ traverse(value, currentPath);
63
+ }
64
+ else if (typeof value === 'string') {
65
+ result[currentPath] = value;
66
+ }
67
+ }
68
+ }
69
+ traverse(nested);
70
+ return result;
71
+ }
72
+ function nestifyLocale(flat) {
73
+ var _a;
74
+ const result = {};
75
+ for (const [path, value] of Object.entries(flat)) {
76
+ const keys = path.split('.');
77
+ let current = result;
78
+ for (let i = 0; i < keys.length - 1; i++) {
79
+ const key = keys[i];
80
+ (_a = current[key]) !== null && _a !== void 0 ? _a : (current[key] = {});
81
+ current = current[key];
82
+ }
83
+ const lastKey = keys[keys.length - 1];
84
+ current[lastKey] = value;
85
+ }
86
+ return result;
87
+ }
88
+ function generateTypesFromNest(nest) {
89
+ const interfaceLines = [];
90
+ function traverse(obj, indent = 1, path = '') {
91
+ for (const [key, value] of Object.entries(obj)) {
92
+ const indentStr = ' '.repeat(indent);
93
+ const currentPath = path ? `${path}.${key}` : key;
94
+ if (Array.isArray(value)) {
95
+ interfaceLines.push(`${indentStr}${key}: string[];`);
96
+ }
97
+ else if (typeof value === 'object' && value !== null) {
98
+ interfaceLines.push(`${indentStr}${key}: {`);
99
+ traverse(value, indent + 1, currentPath);
100
+ interfaceLines.push(`${indentStr}};`);
101
+ }
102
+ else if (typeof value === 'string') {
103
+ interfaceLines.push(`${indentStr}${key}: string;`);
104
+ }
105
+ }
106
+ }
107
+ traverse(nest);
108
+ return `export interface LocaleSchema {\n${interfaceLines.join('\n')}\n}`;
109
+ }
110
+ function getTranslationGuide() {
111
+ return `# Translation Guide
112
+
113
+ ## Getting Started
114
+
115
+ ### 1. Find Your Language Code
116
+ - Visit [ISO 639-1 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to find your language code
117
+ - Use the two-letter code (e.g., \`de\` for German, \`fr\` for French, \`es\` for Spanish)
118
+ - For region-specific variants, use format like \`en-US\`, \`zh-CN\`, \`pt-BR\`
119
+
120
+ ### 2. Start Translating
121
+ 1. Open \`flat.json\` in this folder
122
+ 2. Translate the **values** (keep the keys unchanged)
123
+ 3. When done, run: \`npm run locale nest ${process.argv[3] || 'your-locale'}\`
124
+
125
+ ### 3. Submit Your Translation
126
+ - Create a pull request with your locale folder
127
+ - Include both \`flat.json\` and generated \`index.ts\`
128
+
129
+ ## Translation Rules
130
+
131
+ ### āœ… DO:
132
+ - **Preserve structure**: Keep array and object structures intact
133
+ - \`["item1", "item2"]\` → \`["ŃŠ»ŠµŠ¼ŠµŠ½Ń‚1", "ŃŠ»ŠµŠ¼ŠµŠ½Ń‚2"]\`
134
+ - **Keep variables untouched**: Variables in \`{{brackets}}\` must remain exactly as they are
135
+ - \`"Hello {{name}}"\` → \`"ŠŸŃ€ŠøŠ²ŠµŃ‚ {{name}}"\`
136
+ - **Maintain line breaks**: Keep \`\\n\` for multi-line messages
137
+ - **Respect key hierarchy**: Keys like \`"settings.pages.debug"\` show UI component structure
138
+
139
+ ### āŒ DON'T:
140
+ - Change or remove JSON keys
141
+ - Translate variables in \`{{brackets}}\`
142
+ - Reorder array elements
143
+ - Break JSON syntax (missing quotes, commas, brackets)
144
+
145
+ ## Examples
146
+
147
+ ### Simple String
148
+ \`\`\`json
149
+ "commands.togglePanels.notice.shown": "Control panels shown"
150
+ // āœ… Should become:
151
+ "commands.togglePanels.notice.shown": "Панели ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŃ показаны"
152
+ \`\`\`
153
+
154
+ ### String with Variables
155
+ \`\`\`json
156
+ "settings.pages.debug.clearLogsStorage.desc": "Storage: {{storage}}, Entries: {{entries}}"
157
+ // āœ… Should become:
158
+ "settings.pages.debug.clearLogsStorage.desc": "Єранилище: {{storage}}, Записи: {{entries}}"
159
+ \`\`\`
160
+
161
+ ### Array of Strings
162
+ \`\`\`json
163
+ "settings.pages.debug.reportIssue.desc": [
164
+ "If you encounter any issues, please report them.",
165
+ "How to report:",
166
+ "1. Enable debug logging"
167
+ ]
168
+ // āœ… Should become:
169
+ "settings.pages.debug.reportIssue.desc": [
170
+ "Если у вас возникли проблемы, сообщите о них.",
171
+ "Как ŃŠ¾Š¾Š±Ń‰ŠøŃ‚ŃŒ:",
172
+ "1. Š’ŠŗŠ»ŃŽŃ‡ŠøŃ‚Šµ отлаГочное логирование"
173
+ ]
174
+ \`\`\`
175
+
176
+ ## Best Practices
177
+
178
+ - **Natural translation**: Translate the meaning, not word-by-word
179
+ - **Keep UI context**: Consider where the text appears (buttons, tooltips, messages)
180
+ - **Test length**: Some UI elements have space constraints - shorten if needed
181
+ - **Maintain tone**: Keep the same level of formality as the original
182
+ - **Handle pluralization**: Adapt to your language's plural rules
183
+
184
+ ## Need Help?
185
+ - Check existing translations in other language folders for reference
186
+ - Ask questions in GitHub issues before starting large translations
187
+ - Test your JSON syntax using online JSON validators`;
188
+ }
189
+ function generateTypes() {
190
+ return __awaiter(this, void 0, void 0, function* () {
191
+ const enLocaleDir = (0, path_1.join)(LOCALES_DIR, 'en');
192
+ const jsonPath = (0, path_1.join)(enLocaleDir, 'flat.json');
193
+ const tsPath = (0, path_1.join)(enLocaleDir, 'index.ts');
194
+ if (!(0, fs_1.existsSync)(jsonPath)) {
195
+ console.error('āŒ Error: en/flat.json not found. Run "flat en" first.');
196
+ process.exit(1);
197
+ }
198
+ try {
199
+ const fullPath = (0, url_1.pathToFileURL)((0, path_1.join)(process.cwd(), tsPath)).href;
200
+ const module = yield Promise.resolve(`${fullPath + '?t=' + Date.now()}`).then(s => __importStar(require(s)));
201
+ const nested = module.default;
202
+ const types = generateTypesFromNest(nested);
203
+ if (!(0, fs_1.existsSync)(TYPES_DIR)) {
204
+ (0, fs_1.mkdirSync)(TYPES_DIR, { recursive: true });
205
+ }
206
+ (0, fs_1.writeFileSync)(INTERFACES_FILE, types);
207
+ console.log(`āœ… Generated ${INTERFACES_FILE}`);
208
+ }
209
+ catch (error) {
210
+ console.error('āŒ Error generating types:', error);
211
+ process.exit(1);
212
+ }
213
+ });
214
+ }
215
+ function createTemplate(locale) {
216
+ return __awaiter(this, void 0, void 0, function* () {
217
+ const enLocaleDir = (0, path_1.join)(LOCALES_DIR, 'en');
218
+ const enJsonPath = (0, path_1.join)(enLocaleDir, 'flat.json');
219
+ if (!(0, fs_1.existsSync)(enJsonPath)) {
220
+ console.error('āŒ Error: en/flat.json not found. Run "flat en" first.');
221
+ process.exit(1);
222
+ }
223
+ const newLocaleDir = (0, path_1.join)(LOCALES_DIR, locale);
224
+ if ((0, fs_1.existsSync)(newLocaleDir)) {
225
+ console.error(`āŒ Error: Locale "${locale}" already exists`);
226
+ process.exit(1);
227
+ }
228
+ try {
229
+ (0, fs_1.mkdirSync)(newLocaleDir, { recursive: true });
230
+ // ŠšŠ¾ŠæŠøŃ€ŃƒŠµŠ¼ flat.json ŠøŠ· en
231
+ const enFlatData = (0, fs_1.readFileSync)(enJsonPath, 'utf8');
232
+ const newJsonPath = (0, path_1.join)(newLocaleDir, 'flat.json');
233
+ (0, fs_1.writeFileSync)(newJsonPath, enFlatData);
234
+ // ДозГаём гайГ
235
+ const guidePath = (0, path_1.join)(newLocaleDir, 'TRANSLATION_GUIDE.md');
236
+ (0, fs_1.writeFileSync)(guidePath, getTranslationGuide());
237
+ console.log(`āœ… Created locale template: ${newLocaleDir}/`);
238
+ console.log(`šŸ“„ Files created:`);
239
+ console.log(` - flat.json (copy from en, ready for translation)`);
240
+ console.log(` - TRANSLATION_GUIDE.md (translation instructions)`);
241
+ console.log(`\nšŸš€ Next steps:`);
242
+ console.log(` 1. Edit ${locale}/flat.json - translate the values`);
243
+ console.log(` 2. Run: npm run locale nest ${locale}`);
244
+ console.log(` 3. Test your translation in the app`);
245
+ }
246
+ catch (error) {
247
+ console.error('āŒ Error creating template:', error);
248
+ process.exit(1);
249
+ }
250
+ });
251
+ }
252
+ function flattenAction(locale) {
253
+ return __awaiter(this, void 0, void 0, function* () {
254
+ const localeDir = (0, path_1.join)(LOCALES_DIR, locale);
255
+ const tsPath = (0, path_1.join)(localeDir, 'index.ts');
256
+ const jsonPath = (0, path_1.join)(localeDir, 'flat.json');
257
+ const readmePath = (0, path_1.join)(localeDir, 'TRANSLATION_GUIDE.md');
258
+ if (!(0, fs_1.existsSync)(tsPath)) {
259
+ console.error(`āŒ Error: ${tsPath} not found`);
260
+ process.exit(1);
261
+ }
262
+ try {
263
+ const fullPath = (0, url_1.pathToFileURL)((0, path_1.join)(process.cwd(), tsPath)).href;
264
+ const module = yield Promise.resolve(`${fullPath + '?t=' + Date.now()}`).then(s => __importStar(require(s)));
265
+ const nested = module.default;
266
+ const flat = flattenLocale(nested);
267
+ (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(flat, null, 2));
268
+ if (locale === 'en') {
269
+ (0, fs_1.writeFileSync)(readmePath, getTranslationGuide());
270
+ console.log(`āœ… Generated ${readmePath}`);
271
+ }
272
+ console.log(`āœ… Generated ${jsonPath}`);
273
+ }
274
+ catch (error) {
275
+ console.error(`āŒ Error processing ${tsPath}:`, error);
276
+ process.exit(1);
277
+ }
278
+ });
279
+ }
280
+ function nestifyAction(locale) {
281
+ return __awaiter(this, void 0, void 0, function* () {
282
+ const localeDir = (0, path_1.join)(LOCALES_DIR, locale);
283
+ const jsonPath = (0, path_1.join)(localeDir, 'flat.json');
284
+ const tsPath = (0, path_1.join)(localeDir, 'index.ts');
285
+ if (!(0, fs_1.existsSync)(jsonPath)) {
286
+ console.error(`āŒ Error: ${jsonPath} not found`);
287
+ process.exit(1);
288
+ }
289
+ try {
290
+ const flatData = JSON.parse((0, fs_1.readFileSync)(jsonPath, 'utf8'));
291
+ const nested = nestifyLocale(flatData);
292
+ const tsContent = `import type { LocaleSchema } from '../_types/interfaces';
293
+ ${locale !== 'en' ? `import { DeepPartial } from '../../types/definitions';\n` : ''}
294
+
295
+ const Locale: ${locale !== 'en' ? 'DeepPartial<LocaleSchema>' : 'LocaleSchema'} = ${JSON.stringify(nested, null, 4)};
296
+
297
+ export default Locale;`;
298
+ (0, fs_1.writeFileSync)(tsPath, tsContent);
299
+ console.log(`āœ… Generated ${tsPath}`);
300
+ if (locale === 'en') {
301
+ yield generateTypes();
302
+ }
303
+ }
304
+ catch (error) {
305
+ console.error(`āŒ Error processing ${jsonPath}:`, error);
306
+ process.exit(1);
307
+ }
308
+ });
309
+ }
310
+ function checkAllLocalesAction() {
311
+ return __awaiter(this, void 0, void 0, function* () {
312
+ const enFlatPath = (0, path_1.join)(LOCALES_DIR, 'en', 'flat.json');
313
+ if (!(0, fs_1.existsSync)(enFlatPath)) {
314
+ console.error('āŒ Error: en/flat.json not found');
315
+ process.exit(1);
316
+ }
317
+ const enFlat = JSON.parse((0, fs_1.readFileSync)(enFlatPath, 'utf8'));
318
+ const enKeys = new Set(Object.keys(enFlat));
319
+ const totalKeys = enKeys.size;
320
+ const locales = (0, fs_1.readdirSync)(LOCALES_DIR, { withFileTypes: true })
321
+ .filter((dirent) => dirent.isDirectory() &&
322
+ dirent.name !== 'en' &&
323
+ dirent.name !== '_types')
324
+ .map((dirent) => dirent.name);
325
+ const results = [];
326
+ // Process English
327
+ console.log(`āœ… Locale en: ${totalKeys}/${totalKeys} keys (100%) āœ…\n`);
328
+ // Process other locales
329
+ for (const locale of locales) {
330
+ const jsonPath = (0, path_1.join)(LOCALES_DIR, locale, 'flat.json');
331
+ if (!(0, fs_1.existsSync)(jsonPath)) {
332
+ console.warn(`āš ļø Warning: ${jsonPath} not found\n`);
333
+ continue;
334
+ }
335
+ try {
336
+ const flatData = JSON.parse((0, fs_1.readFileSync)(jsonPath, 'utf8'));
337
+ const flatKeys = new Set(Object.keys(flatData));
338
+ const missingKeys = [...enKeys].filter((key) => !flatKeys.has(key));
339
+ const extraKeys = [...flatKeys].filter((key) => !enKeys.has(key));
340
+ const completed = totalKeys - missingKeys.length;
341
+ const percentage = Math.round((completed / totalKeys) * 100 * 10) / 10;
342
+ results.push({
343
+ locale,
344
+ completed,
345
+ missing: missingKeys,
346
+ extra: extraKeys,
347
+ percentage,
348
+ });
349
+ // Status icon based on completion
350
+ let statusIcon = 'āœ…';
351
+ if (percentage < 50)
352
+ statusIcon = 'šŸ”“';
353
+ else if (percentage < 80)
354
+ statusIcon = '🟔';
355
+ console.log(`${statusIcon} Locale ${locale}: ${completed}/${totalKeys} keys (${percentage}%) ${statusIcon}`);
356
+ if (missingKeys.length > 0) {
357
+ console.log(`āŒ Missing keys:`);
358
+ // Group missing keys by section for better readability
359
+ const keysBySection = new Map();
360
+ missingKeys.forEach((key) => {
361
+ const section = key.split('.').slice(0, 2).join('.');
362
+ if (!keysBySection.has(section)) {
363
+ keysBySection.set(section, []);
364
+ }
365
+ keysBySection.get(section).push(key);
366
+ });
367
+ keysBySection.forEach((keys, section) => {
368
+ if (keys.length === 1) {
369
+ console.log(` - ${keys[0]}`);
370
+ }
371
+ else if (keys.length <= 3) {
372
+ keys.forEach((key) => console.log(` - ${key}`));
373
+ }
374
+ else {
375
+ console.log(` - ${section}.* (${keys.length} keys missing)`);
376
+ console.log(` Examples: ${keys
377
+ .slice(0, 2)
378
+ .map((k) => k.split('.').pop())
379
+ .join(', ')}...`);
380
+ }
381
+ });
382
+ }
383
+ if (extraKeys.length > 0) {
384
+ console.log(`āš ļø Extra keys: ${extraKeys.join(', ')}`);
385
+ }
386
+ console.log(''); // Empty line between locales
387
+ }
388
+ catch (error) {
389
+ console.error(`āŒ Error validating ${jsonPath}:`, error);
390
+ }
391
+ }
392
+ // Summary report
393
+ if (results.length > 0) {
394
+ console.log('šŸ“Š Translation Progress Summary:');
395
+ console.log(` English: ${totalKeys}/${totalKeys} (100%) āœ…`);
396
+ results
397
+ .sort((a, b) => b.percentage - a.percentage)
398
+ .forEach((result) => {
399
+ let statusIcon = 'āœ…';
400
+ if (result.percentage < 50)
401
+ statusIcon = 'šŸ”“';
402
+ else if (result.percentage < 80)
403
+ statusIcon = '🟔';
404
+ const padding = ' '.repeat(8 - result.locale.length);
405
+ console.log(` ${result.locale}:${padding}${result.completed}/${totalKeys} (${result.percentage}%) ${statusIcon}`);
406
+ });
407
+ const avgCompletion = Math.round((results.reduce((sum, r) => sum + r.percentage, 0) /
408
+ results.length) *
409
+ 10) / 10;
410
+ console.log(`\nšŸ“ˆ Average completion: ${avgCompletion}%`);
411
+ }
412
+ });
413
+ }
414
+ function updateAllNested() {
415
+ return __awaiter(this, void 0, void 0, function* () {
416
+ const locales = (0, fs_1.readdirSync)(LOCALES_DIR, { withFileTypes: true })
417
+ .filter((dirent) => dirent.isDirectory() && dirent.name !== '_types')
418
+ .map((dirent) => dirent.name);
419
+ for (const locale of locales) {
420
+ try {
421
+ yield nestifyAction(locale);
422
+ console.log(`āœ… Updated nested structure for locale: ${locale}\n`);
423
+ }
424
+ catch (error) {
425
+ console.error(`āŒ Error updating locale ${locale}:`, error);
426
+ }
427
+ }
428
+ });
429
+ }
430
+ const program = new commander_1.Command();
431
+ program
432
+ .name('locale-tool')
433
+ .description('Locale management tool for translations')
434
+ .version('1.0.0');
435
+ program
436
+ .command('flat')
437
+ .argument('<locale>', 'locale code (e.g., en, ru, de)')
438
+ .description('Convert TypeScript locale to flat JSON format')
439
+ .action(flattenAction);
440
+ program
441
+ .command('nest')
442
+ .argument('<locale>', 'locale code (e.g., en, ru, de)')
443
+ .description('Convert flat JSON to nested TypeScript format')
444
+ .action(nestifyAction);
445
+ program
446
+ .command('types')
447
+ .description('Generate TypeScript interfaces from en/flat.json')
448
+ .action(generateTypes);
449
+ program
450
+ .command('template')
451
+ .argument('<locale>', 'new locale code (e.g., de, fr, es)')
452
+ .description('Create new locale template from en/flat.json')
453
+ .action(createTemplate);
454
+ program
455
+ .command('check-all')
456
+ .description('Check all locales against en/flat.json')
457
+ .action(checkAllLocalesAction);
458
+ program
459
+ .command('update-all-nested')
460
+ .description('Update nested TypeScript files for all locales from their flat.json')
461
+ .action(updateAllNested);
462
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "locale-tool",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "devDependencies": {
7
+ "@types/node": "^24.0.4",
8
+ "typescript": "^5.5.3"
9
+ },
10
+ "private": false,
11
+ "dependencies": {
12
+ "commander": "^14.0.0"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc"
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import {
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ } from 'fs';
10
+ import { join } from 'path';
11
+ import { pathToFileURL } from 'url';
12
+
13
+ type NestedObject = {
14
+ [key: string]: string | string[] | NestedObject;
15
+ };
16
+
17
+ type FlatObject = Record<string, string | string[]>;
18
+
19
+ const LOCALES_DIR = './src/lang/locale';
20
+ const TYPES_DIR = join(LOCALES_DIR, '_types');
21
+ const INTERFACES_FILE = join(TYPES_DIR, 'interfaces.ts');
22
+
23
+ function flattenLocale(nested: NestedObject): FlatObject {
24
+ const result: FlatObject = {};
25
+
26
+ function traverse(obj: NestedObject, path: string = ''): void {
27
+ for (const [key, value] of Object.entries(obj)) {
28
+ const currentPath = path ? `${path}.${key}` : key;
29
+
30
+ if (Array.isArray(value)) {
31
+ result[currentPath] = value;
32
+ } else if (typeof value === 'object' && value !== null) {
33
+ traverse(value, currentPath);
34
+ } else if (typeof value === 'string') {
35
+ result[currentPath] = value;
36
+ }
37
+ }
38
+ }
39
+
40
+ traverse(nested);
41
+ return result;
42
+ }
43
+
44
+ function nestifyLocale(flat: FlatObject): NestedObject {
45
+ const result: NestedObject = {};
46
+
47
+ for (const [path, value] of Object.entries(flat)) {
48
+ const keys = path.split('.');
49
+ let current: any = result;
50
+
51
+ for (let i = 0; i < keys.length - 1; i++) {
52
+ const key = keys[i];
53
+ current[key] ??= {};
54
+ current = current[key];
55
+ }
56
+
57
+ const lastKey = keys[keys.length - 1];
58
+ current[lastKey] = value;
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ function generateTypesFromNest(nest: NestedObject): string {
65
+ const interfaceLines: string[] = [];
66
+
67
+ function traverse(
68
+ obj: NestedObject,
69
+ indent: number = 1,
70
+ path: string = ''
71
+ ): void {
72
+ for (const [key, value] of Object.entries(obj)) {
73
+ const indentStr = ' '.repeat(indent);
74
+ const currentPath = path ? `${path}.${key}` : key;
75
+
76
+ if (Array.isArray(value)) {
77
+ interfaceLines.push(`${indentStr}${key}: string[];`);
78
+ } else if (typeof value === 'object' && value !== null) {
79
+ interfaceLines.push(`${indentStr}${key}: {`);
80
+ traverse(value, indent + 1, currentPath);
81
+ interfaceLines.push(`${indentStr}};`);
82
+ } else if (typeof value === 'string') {
83
+ interfaceLines.push(`${indentStr}${key}: string;`);
84
+ }
85
+ }
86
+ }
87
+
88
+ traverse(nest);
89
+ return `export interface LocaleSchema {\n${interfaceLines.join('\n')}\n}`;
90
+ }
91
+ function getTranslationGuide(): string {
92
+ return `# Translation Guide
93
+
94
+ ## Getting Started
95
+
96
+ ### 1. Find Your Language Code
97
+ - Visit [ISO 639-1 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to find your language code
98
+ - Use the two-letter code (e.g., \`de\` for German, \`fr\` for French, \`es\` for Spanish)
99
+ - For region-specific variants, use format like \`en-US\`, \`zh-CN\`, \`pt-BR\`
100
+
101
+ ### 2. Start Translating
102
+ 1. Open \`flat.json\` in this folder
103
+ 2. Translate the **values** (keep the keys unchanged)
104
+ 3. When done, run: \`npm run locale nest ${process.argv[3] || 'your-locale'}\`
105
+
106
+ ### 3. Submit Your Translation
107
+ - Create a pull request with your locale folder
108
+ - Include both \`flat.json\` and generated \`index.ts\`
109
+
110
+ ## Translation Rules
111
+
112
+ ### āœ… DO:
113
+ - **Preserve structure**: Keep array and object structures intact
114
+ - \`["item1", "item2"]\` → \`["ŃŠ»ŠµŠ¼ŠµŠ½Ń‚1", "ŃŠ»ŠµŠ¼ŠµŠ½Ń‚2"]\`
115
+ - **Keep variables untouched**: Variables in \`{{brackets}}\` must remain exactly as they are
116
+ - \`"Hello {{name}}"\` → \`"ŠŸŃ€ŠøŠ²ŠµŃ‚ {{name}}"\`
117
+ - **Maintain line breaks**: Keep \`\\n\` for multi-line messages
118
+ - **Respect key hierarchy**: Keys like \`"settings.pages.debug"\` show UI component structure
119
+
120
+ ### āŒ DON'T:
121
+ - Change or remove JSON keys
122
+ - Translate variables in \`{{brackets}}\`
123
+ - Reorder array elements
124
+ - Break JSON syntax (missing quotes, commas, brackets)
125
+
126
+ ## Examples
127
+
128
+ ### Simple String
129
+ \`\`\`json
130
+ "commands.togglePanels.notice.shown": "Control panels shown"
131
+ // āœ… Should become:
132
+ "commands.togglePanels.notice.shown": "Панели ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŃ показаны"
133
+ \`\`\`
134
+
135
+ ### String with Variables
136
+ \`\`\`json
137
+ "settings.pages.debug.clearLogsStorage.desc": "Storage: {{storage}}, Entries: {{entries}}"
138
+ // āœ… Should become:
139
+ "settings.pages.debug.clearLogsStorage.desc": "Єранилище: {{storage}}, Записи: {{entries}}"
140
+ \`\`\`
141
+
142
+ ### Array of Strings
143
+ \`\`\`json
144
+ "settings.pages.debug.reportIssue.desc": [
145
+ "If you encounter any issues, please report them.",
146
+ "How to report:",
147
+ "1. Enable debug logging"
148
+ ]
149
+ // āœ… Should become:
150
+ "settings.pages.debug.reportIssue.desc": [
151
+ "Если у вас возникли проблемы, сообщите о них.",
152
+ "Как ŃŠ¾Š¾Š±Ń‰ŠøŃ‚ŃŒ:",
153
+ "1. Š’ŠŗŠ»ŃŽŃ‡ŠøŃ‚Šµ отлаГочное логирование"
154
+ ]
155
+ \`\`\`
156
+
157
+ ## Best Practices
158
+
159
+ - **Natural translation**: Translate the meaning, not word-by-word
160
+ - **Keep UI context**: Consider where the text appears (buttons, tooltips, messages)
161
+ - **Test length**: Some UI elements have space constraints - shorten if needed
162
+ - **Maintain tone**: Keep the same level of formality as the original
163
+ - **Handle pluralization**: Adapt to your language's plural rules
164
+
165
+ ## Need Help?
166
+ - Check existing translations in other language folders for reference
167
+ - Ask questions in GitHub issues before starting large translations
168
+ - Test your JSON syntax using online JSON validators`;
169
+ }
170
+
171
+ async function generateTypes(): Promise<void> {
172
+ const enLocaleDir = join(LOCALES_DIR, 'en');
173
+ const jsonPath = join(enLocaleDir, 'flat.json');
174
+ const tsPath = join(enLocaleDir, 'index.ts');
175
+
176
+ if (!existsSync(jsonPath)) {
177
+ console.error('āŒ Error: en/flat.json not found. Run "flat en" first.');
178
+ process.exit(1);
179
+ }
180
+
181
+ try {
182
+ const fullPath = pathToFileURL(join(process.cwd(), tsPath)).href;
183
+ const module = await import(fullPath + '?t=' + Date.now());
184
+ const nested = module.default;
185
+
186
+ const types = generateTypesFromNest(nested);
187
+
188
+ if (!existsSync(TYPES_DIR)) {
189
+ mkdirSync(TYPES_DIR, { recursive: true });
190
+ }
191
+
192
+ writeFileSync(INTERFACES_FILE, types);
193
+ console.log(`āœ… Generated ${INTERFACES_FILE}`);
194
+ } catch (error) {
195
+ console.error('āŒ Error generating types:', error);
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ async function createTemplate(locale: string): Promise<void> {
201
+ const enLocaleDir = join(LOCALES_DIR, 'en');
202
+ const enJsonPath = join(enLocaleDir, 'flat.json');
203
+
204
+ if (!existsSync(enJsonPath)) {
205
+ console.error('āŒ Error: en/flat.json not found. Run "flat en" first.');
206
+ process.exit(1);
207
+ }
208
+
209
+ const newLocaleDir = join(LOCALES_DIR, locale);
210
+
211
+ if (existsSync(newLocaleDir)) {
212
+ console.error(`āŒ Error: Locale "${locale}" already exists`);
213
+ process.exit(1);
214
+ }
215
+
216
+ try {
217
+ mkdirSync(newLocaleDir, { recursive: true });
218
+
219
+ // ŠšŠ¾ŠæŠøŃ€ŃƒŠµŠ¼ flat.json ŠøŠ· en
220
+ const enFlatData = readFileSync(enJsonPath, 'utf8');
221
+ const newJsonPath = join(newLocaleDir, 'flat.json');
222
+ writeFileSync(newJsonPath, enFlatData);
223
+
224
+ // ДозГаём гайГ
225
+ const guidePath = join(newLocaleDir, 'TRANSLATION_GUIDE.md');
226
+ writeFileSync(guidePath, getTranslationGuide());
227
+
228
+ console.log(`āœ… Created locale template: ${newLocaleDir}/`);
229
+ console.log(`šŸ“„ Files created:`);
230
+ console.log(` - flat.json (copy from en, ready for translation)`);
231
+ console.log(` - TRANSLATION_GUIDE.md (translation instructions)`);
232
+ console.log(`\nšŸš€ Next steps:`);
233
+ console.log(` 1. Edit ${locale}/flat.json - translate the values`);
234
+ console.log(` 2. Run: npm run locale nest ${locale}`);
235
+ console.log(` 3. Test your translation in the app`);
236
+ } catch (error) {
237
+ console.error('āŒ Error creating template:', error);
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ async function flattenAction(locale: string): Promise<void> {
243
+ const localeDir = join(LOCALES_DIR, locale);
244
+ const tsPath = join(localeDir, 'index.ts');
245
+ const jsonPath = join(localeDir, 'flat.json');
246
+ const readmePath = join(localeDir, 'TRANSLATION_GUIDE.md');
247
+
248
+ if (!existsSync(tsPath)) {
249
+ console.error(`āŒ Error: ${tsPath} not found`);
250
+ process.exit(1);
251
+ }
252
+
253
+ try {
254
+ const fullPath = pathToFileURL(join(process.cwd(), tsPath)).href;
255
+ const module = await import(fullPath + '?t=' + Date.now());
256
+ const nested = module.default;
257
+
258
+ const flat = flattenLocale(nested);
259
+ writeFileSync(jsonPath, JSON.stringify(flat, null, 2));
260
+
261
+ if (locale === 'en') {
262
+ writeFileSync(readmePath, getTranslationGuide());
263
+ console.log(`āœ… Generated ${readmePath}`);
264
+ }
265
+
266
+ console.log(`āœ… Generated ${jsonPath}`);
267
+ } catch (error) {
268
+ console.error(`āŒ Error processing ${tsPath}:`, error);
269
+ process.exit(1);
270
+ }
271
+ }
272
+
273
+ async function nestifyAction(locale: string): Promise<void> {
274
+ const localeDir = join(LOCALES_DIR, locale);
275
+ const jsonPath = join(localeDir, 'flat.json');
276
+ const tsPath = join(localeDir, 'index.ts');
277
+
278
+ if (!existsSync(jsonPath)) {
279
+ console.error(`āŒ Error: ${jsonPath} not found`);
280
+ process.exit(1);
281
+ }
282
+
283
+ try {
284
+ const flatData = JSON.parse(readFileSync(jsonPath, 'utf8'));
285
+ const nested = nestifyLocale(flatData);
286
+
287
+ const tsContent = `import type { LocaleSchema } from '../_types/interfaces';
288
+ ${locale !== 'en' ? `import { DeepPartial } from '../../types/definitions';\n` : ''}
289
+
290
+ const Locale: ${locale !== 'en' ? 'DeepPartial<LocaleSchema>' : 'LocaleSchema'} = ${JSON.stringify(nested, null, 4)};
291
+
292
+ export default Locale;`;
293
+
294
+ writeFileSync(tsPath, tsContent);
295
+ console.log(`āœ… Generated ${tsPath}`);
296
+
297
+ if (locale === 'en') {
298
+ await generateTypes();
299
+ }
300
+ } catch (error) {
301
+ console.error(`āŒ Error processing ${jsonPath}:`, error);
302
+ process.exit(1);
303
+ }
304
+ }
305
+
306
+ async function checkAllLocalesAction(): Promise<void> {
307
+ const enFlatPath = join(LOCALES_DIR, 'en', 'flat.json');
308
+ if (!existsSync(enFlatPath)) {
309
+ console.error('āŒ Error: en/flat.json not found');
310
+ process.exit(1);
311
+ }
312
+
313
+ const enFlat: FlatObject = JSON.parse(readFileSync(enFlatPath, 'utf8'));
314
+ const enKeys = new Set(Object.keys(enFlat));
315
+ const totalKeys = enKeys.size;
316
+
317
+ const locales = readdirSync(LOCALES_DIR, { withFileTypes: true })
318
+ .filter(
319
+ (dirent) =>
320
+ dirent.isDirectory() &&
321
+ dirent.name !== 'en' &&
322
+ dirent.name !== '_types'
323
+ )
324
+ .map((dirent) => dirent.name);
325
+
326
+ const results: Array<{
327
+ locale: string;
328
+ completed: number;
329
+ missing: string[];
330
+ extra: string[];
331
+ percentage: number;
332
+ }> = [];
333
+
334
+ // Process English
335
+ console.log(`āœ… Locale en: ${totalKeys}/${totalKeys} keys (100%) āœ…\n`);
336
+
337
+ // Process other locales
338
+ for (const locale of locales) {
339
+ const jsonPath = join(LOCALES_DIR, locale, 'flat.json');
340
+ if (!existsSync(jsonPath)) {
341
+ console.warn(`āš ļø Warning: ${jsonPath} not found\n`);
342
+ continue;
343
+ }
344
+
345
+ try {
346
+ const flatData: FlatObject = JSON.parse(
347
+ readFileSync(jsonPath, 'utf8')
348
+ );
349
+ const flatKeys = new Set(Object.keys(flatData));
350
+
351
+ const missingKeys = [...enKeys].filter((key) => !flatKeys.has(key));
352
+ const extraKeys = [...flatKeys].filter((key) => !enKeys.has(key));
353
+ const completed = totalKeys - missingKeys.length;
354
+ const percentage =
355
+ Math.round((completed / totalKeys) * 100 * 10) / 10;
356
+
357
+ results.push({
358
+ locale,
359
+ completed,
360
+ missing: missingKeys,
361
+ extra: extraKeys,
362
+ percentage,
363
+ });
364
+
365
+ // Status icon based on completion
366
+ let statusIcon = 'āœ…';
367
+ if (percentage < 50) statusIcon = 'šŸ”“';
368
+ else if (percentage < 80) statusIcon = '🟔';
369
+
370
+ console.log(
371
+ `${statusIcon} Locale ${locale}: ${completed}/${totalKeys} keys (${percentage}%) ${statusIcon}`
372
+ );
373
+
374
+ if (missingKeys.length > 0) {
375
+ console.log(`āŒ Missing keys:`);
376
+ // Group missing keys by section for better readability
377
+ const keysBySection = new Map<string, string[]>();
378
+
379
+ missingKeys.forEach((key) => {
380
+ const section = key.split('.').slice(0, 2).join('.');
381
+ if (!keysBySection.has(section)) {
382
+ keysBySection.set(section, []);
383
+ }
384
+ keysBySection.get(section)!.push(key);
385
+ });
386
+
387
+ keysBySection.forEach((keys, section) => {
388
+ if (keys.length === 1) {
389
+ console.log(` - ${keys[0]}`);
390
+ } else if (keys.length <= 3) {
391
+ keys.forEach((key) => console.log(` - ${key}`));
392
+ } else {
393
+ console.log(
394
+ ` - ${section}.* (${keys.length} keys missing)`
395
+ );
396
+ console.log(
397
+ ` Examples: ${keys
398
+ .slice(0, 2)
399
+ .map((k) => k.split('.').pop())
400
+ .join(', ')}...`
401
+ );
402
+ }
403
+ });
404
+ }
405
+
406
+ if (extraKeys.length > 0) {
407
+ console.log(`āš ļø Extra keys: ${extraKeys.join(', ')}`);
408
+ }
409
+
410
+ console.log(''); // Empty line between locales
411
+ } catch (error) {
412
+ console.error(`āŒ Error validating ${jsonPath}:`, error);
413
+ }
414
+ }
415
+
416
+ // Summary report
417
+ if (results.length > 0) {
418
+ console.log('šŸ“Š Translation Progress Summary:');
419
+ console.log(` English: ${totalKeys}/${totalKeys} (100%) āœ…`);
420
+
421
+ results
422
+ .sort((a, b) => b.percentage - a.percentage)
423
+ .forEach((result) => {
424
+ let statusIcon = 'āœ…';
425
+ if (result.percentage < 50) statusIcon = 'šŸ”“';
426
+ else if (result.percentage < 80) statusIcon = '🟔';
427
+
428
+ const padding = ' '.repeat(8 - result.locale.length);
429
+ console.log(
430
+ ` ${result.locale}:${padding}${result.completed}/${totalKeys} (${result.percentage}%) ${statusIcon}`
431
+ );
432
+ });
433
+
434
+ const avgCompletion =
435
+ Math.round(
436
+ (results.reduce((sum, r) => sum + r.percentage, 0) /
437
+ results.length) *
438
+ 10
439
+ ) / 10;
440
+ console.log(`\nšŸ“ˆ Average completion: ${avgCompletion}%`);
441
+ }
442
+ }
443
+
444
+ async function updateAllNested(): Promise<void> {
445
+ const locales = readdirSync(LOCALES_DIR, { withFileTypes: true })
446
+ .filter((dirent) => dirent.isDirectory() && dirent.name !== '_types')
447
+ .map((dirent) => dirent.name);
448
+
449
+ for (const locale of locales) {
450
+ try {
451
+ await nestifyAction(locale);
452
+ console.log(`āœ… Updated nested structure for locale: ${locale}\n`);
453
+ } catch (error) {
454
+ console.error(`āŒ Error updating locale ${locale}:`, error);
455
+ }
456
+ }
457
+ }
458
+
459
+ const program = new Command();
460
+
461
+ program
462
+ .name('locale-tool')
463
+ .description('Locale management tool for translations')
464
+ .version('1.0.0');
465
+
466
+ program
467
+ .command('flat')
468
+ .argument('<locale>', 'locale code (e.g., en, ru, de)')
469
+ .description('Convert TypeScript locale to flat JSON format')
470
+ .action(flattenAction);
471
+
472
+ program
473
+ .command('nest')
474
+ .argument('<locale>', 'locale code (e.g., en, ru, de)')
475
+ .description('Convert flat JSON to nested TypeScript format')
476
+ .action(nestifyAction);
477
+
478
+ program
479
+ .command('types')
480
+ .description('Generate TypeScript interfaces from en/flat.json')
481
+ .action(generateTypes);
482
+
483
+ program
484
+ .command('template')
485
+ .argument('<locale>', 'new locale code (e.g., de, fr, es)')
486
+ .description('Create new locale template from en/flat.json')
487
+ .action(createTemplate);
488
+
489
+ program
490
+ .command('check-all')
491
+ .description('Check all locales against en/flat.json')
492
+ .action(checkAllLocalesAction);
493
+
494
+ program
495
+ .command('update-all-nested')
496
+ .description(
497
+ 'Update nested TypeScript files for all locales from their flat.json'
498
+ )
499
+ .action(updateAllNested);
500
+
501
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2016",
4
+ "module": "commonjs",
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["src"]
12
+ }