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.
- package/.idea/locale-tool.iml +12 -0
- package/.idea/material_theme_project_new.xml +17 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/index.js +462 -0
- package/package.json +17 -0
- package/src/index.ts +501 -0
- package/tsconfig.json +12 -0
@@ -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
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