i18n-ai-cli 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/README.md +498 -0
- package/dist/cli.js +1013 -0
- package/dist/cli.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk8 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/config/config-loader.ts
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var CONFIG_FILE_NAME = "i18n-cli.config.json";
|
|
12
|
+
var ConfigSchema = z.object({
|
|
13
|
+
localesPath: z.string().min(1),
|
|
14
|
+
defaultLocale: z.string().min(2),
|
|
15
|
+
supportedLocales: z.array(z.string().min(2)),
|
|
16
|
+
keyStyle: z.enum(["flat", "nested"]).default("nested"),
|
|
17
|
+
usagePatterns: z.array(z.string()).default([]),
|
|
18
|
+
autoSort: z.boolean().default(true)
|
|
19
|
+
});
|
|
20
|
+
function resolveConfigPath() {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
return path.join(cwd, CONFIG_FILE_NAME);
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig() {
|
|
25
|
+
const configPath = resolveConfigPath();
|
|
26
|
+
if (!await fs.pathExists(configPath)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Configuration file "${CONFIG_FILE_NAME}" not found in project root.
|
|
29
|
+
Run "i18n-cli init" to create one.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
let rawConfig;
|
|
33
|
+
try {
|
|
34
|
+
rawConfig = await fs.readJson(configPath);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to parse ${CONFIG_FILE_NAME}. Ensure it contains valid JSON.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const parsed = ConfigSchema.safeParse(rawConfig);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
const errors = parsed.error.issues.map((e) => `\u2022 ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid configuration in ${CONFIG_FILE_NAME}:
|
|
45
|
+
${errors}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const config = parsed.data;
|
|
49
|
+
validateConfigLogic(config);
|
|
50
|
+
const compiledUsagePatterns = compileUsagePatterns(
|
|
51
|
+
config.usagePatterns
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
...config,
|
|
55
|
+
compiledUsagePatterns
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function validateConfigLogic(config) {
|
|
59
|
+
if (!config.supportedLocales.includes(config.defaultLocale)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`defaultLocale "${config.defaultLocale}" must be included in supportedLocales.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const duplicates = findDuplicates(config.supportedLocales);
|
|
65
|
+
if (duplicates.length > 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Duplicate locales found in supportedLocales: ${duplicates.join(", ")}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function compileUsagePatterns(patterns) {
|
|
72
|
+
if (patterns.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return patterns.map((pattern, index) => {
|
|
76
|
+
let regex;
|
|
77
|
+
try {
|
|
78
|
+
regex = new RegExp(pattern, "g");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid regex in usagePatterns[${index}]: ${String(err)}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const groupCount = countCapturingGroups(pattern);
|
|
85
|
+
if (groupCount === 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`usagePatterns[${index}] must include a capturing group (use a named group like "(?<key>...)" or a standard "(...)").`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return regex;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function countCapturingGroups(pattern) {
|
|
94
|
+
let count = 0;
|
|
95
|
+
let inCharClass = false;
|
|
96
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
97
|
+
const char = pattern[i];
|
|
98
|
+
if (char === "\\") {
|
|
99
|
+
i += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === "[") {
|
|
103
|
+
inCharClass = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (char === "]" && inCharClass) {
|
|
107
|
+
inCharClass = false;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (inCharClass) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char !== "(") {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const next = pattern[i + 1];
|
|
117
|
+
if (next !== "?") {
|
|
118
|
+
count += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const next2 = pattern[i + 2];
|
|
122
|
+
if (next2 !== "<") {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const next3 = pattern[i + 3];
|
|
126
|
+
if (next3 === "=" || next3 === "!") {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
count += 1;
|
|
130
|
+
}
|
|
131
|
+
return count;
|
|
132
|
+
}
|
|
133
|
+
function findDuplicates(arr) {
|
|
134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
135
|
+
const duplicates = [];
|
|
136
|
+
for (const item of arr) {
|
|
137
|
+
if (seen.has(item)) {
|
|
138
|
+
duplicates.push(item);
|
|
139
|
+
}
|
|
140
|
+
seen.add(item);
|
|
141
|
+
}
|
|
142
|
+
return duplicates;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/core/file-manager.ts
|
|
146
|
+
import fs2 from "fs-extra";
|
|
147
|
+
import path2 from "path";
|
|
148
|
+
var FileManager = class {
|
|
149
|
+
localesPath;
|
|
150
|
+
config;
|
|
151
|
+
constructor(config) {
|
|
152
|
+
this.config = config;
|
|
153
|
+
this.localesPath = path2.resolve(process.cwd(), config.localesPath);
|
|
154
|
+
}
|
|
155
|
+
getLocaleFilePath(locale) {
|
|
156
|
+
return path2.join(this.localesPath, `${locale}.json`);
|
|
157
|
+
}
|
|
158
|
+
async ensureLocalesDirectory() {
|
|
159
|
+
await fs2.ensureDir(this.localesPath);
|
|
160
|
+
}
|
|
161
|
+
async localeExists(locale) {
|
|
162
|
+
const filePath = this.getLocaleFilePath(locale);
|
|
163
|
+
return fs2.pathExists(filePath);
|
|
164
|
+
}
|
|
165
|
+
async listLocales() {
|
|
166
|
+
return this.config.supportedLocales;
|
|
167
|
+
}
|
|
168
|
+
async readLocale(locale) {
|
|
169
|
+
const filePath = this.getLocaleFilePath(locale);
|
|
170
|
+
if (!await fs2.pathExists(filePath)) {
|
|
171
|
+
throw new Error(`Locale file "${locale}.json" does not exist.`);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return await fs2.readJson(filePath);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(`Invalid JSON in "${locale}.json".`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async writeLocale(locale, data, options) {
|
|
180
|
+
const filePath = this.getLocaleFilePath(locale);
|
|
181
|
+
const finalData = this.config.autoSort ? this.sortKeysRecursively(data) : data;
|
|
182
|
+
if (options?.dryRun) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await fs2.writeJson(filePath, finalData, { spaces: 2 });
|
|
186
|
+
}
|
|
187
|
+
async deleteLocale(locale, options) {
|
|
188
|
+
const filePath = this.getLocaleFilePath(locale);
|
|
189
|
+
if (!await fs2.pathExists(filePath)) {
|
|
190
|
+
throw new Error(`Locale "${locale}" does not exist.`);
|
|
191
|
+
}
|
|
192
|
+
if (options?.dryRun) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await fs2.remove(filePath);
|
|
196
|
+
}
|
|
197
|
+
async createLocale(locale, initialData, options) {
|
|
198
|
+
await this.ensureLocalesDirectory();
|
|
199
|
+
const filePath = this.getLocaleFilePath(locale);
|
|
200
|
+
if (await fs2.pathExists(filePath)) {
|
|
201
|
+
throw new Error(`Locale "${locale}" already exists.`);
|
|
202
|
+
}
|
|
203
|
+
if (options?.dryRun) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await fs2.writeJson(filePath, initialData, { spaces: 2 });
|
|
207
|
+
}
|
|
208
|
+
sortKeysRecursively(obj) {
|
|
209
|
+
if (Array.isArray(obj)) {
|
|
210
|
+
return obj.map((item) => this.sortKeysRecursively(item));
|
|
211
|
+
}
|
|
212
|
+
if (obj !== null && typeof obj === "object") {
|
|
213
|
+
return Object.keys(obj).sort().reduce((acc, key) => {
|
|
214
|
+
acc[key] = this.sortKeysRecursively(obj[key]);
|
|
215
|
+
return acc;
|
|
216
|
+
}, /* @__PURE__ */ Object.create(null));
|
|
217
|
+
}
|
|
218
|
+
return obj;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/context/build-context.ts
|
|
223
|
+
async function buildContext(options) {
|
|
224
|
+
const config = await loadConfig();
|
|
225
|
+
const fileManager = new FileManager(config);
|
|
226
|
+
return {
|
|
227
|
+
config,
|
|
228
|
+
fileManager,
|
|
229
|
+
options
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/commands/init.ts
|
|
234
|
+
import chalk from "chalk";
|
|
235
|
+
import fs3 from "fs-extra";
|
|
236
|
+
import inquirer2 from "inquirer";
|
|
237
|
+
import path3 from "path";
|
|
238
|
+
|
|
239
|
+
// src/core/confirmation.ts
|
|
240
|
+
import inquirer from "inquirer";
|
|
241
|
+
async function confirmAction(message, options = {}) {
|
|
242
|
+
const { skip = false, defaultValue = false, ci = false } = options;
|
|
243
|
+
if (skip) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (ci) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"Confirmation required in CI mode. Re-run with --yes to proceed."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (!process.stdout.isTTY) {
|
|
252
|
+
return defaultValue;
|
|
253
|
+
}
|
|
254
|
+
const { confirmed } = await inquirer.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: "confirm",
|
|
257
|
+
name: "confirmed",
|
|
258
|
+
message,
|
|
259
|
+
default: defaultValue
|
|
260
|
+
}
|
|
261
|
+
]);
|
|
262
|
+
return confirmed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/commands/init.ts
|
|
266
|
+
var DEFAULT_USAGE_PATTERNS = [
|
|
267
|
+
`t\\(['"](.*?)['"]\\)`,
|
|
268
|
+
`translate\\(['"](.*?)['"]\\)`,
|
|
269
|
+
`i18n\\.t\\(['"](.*?)['"]\\)`
|
|
270
|
+
];
|
|
271
|
+
async function initCommand(options) {
|
|
272
|
+
const { yes, dryRun, ci, force } = options;
|
|
273
|
+
const configPath = path3.join(process.cwd(), CONFIG_FILE_NAME);
|
|
274
|
+
const configExists = await fs3.pathExists(configPath);
|
|
275
|
+
if (configExists && !force) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Configuration file "${CONFIG_FILE_NAME}" already exists. Use --force to overwrite.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const canPrompt = process.stdout.isTTY && !yes && !ci;
|
|
281
|
+
let config;
|
|
282
|
+
if (canPrompt) {
|
|
283
|
+
const answers = await inquirer2.prompt([
|
|
284
|
+
{
|
|
285
|
+
type: "input",
|
|
286
|
+
name: "localesPath",
|
|
287
|
+
message: "Locales directory",
|
|
288
|
+
default: "./locales",
|
|
289
|
+
validate: (input) => input.trim().length > 0 || "Please provide a locales path."
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: "input",
|
|
293
|
+
name: "defaultLocale",
|
|
294
|
+
message: "Default locale",
|
|
295
|
+
default: "en",
|
|
296
|
+
validate: (input) => input.trim().length >= 2 || "Default locale must be at least 2 characters."
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: "input",
|
|
300
|
+
name: "supportedLocales",
|
|
301
|
+
message: "Supported locales (comma-separated)",
|
|
302
|
+
default: (answers2) => answers2.defaultLocale
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
type: "list",
|
|
306
|
+
name: "keyStyle",
|
|
307
|
+
message: "Key style",
|
|
308
|
+
choices: ["nested", "flat"],
|
|
309
|
+
default: "nested"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
type: "confirm",
|
|
313
|
+
name: "autoSort",
|
|
314
|
+
message: "Auto-sort keys?",
|
|
315
|
+
default: true
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
type: "confirm",
|
|
319
|
+
name: "useDefaultUsagePatterns",
|
|
320
|
+
message: "Use default usagePatterns?",
|
|
321
|
+
default: true
|
|
322
|
+
}
|
|
323
|
+
]);
|
|
324
|
+
let usagePatterns = DEFAULT_USAGE_PATTERNS;
|
|
325
|
+
if (!answers.useDefaultUsagePatterns) {
|
|
326
|
+
usagePatterns = [];
|
|
327
|
+
let addMore = true;
|
|
328
|
+
while (addMore) {
|
|
329
|
+
const { pattern } = await inquirer2.prompt([
|
|
330
|
+
{
|
|
331
|
+
type: "input",
|
|
332
|
+
name: "pattern",
|
|
333
|
+
message: "Add usage pattern (regex)",
|
|
334
|
+
validate: (input) => input.trim().length > 0 || "Pattern cannot be empty."
|
|
335
|
+
}
|
|
336
|
+
]);
|
|
337
|
+
usagePatterns.push(pattern.trim());
|
|
338
|
+
const { again } = await inquirer2.prompt([
|
|
339
|
+
{
|
|
340
|
+
type: "confirm",
|
|
341
|
+
name: "again",
|
|
342
|
+
message: "Add another pattern?",
|
|
343
|
+
default: false
|
|
344
|
+
}
|
|
345
|
+
]);
|
|
346
|
+
addMore = again;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const supportedLocales = parseLocales(
|
|
350
|
+
answers.supportedLocales
|
|
351
|
+
);
|
|
352
|
+
config = {
|
|
353
|
+
localesPath: answers.localesPath.trim(),
|
|
354
|
+
defaultLocale: answers.defaultLocale.trim(),
|
|
355
|
+
supportedLocales,
|
|
356
|
+
keyStyle: answers.keyStyle,
|
|
357
|
+
usagePatterns,
|
|
358
|
+
autoSort: answers.autoSort
|
|
359
|
+
};
|
|
360
|
+
} else {
|
|
361
|
+
config = {
|
|
362
|
+
localesPath: "./locales",
|
|
363
|
+
defaultLocale: "en",
|
|
364
|
+
supportedLocales: ["en"],
|
|
365
|
+
keyStyle: "nested",
|
|
366
|
+
usagePatterns: DEFAULT_USAGE_PATTERNS,
|
|
367
|
+
autoSort: true
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
config.supportedLocales = normalizeLocales(
|
|
371
|
+
config.supportedLocales,
|
|
372
|
+
config.defaultLocale
|
|
373
|
+
);
|
|
374
|
+
compileUsagePatterns(config.usagePatterns);
|
|
375
|
+
if (ci && !yes) {
|
|
376
|
+
const action = configExists ? "overwritten" : "created";
|
|
377
|
+
throw new Error(
|
|
378
|
+
`CI mode: configuration file would be ${action}. Re-run with --yes to apply.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (configExists && !yes) {
|
|
382
|
+
const confirmed = await confirmAction(
|
|
383
|
+
`This will overwrite "${CONFIG_FILE_NAME}". Continue?`,
|
|
384
|
+
{ skip: false, ci: ci ?? false }
|
|
385
|
+
);
|
|
386
|
+
if (!confirmed) {
|
|
387
|
+
console.log(chalk.red("\nOperation cancelled."));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (dryRun) {
|
|
392
|
+
console.log(chalk.yellow("\n[DRY RUN] Configuration not written."));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await fs3.writeJson(configPath, config, { spaces: 2 });
|
|
396
|
+
await maybeInitLocales(config, { dryRun: false });
|
|
397
|
+
console.log(
|
|
398
|
+
chalk.green(`
|
|
399
|
+
\u2714 Created ${CONFIG_FILE_NAME} successfully.`)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
function parseLocales(input) {
|
|
403
|
+
return input.split(",").map((locale) => locale.trim()).filter(Boolean);
|
|
404
|
+
}
|
|
405
|
+
function normalizeLocales(locales, defaultLocale) {
|
|
406
|
+
const unique = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const locale of locales) {
|
|
408
|
+
if (locale.length > 0) {
|
|
409
|
+
unique.add(locale);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!unique.has(defaultLocale)) {
|
|
413
|
+
unique.add(defaultLocale);
|
|
414
|
+
}
|
|
415
|
+
return Array.from(unique);
|
|
416
|
+
}
|
|
417
|
+
async function maybeInitLocales(config, options) {
|
|
418
|
+
const localesPath = path3.resolve(
|
|
419
|
+
process.cwd(),
|
|
420
|
+
config.localesPath
|
|
421
|
+
);
|
|
422
|
+
if (options.dryRun) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
await fs3.ensureDir(localesPath);
|
|
426
|
+
const defaultLocaleFile = path3.join(
|
|
427
|
+
localesPath,
|
|
428
|
+
`${config.defaultLocale}.json`
|
|
429
|
+
);
|
|
430
|
+
if (await fs3.pathExists(defaultLocaleFile)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
await fs3.writeJson(defaultLocaleFile, {}, { spaces: 2 });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/commands/add-lang.ts
|
|
437
|
+
import chalk2 from "chalk";
|
|
438
|
+
import ISO6391 from "iso-639-1";
|
|
439
|
+
function isValidLocale(code) {
|
|
440
|
+
const parts = code.split("-");
|
|
441
|
+
if (parts.length === 1) {
|
|
442
|
+
return ISO6391.validate(parts[0]);
|
|
443
|
+
}
|
|
444
|
+
if (parts.length === 2) {
|
|
445
|
+
return ISO6391.validate(parts[0]);
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
async function addLang(lang, options, context) {
|
|
450
|
+
const { config, fileManager } = context;
|
|
451
|
+
const { yes, dryRun, ci } = context.options;
|
|
452
|
+
const locale = lang.trim();
|
|
453
|
+
if (!isValidLocale(locale)) {
|
|
454
|
+
throw new Error(`Invalid language code: ${locale}`);
|
|
455
|
+
}
|
|
456
|
+
if (config.supportedLocales.includes(locale)) {
|
|
457
|
+
throw new Error(`Language ${locale} already exists`);
|
|
458
|
+
}
|
|
459
|
+
const exists = await fileManager.localeExists(locale);
|
|
460
|
+
if (exists) {
|
|
461
|
+
throw new Error(`File already exists: ${locale}.json`);
|
|
462
|
+
}
|
|
463
|
+
let baseContent = {};
|
|
464
|
+
if (options.from) {
|
|
465
|
+
const baseLocale = options.from;
|
|
466
|
+
if (!config.supportedLocales.includes(baseLocale)) {
|
|
467
|
+
throw new Error(`Base locale ${baseLocale} does not exist`);
|
|
468
|
+
}
|
|
469
|
+
baseContent = await fileManager.readLocale(baseLocale);
|
|
470
|
+
}
|
|
471
|
+
console.log(chalk2.cyan(`
|
|
472
|
+
Preparing to add locale:`));
|
|
473
|
+
console.log(chalk2.yellow(` ${locale}
|
|
474
|
+
`));
|
|
475
|
+
if (ci && !yes) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`CI mode: locale "${locale}" would be created. Re-run with --yes to apply.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const confirmed = await confirmAction(
|
|
481
|
+
`This will create new locale "${locale}". Continue?`,
|
|
482
|
+
{ skip: yes ?? false, ci: ci ?? false }
|
|
483
|
+
);
|
|
484
|
+
if (!confirmed) {
|
|
485
|
+
console.log(chalk2.red("\nOperation cancelled."));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
await fileManager.createLocale(locale, baseContent, { dryRun: dryRun ?? false });
|
|
489
|
+
if (dryRun) {
|
|
490
|
+
console.log(
|
|
491
|
+
chalk2.yellow("\n[DRY RUN] No files were modified.")
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
console.log(
|
|
495
|
+
chalk2.green(`
|
|
496
|
+
\u2714 Locale "${locale}" created successfully.`)
|
|
497
|
+
);
|
|
498
|
+
console.log(
|
|
499
|
+
chalk2.gray(
|
|
500
|
+
`Note: Add "${locale}" to supportedLocales in config manually.`
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/commands/remove-lang.ts
|
|
507
|
+
import chalk3 from "chalk";
|
|
508
|
+
async function removeLangCommand(context, lang) {
|
|
509
|
+
const { config, fileManager, options } = context;
|
|
510
|
+
const { yes, dryRun, ci } = options;
|
|
511
|
+
const locale = lang.trim();
|
|
512
|
+
if (!config.supportedLocales.includes(locale)) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Locale "${locale}" is not in supportedLocales.`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (locale === config.defaultLocale) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`Cannot remove default locale "${locale}". Change defaultLocale first.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const exists = await fileManager.localeExists(locale);
|
|
523
|
+
if (!exists) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Locale file "${locale}.json" does not exist.`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
console.log(chalk3.cyan(`
|
|
529
|
+
Preparing to remove locale:`));
|
|
530
|
+
console.log(chalk3.yellow(` ${locale}
|
|
531
|
+
`));
|
|
532
|
+
if (ci && !yes) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`CI mode: locale "${locale}" would be removed. Re-run with --yes to apply.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const confirmed = await confirmAction(
|
|
538
|
+
`This will permanently delete "${locale}.json". Continue?`,
|
|
539
|
+
{ skip: yes ?? false, ci: ci ?? false }
|
|
540
|
+
);
|
|
541
|
+
if (!confirmed) {
|
|
542
|
+
console.log(chalk3.red("\nOperation cancelled."));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
await fileManager.deleteLocale(locale, { dryRun: dryRun ?? false });
|
|
546
|
+
if (dryRun) {
|
|
547
|
+
console.log(
|
|
548
|
+
chalk3.yellow("\n[DRY RUN] No files were modified.")
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
console.log(
|
|
552
|
+
chalk3.green(`
|
|
553
|
+
\u2714 Locale "${locale}" removed successfully.`)
|
|
554
|
+
);
|
|
555
|
+
console.log(
|
|
556
|
+
chalk3.gray(
|
|
557
|
+
`Note: Remove "${locale}" from supportedLocales in config manually.`
|
|
558
|
+
)
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/commands/add-key.ts
|
|
564
|
+
import chalk4 from "chalk";
|
|
565
|
+
|
|
566
|
+
// src/core/object-utils.ts
|
|
567
|
+
var DANGEROUS_KEY_SEGMENTS = /* @__PURE__ */ new Set([
|
|
568
|
+
"__proto__",
|
|
569
|
+
"constructor",
|
|
570
|
+
"prototype"
|
|
571
|
+
]);
|
|
572
|
+
function assertSafeKeySegment(segment) {
|
|
573
|
+
if (DANGEROUS_KEY_SEGMENTS.has(segment)) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Unsafe key segment "${segment}" is not allowed.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function flattenObject(obj, parentKey = "", result = /* @__PURE__ */ Object.create(null)) {
|
|
580
|
+
for (const key of Object.keys(obj)) {
|
|
581
|
+
assertSafeKeySegment(key);
|
|
582
|
+
const value = obj[key];
|
|
583
|
+
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
584
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
585
|
+
flattenObject(value, newKey, result);
|
|
586
|
+
} else {
|
|
587
|
+
result[newKey] = value;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
function unflattenObject(flatObj) {
|
|
593
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
594
|
+
for (const flatKey of Object.keys(flatObj)) {
|
|
595
|
+
const keys = flatKey.split(".");
|
|
596
|
+
let current = result;
|
|
597
|
+
keys.forEach((key, index) => {
|
|
598
|
+
assertSafeKeySegment(key);
|
|
599
|
+
const isLast = index === keys.length - 1;
|
|
600
|
+
if (isLast) {
|
|
601
|
+
current[key] = flatObj[flatKey];
|
|
602
|
+
} else {
|
|
603
|
+
if (!current[key] || typeof current[key] !== "object") {
|
|
604
|
+
current[key] = /* @__PURE__ */ Object.create(null);
|
|
605
|
+
}
|
|
606
|
+
current = current[key];
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
function removeEmptyObjects(obj) {
|
|
613
|
+
if (Array.isArray(obj)) {
|
|
614
|
+
return obj;
|
|
615
|
+
}
|
|
616
|
+
if (obj !== null && typeof obj === "object") {
|
|
617
|
+
const cleaned = /* @__PURE__ */ Object.create(null);
|
|
618
|
+
for (const key of Object.keys(obj)) {
|
|
619
|
+
assertSafeKeySegment(key);
|
|
620
|
+
const value = removeEmptyObjects(obj[key]);
|
|
621
|
+
if (value !== void 0 && (typeof value !== "object" || Object.keys(value).length > 0)) {
|
|
622
|
+
cleaned[key] = value;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return cleaned;
|
|
626
|
+
}
|
|
627
|
+
return obj;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/core/key-validator.ts
|
|
631
|
+
function validateNoStructuralConflict(flatObject, newKey) {
|
|
632
|
+
const parts = newKey.split(".");
|
|
633
|
+
for (let i = 1; i < parts.length; i++) {
|
|
634
|
+
const parentPath = parts.slice(0, i).join(".");
|
|
635
|
+
if (flatObject[parentPath] !== void 0) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`Structural conflict detected:
|
|
638
|
+
|
|
639
|
+
Cannot create key "${newKey}" because "${parentPath}" is already defined as a non-object value.
|
|
640
|
+
|
|
641
|
+
Resolve conflict before proceeding.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const prefix = `${newKey}.`;
|
|
646
|
+
for (const existingKey of Object.keys(flatObject)) {
|
|
647
|
+
if (existingKey.startsWith(prefix)) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Structural conflict detected:
|
|
650
|
+
|
|
651
|
+
Cannot create key "${newKey}" because it would overwrite nested keys like "${existingKey}".
|
|
652
|
+
|
|
653
|
+
Resolve conflict before proceeding.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/commands/add-key.ts
|
|
660
|
+
async function addKeyCommand(context, key, options) {
|
|
661
|
+
const { config, fileManager } = context;
|
|
662
|
+
const { yes, dryRun, ci } = context.options;
|
|
663
|
+
const value = options.value;
|
|
664
|
+
if (!key || !value) {
|
|
665
|
+
throw new Error("Both key and --value are required.");
|
|
666
|
+
}
|
|
667
|
+
const locales = config.supportedLocales;
|
|
668
|
+
console.log(chalk4.cyan(`
|
|
669
|
+
Preparing to add key:`));
|
|
670
|
+
console.log(chalk4.yellow(` ${key}
|
|
671
|
+
`));
|
|
672
|
+
const updatedLocales = [];
|
|
673
|
+
for (const locale of locales) {
|
|
674
|
+
const nested = await fileManager.readLocale(locale);
|
|
675
|
+
const flat = flattenObject(nested);
|
|
676
|
+
validateNoStructuralConflict(flat, key);
|
|
677
|
+
if (flat[key] !== void 0) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`Key "${key}" already exists in locale "${locale}". Use update:key instead.`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
updatedLocales.push(locale);
|
|
683
|
+
}
|
|
684
|
+
console.log(chalk4.white("This operation will update:"));
|
|
685
|
+
updatedLocales.forEach(
|
|
686
|
+
(l) => console.log(chalk4.gray(` \u2022 ${l}.json`))
|
|
687
|
+
);
|
|
688
|
+
if (ci && !yes) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`CI mode: key "${key}" would be added to ${updatedLocales.length} locale(s). Re-run with --yes to apply.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
const confirmed = await confirmAction(
|
|
694
|
+
"\nDo you want to continue?",
|
|
695
|
+
{ skip: yes ?? false, ci: ci ?? false }
|
|
696
|
+
);
|
|
697
|
+
if (!confirmed) {
|
|
698
|
+
console.log(chalk4.red("\nOperation cancelled."));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
for (const locale of locales) {
|
|
702
|
+
const nested = await fileManager.readLocale(locale);
|
|
703
|
+
const flat = flattenObject(nested);
|
|
704
|
+
flat[key] = locale === config.defaultLocale ? value : "";
|
|
705
|
+
const finalData = config.keyStyle === "nested" ? unflattenObject(flat) : flat;
|
|
706
|
+
await fileManager.writeLocale(locale, finalData, { dryRun: dryRun ?? false });
|
|
707
|
+
}
|
|
708
|
+
if (dryRun) {
|
|
709
|
+
console.log(
|
|
710
|
+
chalk4.yellow("\n[DRY RUN] No files were modified.")
|
|
711
|
+
);
|
|
712
|
+
} else {
|
|
713
|
+
updatedLocales.forEach(
|
|
714
|
+
(l) => console.log(chalk4.green(`\u2714 Updated ${l}.json`))
|
|
715
|
+
);
|
|
716
|
+
console.log(
|
|
717
|
+
chalk4.green("\n\u2728 Key added successfully across all locales.")
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/commands/update-key.ts
|
|
723
|
+
import chalk5 from "chalk";
|
|
724
|
+
async function updateKeyCommand(context, key, options) {
|
|
725
|
+
const { config, fileManager, options: globalOptions } = context;
|
|
726
|
+
const { yes, dryRun, ci } = globalOptions;
|
|
727
|
+
const { value, locale } = options;
|
|
728
|
+
if (!key || value === void 0) {
|
|
729
|
+
throw new Error("Both key and --value are required.");
|
|
730
|
+
}
|
|
731
|
+
const targetLocale = locale ?? config.defaultLocale;
|
|
732
|
+
if (!config.supportedLocales.includes(targetLocale)) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Locale "${targetLocale}" is not defined in configuration.`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
console.log(chalk5.cyan(`
|
|
738
|
+
Preparing to update key:`));
|
|
739
|
+
console.log(chalk5.yellow(` ${key}`));
|
|
740
|
+
console.log(chalk5.gray(` Locale: ${targetLocale}
|
|
741
|
+
`));
|
|
742
|
+
const nested = await fileManager.readLocale(targetLocale);
|
|
743
|
+
const flat = flattenObject(nested);
|
|
744
|
+
validateNoStructuralConflict(flat, key);
|
|
745
|
+
if (flat[key] === void 0) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Key "${key}" does not exist in locale "${targetLocale}".`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
console.log(
|
|
751
|
+
chalk5.white(
|
|
752
|
+
`Old value: ${chalk5.gray(JSON.stringify(flat[key]))}`
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
console.log(
|
|
756
|
+
chalk5.white(
|
|
757
|
+
`New value: ${chalk5.green(JSON.stringify(value))}`
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
if (ci && !yes) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
`CI mode: key "${key}" in "${targetLocale}" would be updated. Re-run with --yes to apply.`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const confirmed = await confirmAction(
|
|
766
|
+
"\nDo you want to continue?",
|
|
767
|
+
{ skip: yes ?? false, ci: ci ?? false }
|
|
768
|
+
);
|
|
769
|
+
if (!confirmed) {
|
|
770
|
+
console.log(chalk5.red("\nOperation cancelled."));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
flat[key] = value;
|
|
774
|
+
const rebuilt = config.keyStyle === "nested" ? unflattenObject(flat) : flat;
|
|
775
|
+
await fileManager.writeLocale(targetLocale, rebuilt, {
|
|
776
|
+
dryRun: dryRun ?? false
|
|
777
|
+
});
|
|
778
|
+
if (dryRun) {
|
|
779
|
+
console.log(
|
|
780
|
+
chalk5.yellow("\n[DRY RUN] No files were modified.")
|
|
781
|
+
);
|
|
782
|
+
} else {
|
|
783
|
+
console.log(
|
|
784
|
+
chalk5.green(
|
|
785
|
+
`
|
|
786
|
+
\u2714 Successfully updated "${key}" in ${targetLocale}.json`
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/commands/remove-key.ts
|
|
793
|
+
import chalk6 from "chalk";
|
|
794
|
+
async function removeKeyCommand(context, key) {
|
|
795
|
+
const { config, fileManager, options } = context;
|
|
796
|
+
const { yes, dryRun, ci } = options;
|
|
797
|
+
if (!key) {
|
|
798
|
+
throw new Error("Key is required.");
|
|
799
|
+
}
|
|
800
|
+
const locales = config.supportedLocales;
|
|
801
|
+
console.log(chalk6.cyan(`
|
|
802
|
+
Preparing to remove key:`));
|
|
803
|
+
console.log(chalk6.yellow(` ${key}
|
|
804
|
+
`));
|
|
805
|
+
const localesContainingKey = [];
|
|
806
|
+
for (const locale of locales) {
|
|
807
|
+
const nested = await fileManager.readLocale(locale);
|
|
808
|
+
const flat = flattenObject(nested);
|
|
809
|
+
if (flat[key] !== void 0) {
|
|
810
|
+
localesContainingKey.push(locale);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (localesContainingKey.length === 0) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
`Key "${key}" does not exist in any locale.`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
console.log(chalk6.white("This operation will update:"));
|
|
819
|
+
localesContainingKey.forEach(
|
|
820
|
+
(l) => console.log(chalk6.gray(` \u2022 ${l}.json`))
|
|
821
|
+
);
|
|
822
|
+
if (ci && !yes) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
`CI mode: key "${key}" would be removed from ${localesContainingKey.length} locale(s). Re-run with --yes to apply.`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const confirmed = await confirmAction(
|
|
828
|
+
"\nThis will remove the key from ALL locales. Continue?",
|
|
829
|
+
yes !== void 0 ? { skip: yes, ci: ci ?? false } : { ci: ci ?? false }
|
|
830
|
+
);
|
|
831
|
+
if (!confirmed) {
|
|
832
|
+
console.log(chalk6.red("\nOperation cancelled."));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const locale of locales) {
|
|
836
|
+
const nested = await fileManager.readLocale(locale);
|
|
837
|
+
const flat = flattenObject(nested);
|
|
838
|
+
if (flat[key] !== void 0) {
|
|
839
|
+
delete flat[key];
|
|
840
|
+
}
|
|
841
|
+
const rebuilt = config.keyStyle === "nested" ? removeEmptyObjects(unflattenObject(flat)) : flat;
|
|
842
|
+
await fileManager.writeLocale(locale, rebuilt, dryRun !== void 0 ? { dryRun } : void 0);
|
|
843
|
+
}
|
|
844
|
+
if (dryRun) {
|
|
845
|
+
console.log(
|
|
846
|
+
chalk6.yellow("\n[DRY RUN] No files were modified.")
|
|
847
|
+
);
|
|
848
|
+
} else {
|
|
849
|
+
localesContainingKey.forEach(
|
|
850
|
+
(l) => console.log(chalk6.green(`\u2714 Updated ${l}.json`))
|
|
851
|
+
);
|
|
852
|
+
console.log(
|
|
853
|
+
chalk6.green("\n\u2728 Key removed successfully from all locales.")
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/commands/clean-unused.ts
|
|
859
|
+
import fs4 from "fs-extra";
|
|
860
|
+
import { glob } from "glob";
|
|
861
|
+
import chalk7 from "chalk";
|
|
862
|
+
async function cleanUnusedCommand(context) {
|
|
863
|
+
const { config, fileManager, options } = context;
|
|
864
|
+
const { dryRun, yes, ci } = options;
|
|
865
|
+
console.log(chalk7.cyan("\nScanning project for translation usage...\n"));
|
|
866
|
+
const patterns = config.compiledUsagePatterns;
|
|
867
|
+
if (!patterns || patterns.length === 0) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
"No usagePatterns defined in config."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const files = await glob("src/**/*.{ts,tsx,js,jsx,html}");
|
|
873
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
874
|
+
for (const file of files) {
|
|
875
|
+
const content = await fs4.readFile(file, "utf8");
|
|
876
|
+
for (const regex of patterns) {
|
|
877
|
+
regex.lastIndex = 0;
|
|
878
|
+
let match;
|
|
879
|
+
while (match = regex.exec(content)) {
|
|
880
|
+
const key = match.groups?.key ?? match[1];
|
|
881
|
+
if (key) {
|
|
882
|
+
usedKeys.add(key);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
console.log(
|
|
888
|
+
chalk7.gray(`Found ${usedKeys.size} used keys in project
|
|
889
|
+
`)
|
|
890
|
+
);
|
|
891
|
+
const defaultLocale = config.defaultLocale;
|
|
892
|
+
const nested = await fileManager.readLocale(defaultLocale);
|
|
893
|
+
const flat = flattenObject(nested);
|
|
894
|
+
const localeKeys = Object.keys(flat);
|
|
895
|
+
const unusedKeys = localeKeys.filter(
|
|
896
|
+
(key) => !usedKeys.has(key)
|
|
897
|
+
);
|
|
898
|
+
if (unusedKeys.length === 0) {
|
|
899
|
+
console.log(
|
|
900
|
+
chalk7.green("\u2714 No unused translation keys found.\n")
|
|
901
|
+
);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
console.log(
|
|
905
|
+
chalk7.yellow(`Unused keys (${unusedKeys.length}):`)
|
|
906
|
+
);
|
|
907
|
+
unusedKeys.slice(0, 20).forEach(
|
|
908
|
+
(key) => console.log(` - ${key}`)
|
|
909
|
+
);
|
|
910
|
+
if (unusedKeys.length > 20) {
|
|
911
|
+
console.log(
|
|
912
|
+
chalk7.gray(
|
|
913
|
+
`... and ${unusedKeys.length - 20} more`
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
console.log("");
|
|
918
|
+
if (ci && !yes) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`CI mode: ${unusedKeys.length} unused key(s) would be removed. Re-run with --yes to apply.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const confirmed = await confirmAction(
|
|
924
|
+
`This will remove ${unusedKeys.length} keys from ALL locales. Continue?`,
|
|
925
|
+
{ skip: yes ?? false, ci: ci ?? false }
|
|
926
|
+
);
|
|
927
|
+
if (!confirmed) {
|
|
928
|
+
console.log(chalk7.red("\nOperation cancelled.\n"));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const locales = config.supportedLocales;
|
|
932
|
+
for (const locale of locales) {
|
|
933
|
+
const nestedLocale = await fileManager.readLocale(locale);
|
|
934
|
+
const flatLocale = flattenObject(nestedLocale);
|
|
935
|
+
for (const key of unusedKeys) {
|
|
936
|
+
delete flatLocale[key];
|
|
937
|
+
}
|
|
938
|
+
const rebuilt = config.keyStyle === "nested" ? unflattenObject(flatLocale) : flatLocale;
|
|
939
|
+
await fileManager.writeLocale(locale, rebuilt, {
|
|
940
|
+
dryRun: dryRun ?? false
|
|
941
|
+
});
|
|
942
|
+
console.log(chalk7.green(`\u2714 Updated ${locale}.json`));
|
|
943
|
+
}
|
|
944
|
+
if (dryRun) {
|
|
945
|
+
console.log(
|
|
946
|
+
chalk7.yellow("\n[DRY RUN] No files were modified.\n")
|
|
947
|
+
);
|
|
948
|
+
} else {
|
|
949
|
+
console.log(
|
|
950
|
+
chalk7.green(
|
|
951
|
+
`
|
|
952
|
+
\u2728 Removed ${unusedKeys.length} unused keys from all locales.
|
|
953
|
+
`
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/bin/cli.ts
|
|
960
|
+
var program = new Command();
|
|
961
|
+
program.name("i18n-ai-cli").description("Professional CLI tool for managing translation files").version("1.0.0");
|
|
962
|
+
function withGlobalOptions(command) {
|
|
963
|
+
return command.option("-y, --yes", "Skip confirmation prompts").option("--dry-run", "Preview changes without writing files").option("--ci", "Run in CI mode (no prompts, exit on issues)").option("-f, --force", "Force operation even if validation fails");
|
|
964
|
+
}
|
|
965
|
+
withGlobalOptions(
|
|
966
|
+
program.command("init").description("Create an i18n-cli configuration file").action(async (options) => {
|
|
967
|
+
await initCommand(options);
|
|
968
|
+
})
|
|
969
|
+
);
|
|
970
|
+
withGlobalOptions(
|
|
971
|
+
program.command("add:lang <lang>").option("--from <locale>", "Clone from existing locale").option("--strict", "Enable strict mode").description("Add new language locale").action(async (lang, options) => {
|
|
972
|
+
const context = await buildContext(options);
|
|
973
|
+
await addLang(lang, options, context);
|
|
974
|
+
})
|
|
975
|
+
);
|
|
976
|
+
withGlobalOptions(
|
|
977
|
+
program.command("remove:lang").argument("<lang>", "Language code to remove").description("Remove a language translation file").action(async (lang, options) => {
|
|
978
|
+
const context = await buildContext(options);
|
|
979
|
+
await removeLangCommand(context, lang);
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
withGlobalOptions(
|
|
983
|
+
program.command("add:key").argument("<key>", "Translation key (e.g., auth.login.title)").requiredOption("-v, --value <value>", "Value for default locale").description("Add new translation key to all locales").action(async (key, options) => {
|
|
984
|
+
const context = await buildContext(options);
|
|
985
|
+
await addKeyCommand(context, key, options);
|
|
986
|
+
})
|
|
987
|
+
);
|
|
988
|
+
withGlobalOptions(
|
|
989
|
+
program.command("update:key").argument("<key>", "Translation key").requiredOption("-v, --value <value>", "New value").option("-l, --locale <locale>", "Specific locale to update").description("Update translation key").action(async (key, options) => {
|
|
990
|
+
const context = await buildContext(options);
|
|
991
|
+
await updateKeyCommand(context, key, options);
|
|
992
|
+
})
|
|
993
|
+
);
|
|
994
|
+
withGlobalOptions(
|
|
995
|
+
program.command("remove:key").argument("<key>", "Translation key to remove").description("Remove translation key from all locales").action(async (key, options) => {
|
|
996
|
+
const context = await buildContext(options);
|
|
997
|
+
await removeKeyCommand(context, key);
|
|
998
|
+
})
|
|
999
|
+
);
|
|
1000
|
+
withGlobalOptions(
|
|
1001
|
+
program.command("clean:unused").description("Remove unused translation keys from all locales").action(async (options) => {
|
|
1002
|
+
const context = await buildContext(options);
|
|
1003
|
+
await cleanUnusedCommand(context);
|
|
1004
|
+
})
|
|
1005
|
+
);
|
|
1006
|
+
program.exitOverride();
|
|
1007
|
+
try {
|
|
1008
|
+
await program.parseAsync(process.argv);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
console.error(chalk8.red("\u274C Error:"), err.message);
|
|
1011
|
+
process.exitCode = 1;
|
|
1012
|
+
}
|
|
1013
|
+
//# sourceMappingURL=cli.js.map
|