ai-localize-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/dist/cli.js +521 -0
- package/package.json +41 -0
- package/src/cli.ts +33 -0
- package/src/commands/cleanup.ts +56 -0
- package/src/commands/extract.ts +55 -0
- package/src/commands/full-migrate.ts +77 -0
- package/src/commands/init.ts +33 -0
- package/src/commands/migrate-cdn.ts +57 -0
- package/src/commands/replace-cdn.ts +55 -0
- package/src/commands/report.ts +56 -0
- package/src/commands/scan.ts +52 -0
- package/src/commands/upload-assets.ts +50 -0
- package/src/commands/validate.ts +56 -0
- package/src/utils/logger.ts +11 -0
- package/src/utils/spinner.ts +5 -0
- package/tsconfig.json +6 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/cli.ts
|
|
26
|
+
var import_commander11 = require("commander");
|
|
27
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
28
|
+
var import_shared5 = require("@ai-localize/shared");
|
|
29
|
+
|
|
30
|
+
// src/commands/init.ts
|
|
31
|
+
var import_commander = require("commander");
|
|
32
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
33
|
+
|
|
34
|
+
// src/utils/logger.ts
|
|
35
|
+
var import_chalk = __toESM(require("chalk"));
|
|
36
|
+
var logger = {
|
|
37
|
+
info: (msg) => console.log(import_chalk.default.blue("i"), msg),
|
|
38
|
+
success: (msg) => console.log(import_chalk.default.green("v"), msg),
|
|
39
|
+
warn: (msg) => console.log(import_chalk.default.yellow("!"), msg),
|
|
40
|
+
error: (msg) => console.error(import_chalk.default.red("x"), msg),
|
|
41
|
+
step: (msg) => console.log(import_chalk.default.cyan(">"), msg),
|
|
42
|
+
dim: (msg) => console.log(import_chalk.default.dim(msg)),
|
|
43
|
+
header: (msg) => console.log("\n" + import_chalk.default.bold.cyan(msg) + "\n")
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/utils/spinner.ts
|
|
47
|
+
var import_ora = __toESM(require("ora"));
|
|
48
|
+
function createSpinner(text) {
|
|
49
|
+
return (0, import_ora.default)({ text, color: "cyan" });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/commands/init.ts
|
|
53
|
+
var import_config = require("@ai-localize/config");
|
|
54
|
+
var import_framework_detectors = require("@ai-localize/framework-detectors");
|
|
55
|
+
function initCommand() {
|
|
56
|
+
return new import_commander.Command("init").description("Initialize ai-localize configuration in the current project").option("-f, --framework <type>", "Override framework detection").option("--cwd <path>", "Working directory", process.cwd()).action(async (opts) => {
|
|
57
|
+
logger.header("ai-localize init");
|
|
58
|
+
const spinner = createSpinner("Detecting framework...").start();
|
|
59
|
+
try {
|
|
60
|
+
const cwd = opts.cwd;
|
|
61
|
+
let framework = opts.framework;
|
|
62
|
+
if (!framework) {
|
|
63
|
+
framework = (0, import_framework_detectors.detectFramework)(cwd);
|
|
64
|
+
spinner.succeed("Detected framework: " + import_chalk2.default.cyan(framework));
|
|
65
|
+
} else {
|
|
66
|
+
spinner.succeed("Using framework: " + import_chalk2.default.cyan(framework));
|
|
67
|
+
}
|
|
68
|
+
const configPath = (0, import_config.writeDefaultConfig)(cwd, framework);
|
|
69
|
+
logger.success("Config created: " + import_chalk2.default.cyan(configPath));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
spinner.fail("Initialization failed");
|
|
72
|
+
logger.error(err.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/commands/scan.ts
|
|
79
|
+
var import_commander2 = require("commander");
|
|
80
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
81
|
+
var path = __toESM(require("path"));
|
|
82
|
+
var import_config2 = require("@ai-localize/config");
|
|
83
|
+
var import_scanner = require("@ai-localize/scanner");
|
|
84
|
+
var import_shared = require("@ai-localize/shared");
|
|
85
|
+
function scanCommand() {
|
|
86
|
+
return new import_commander2.Command("scan").description("Scan project for hardcoded texts and asset references").option("--incremental", "Scan only changed files based on git diff").option("--staged", "Scan only git staged files").option("--cwd <path>", "Working directory", process.cwd()).option("--output <path>", "Output JSON file path").action(async (opts) => {
|
|
87
|
+
logger.header("ai-localize scan");
|
|
88
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
89
|
+
try {
|
|
90
|
+
const cwd = opts.cwd;
|
|
91
|
+
const { config } = await (0, import_config2.loadConfig)(cwd);
|
|
92
|
+
spinner.succeed("Configuration loaded");
|
|
93
|
+
let files;
|
|
94
|
+
if (opts.staged || opts.incremental) {
|
|
95
|
+
const git = new import_scanner.GitScanner(cwd);
|
|
96
|
+
files = opts.staged ? git.getStagedFiles() : git.getChangedFiles();
|
|
97
|
+
logger.info("Changed files: " + import_chalk3.default.cyan(String(files.length)));
|
|
98
|
+
}
|
|
99
|
+
const scanSpinner = createSpinner("Scanning files...").start();
|
|
100
|
+
const scanner = new import_scanner.ProjectScanner(config);
|
|
101
|
+
const result = await scanner.scan({ files });
|
|
102
|
+
scanSpinner.succeed("Scanned " + import_chalk3.default.cyan(String(result.scannedFiles)) + " files in " + import_chalk3.default.cyan(result.duration + "ms"));
|
|
103
|
+
logger.info("Hardcoded texts: " + import_chalk3.default.yellow(String(result.detectedTexts.length)));
|
|
104
|
+
logger.info("Asset references: " + import_chalk3.default.blue(String(result.assets.length)));
|
|
105
|
+
logger.info("Legacy CDN URLs: " + import_chalk3.default.red(String(result.legacyCdnUrls.length)));
|
|
106
|
+
if (opts.output) {
|
|
107
|
+
const outPath = path.resolve(cwd, opts.output);
|
|
108
|
+
(0, import_shared.writeJson)(outPath, result);
|
|
109
|
+
logger.success("Results saved to " + import_chalk3.default.cyan(outPath));
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
spinner.fail("Scan failed");
|
|
113
|
+
logger.error(err.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/commands/extract.ts
|
|
120
|
+
var import_commander3 = require("commander");
|
|
121
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
122
|
+
var path2 = __toESM(require("path"));
|
|
123
|
+
var import_config3 = require("@ai-localize/config");
|
|
124
|
+
var import_scanner2 = require("@ai-localize/scanner");
|
|
125
|
+
var import_locale_engine = require("@ai-localize/locale-engine");
|
|
126
|
+
function extractCommand() {
|
|
127
|
+
return new import_commander3.Command("extract").description("Extract hardcoded text to locale JSON files").option("--cwd <path>", "Working directory", process.cwd()).option("--dry-run", "Preview changes without modifying files").option("--no-merge", "Overwrite existing keys").action(async (opts) => {
|
|
128
|
+
logger.header("ai-localize extract");
|
|
129
|
+
const cwd = opts.cwd;
|
|
130
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
131
|
+
try {
|
|
132
|
+
const { config } = await (0, import_config3.loadConfig)(cwd);
|
|
133
|
+
spinner.succeed("Configuration loaded");
|
|
134
|
+
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
135
|
+
const scanner = new import_scanner2.ProjectScanner(config);
|
|
136
|
+
const scanResult = await scanner.scan();
|
|
137
|
+
ss.succeed("Found " + import_chalk4.default.cyan(String(scanResult.detectedTexts.length)) + " texts in " + scanResult.scannedFiles + " files");
|
|
138
|
+
const uniqueTexts = (0, import_locale_engine.deduplicateTexts)(scanResult.detectedTexts);
|
|
139
|
+
logger.info("Unique texts: " + import_chalk4.default.cyan(String(uniqueTexts.length)));
|
|
140
|
+
const extractor = new import_locale_engine.LocaleExtractor({
|
|
141
|
+
defaultLanguage: config.defaultLanguage,
|
|
142
|
+
targetLanguages: config.targetLanguages,
|
|
143
|
+
namespaceSplitting: true
|
|
144
|
+
});
|
|
145
|
+
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
146
|
+
logger.info("Keys generated: " + import_chalk4.default.green(String(keyCount)));
|
|
147
|
+
logger.info("Namespaces: " + import_chalk4.default.cyan(namespaces.join(", ")));
|
|
148
|
+
if (!opts.dryRun) {
|
|
149
|
+
const localesDir = path2.resolve(cwd, config.localesDir);
|
|
150
|
+
const writer = new import_locale_engine.LocaleWriter({ localesDir, merge: opts.merge !== false });
|
|
151
|
+
const { written, created, merged } = writer.write(localeFiles);
|
|
152
|
+
logger.success("Wrote " + written.length + " locale files (" + created.length + " new, " + merged.length + " merged)");
|
|
153
|
+
} else {
|
|
154
|
+
logger.info("Dry run - no files written");
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
spinner.fail("Extraction failed");
|
|
158
|
+
logger.error(String(err));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/commands/validate.ts
|
|
165
|
+
var import_commander4 = require("commander");
|
|
166
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
167
|
+
var path3 = __toESM(require("path"));
|
|
168
|
+
var import_config4 = require("@ai-localize/config");
|
|
169
|
+
var import_validators = require("@ai-localize/validators");
|
|
170
|
+
function validateCommand() {
|
|
171
|
+
return new import_commander4.Command("validate").description("Validate locale files for missing/duplicate/unused keys").option("--cwd <path>", "Working directory", process.cwd()).option("--no-unused", "Skip unused key check").option("--no-duplicates", "Skip duplicate key check").option("--no-placeholders", "Skip placeholder check").option("--fail-on-warning", "Exit with error if warnings exist").action(async (opts) => {
|
|
172
|
+
logger.header("ai-localize validate");
|
|
173
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
174
|
+
try {
|
|
175
|
+
const cwd = opts.cwd;
|
|
176
|
+
const { config } = await (0, import_config4.loadConfig)(cwd);
|
|
177
|
+
spinner.succeed("Configuration loaded");
|
|
178
|
+
const vs = createSpinner("Validating locale files...").start();
|
|
179
|
+
const validator = new import_validators.LocaleValidator({
|
|
180
|
+
localesDir: path3.resolve(cwd, config.localesDir),
|
|
181
|
+
sourceDir: path3.resolve(cwd, config.sourceDir),
|
|
182
|
+
defaultLanguage: config.defaultLanguage,
|
|
183
|
+
targetLanguages: config.targetLanguages,
|
|
184
|
+
checkUnused: opts.unused !== false,
|
|
185
|
+
checkDuplicates: opts.duplicates !== false,
|
|
186
|
+
checkPlaceholders: opts.placeholders !== false
|
|
187
|
+
});
|
|
188
|
+
const result = validator.validate();
|
|
189
|
+
if (result.valid) {
|
|
190
|
+
vs.succeed(import_chalk5.default.green("Locale files are valid!"));
|
|
191
|
+
} else {
|
|
192
|
+
vs.fail(import_chalk5.default.red(`Validation failed with ${result.errors.length} errors and ${result.warnings.length} warnings.`));
|
|
193
|
+
}
|
|
194
|
+
result.errors.forEach((err) => logger.error(`[${err.type}] ${err.message}`));
|
|
195
|
+
result.warnings.forEach((warn) => logger.warn(`[${warn.type}] ${warn.message}`));
|
|
196
|
+
if (!result.valid || opts.failOnWarning && result.warnings.length > 0) {
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
spinner.fail("Validation failed");
|
|
201
|
+
logger.error(String(err));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/commands/cleanup.ts
|
|
208
|
+
var import_commander5 = require("commander");
|
|
209
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
210
|
+
var path4 = __toESM(require("path"));
|
|
211
|
+
var fs = __toESM(require("fs"));
|
|
212
|
+
var import_config5 = require("@ai-localize/config");
|
|
213
|
+
var import_validators2 = require("@ai-localize/validators");
|
|
214
|
+
var import_shared2 = require("@ai-localize/shared");
|
|
215
|
+
function cleanupCommand() {
|
|
216
|
+
return new import_commander5.Command("cleanup").description("Remove unused locale keys from translation files").option("--cwd <path>", "Working directory", process.cwd()).option("--dry-run", "Preview changes without modifying files").action(async (opts) => {
|
|
217
|
+
logger.header("ai-localize cleanup");
|
|
218
|
+
const spinner = createSpinner("Validating keys...").start();
|
|
219
|
+
try {
|
|
220
|
+
const cwd = opts.cwd;
|
|
221
|
+
const { config } = await (0, import_config5.loadConfig)(cwd);
|
|
222
|
+
const localesDir = path4.resolve(cwd, config.localesDir);
|
|
223
|
+
const sourceDir = path4.resolve(cwd, config.sourceDir);
|
|
224
|
+
const validator = new import_validators2.UnusedKeyValidator(localesDir, sourceDir, config.defaultLanguage);
|
|
225
|
+
const { unusedKeys } = validator.validate();
|
|
226
|
+
spinner.succeed("Found " + import_chalk6.default.yellow(String(unusedKeys.length)) + " unused keys");
|
|
227
|
+
if (unusedKeys.length === 0) {
|
|
228
|
+
logger.success("No unused keys found!");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
unusedKeys.slice(0, 20).forEach((k) => logger.dim("- " + k));
|
|
232
|
+
if (!opts.dryRun) {
|
|
233
|
+
const defaultDir = path4.join(localesDir, config.defaultLanguage);
|
|
234
|
+
const nsFiles = fs.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
|
|
235
|
+
let removed = 0;
|
|
236
|
+
for (const nsFile of nsFiles) {
|
|
237
|
+
const ns = nsFile.replace(".json", "");
|
|
238
|
+
const fp = path4.join(defaultDir, nsFile);
|
|
239
|
+
const entries = (0, import_shared2.readJsonSafe)(fp) || {};
|
|
240
|
+
for (const key of unusedKeys) {
|
|
241
|
+
const lk = key.startsWith(ns + ".") ? key.slice(ns.length + 1) : null;
|
|
242
|
+
if (lk && lk in entries) {
|
|
243
|
+
delete entries[lk];
|
|
244
|
+
removed++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
(0, import_shared2.writeJson)(fp, entries);
|
|
248
|
+
}
|
|
249
|
+
logger.success("Removed " + removed + " unused keys");
|
|
250
|
+
} else {
|
|
251
|
+
logger.info("Dry run - " + unusedKeys.length + " keys would be removed");
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
spinner.fail("Cleanup failed");
|
|
255
|
+
logger.error(String(err));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/commands/migrate-cdn.ts
|
|
262
|
+
var import_commander6 = require("commander");
|
|
263
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
264
|
+
var path5 = __toESM(require("path"));
|
|
265
|
+
var import_config6 = require("@ai-localize/config");
|
|
266
|
+
var import_scanner3 = require("@ai-localize/scanner");
|
|
267
|
+
var import_aws_cloudfront = require("@ai-localize/aws-cloudfront");
|
|
268
|
+
function migrateCdnCommand() {
|
|
269
|
+
return new import_commander6.Command("migrate-cdn").description("Migrate legacy CDN URLs to CloudFront").option("--cwd <path>", "Working directory", process.cwd()).option("--assets-dir <path>", "Local assets directory to upload").option("--dry-run", "Preview changes without executing").option("--invalidate", "Invalidate CloudFront cache after upload").action(async (opts) => {
|
|
270
|
+
logger.header("ai-localize migrate-cdn");
|
|
271
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
272
|
+
try {
|
|
273
|
+
const cwd = opts.cwd;
|
|
274
|
+
const { config } = await (0, import_config6.loadConfig)(cwd);
|
|
275
|
+
if (!config.aws?.bucket) {
|
|
276
|
+
spinner.fail("AWS configuration missing");
|
|
277
|
+
logger.error("Set aws.bucket and aws.distributionId in ai-localize.config.json");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
spinner.succeed("Configuration loaded");
|
|
281
|
+
const ss = createSpinner("Scanning for legacy CDN URLs...").start();
|
|
282
|
+
const scanner = new import_scanner3.ProjectScanner(config);
|
|
283
|
+
const scanResult = await scanner.scan();
|
|
284
|
+
ss.succeed("Found " + import_chalk7.default.yellow(String(scanResult.legacyCdnUrls.length)) + " legacy CDN URLs");
|
|
285
|
+
if (scanResult.legacyCdnUrls.length === 0) {
|
|
286
|
+
logger.success("No legacy CDN URLs found!");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const migrator = new import_aws_cloudfront.CdnMigrator(config.aws);
|
|
290
|
+
const result = await migrator.migrate({
|
|
291
|
+
sourceDir: path5.resolve(cwd, config.sourceDir),
|
|
292
|
+
assetsDir: path5.resolve(cwd, opts.assetsDir),
|
|
293
|
+
legacyCdnUrls: scanResult.legacyCdnUrls,
|
|
294
|
+
dryRun: opts.dryRun,
|
|
295
|
+
invalidateCache: opts.invalidate,
|
|
296
|
+
onProgress: (step) => logger.step(step)
|
|
297
|
+
});
|
|
298
|
+
logger.success("Migration complete in " + result.duration + "ms");
|
|
299
|
+
logger.info(" Assets uploaded: " + import_chalk7.default.green(String(result.uploadedAssets.length)));
|
|
300
|
+
logger.info(" URLs replaced: " + import_chalk7.default.green(String(result.replacedUrls)));
|
|
301
|
+
if (result.invalidationId) logger.info(" Invalidation ID: " + import_chalk7.default.cyan(result.invalidationId));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
spinner.fail("CDN migration failed");
|
|
304
|
+
logger.error(String(err));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/commands/upload-assets.ts
|
|
311
|
+
var import_commander7 = require("commander");
|
|
312
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
313
|
+
var path6 = __toESM(require("path"));
|
|
314
|
+
var import_config7 = require("@ai-localize/config");
|
|
315
|
+
var import_aws_cloudfront2 = require("@ai-localize/aws-cloudfront");
|
|
316
|
+
var import_shared3 = require("@ai-localize/shared");
|
|
317
|
+
function uploadAssetsCommand() {
|
|
318
|
+
return new import_commander7.Command("upload-assets").description("Upload local assets to AWS S3").option("--cwd <path>", "Working directory", process.cwd()).option("--assets-dir <path>", "Directory containing assets to upload").option("--force", "Force upload even if file already exists with same hash").option("--output <path>", "Output manifest JSON path").action(async (opts) => {
|
|
319
|
+
logger.header("ai-localize upload-assets");
|
|
320
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
321
|
+
try {
|
|
322
|
+
const cwd = opts.cwd;
|
|
323
|
+
const { config } = await (0, import_config7.loadConfig)(cwd);
|
|
324
|
+
if (!config.aws?.bucket) {
|
|
325
|
+
spinner.fail("AWS configuration missing");
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
spinner.succeed("Configuration loaded");
|
|
329
|
+
const uploader = new import_aws_cloudfront2.S3Uploader(config.aws);
|
|
330
|
+
const assetsDir = path6.resolve(cwd, opts.assetsDir);
|
|
331
|
+
const uploadSpinner = createSpinner("Uploading assets...").start();
|
|
332
|
+
const result = await uploader.uploadDirectory({
|
|
333
|
+
assetsDir,
|
|
334
|
+
force: opts.force,
|
|
335
|
+
onProgress: (_a, done, total) => {
|
|
336
|
+
uploadSpinner.text = "Uploading " + done + "/" + total;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
uploadSpinner.succeed("Uploaded " + import_chalk8.default.green(String(result.uploaded.length)) + " (" + result.skipped.length + " skipped)");
|
|
340
|
+
if (opts.output) {
|
|
341
|
+
const outPath = path6.resolve(cwd, opts.output);
|
|
342
|
+
(0, import_shared3.writeJson)(outPath, [...result.uploaded, ...result.skipped]);
|
|
343
|
+
logger.success("Manifest saved to " + import_chalk8.default.cyan(outPath));
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
spinner.fail("Upload failed");
|
|
347
|
+
logger.error(String(err));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/replace-cdn.ts
|
|
354
|
+
var import_commander8 = require("commander");
|
|
355
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
356
|
+
var path7 = __toESM(require("path"));
|
|
357
|
+
var import_config8 = require("@ai-localize/config");
|
|
358
|
+
var import_scanner4 = require("@ai-localize/scanner");
|
|
359
|
+
var import_shared4 = require("@ai-localize/shared");
|
|
360
|
+
var import_codemods = require("@ai-localize/codemods");
|
|
361
|
+
function replaceCdnCommand() {
|
|
362
|
+
return new import_commander8.Command("replace-cdn").description("Replace legacy CDN URLs with CloudFront URLs in source files").option("--cwd <path>", "Working directory", process.cwd()).option("--manifest <path>", "Path to upload manifest JSON").option("--dry-run", "Preview changes without executing").action(async (opts) => {
|
|
363
|
+
logger.header("ai-localize replace-cdn");
|
|
364
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
365
|
+
try {
|
|
366
|
+
const cwd = opts.cwd;
|
|
367
|
+
const { config } = await (0, import_config8.loadConfig)(cwd);
|
|
368
|
+
spinner.succeed("Configuration loaded");
|
|
369
|
+
let assets = [];
|
|
370
|
+
if (opts.manifest) {
|
|
371
|
+
assets = (0, import_shared4.readJsonSafe)(path7.resolve(cwd, opts.manifest)) || [];
|
|
372
|
+
}
|
|
373
|
+
const ss = createSpinner("Scanning for legacy CDN URLs...").start();
|
|
374
|
+
const scanner = new import_scanner4.ProjectScanner(config);
|
|
375
|
+
const scanResult = await scanner.scan();
|
|
376
|
+
ss.succeed("Found " + import_chalk9.default.yellow(String(scanResult.legacyCdnUrls.length)) + " legacy CDN URLs");
|
|
377
|
+
if (scanResult.legacyCdnUrls.length === 0) {
|
|
378
|
+
logger.success("No legacy CDN URLs found!");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!opts.dryRun) {
|
|
382
|
+
const rs = createSpinner("Replacing URLs...").start();
|
|
383
|
+
const replacedCount = await (0, import_codemods.batchReplaceCdnUrls)(
|
|
384
|
+
path7.resolve(cwd, config.sourceDir),
|
|
385
|
+
scanResult.legacyCdnUrls,
|
|
386
|
+
assets
|
|
387
|
+
);
|
|
388
|
+
rs.succeed("Replaced " + import_chalk9.default.green(String(replacedCount)) + " URLs");
|
|
389
|
+
} else {
|
|
390
|
+
logger.info("Dry run - no URLs replaced");
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
spinner.fail("CDN replacement failed");
|
|
394
|
+
logger.error(String(err));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/commands/report.ts
|
|
401
|
+
var import_commander9 = require("commander");
|
|
402
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
403
|
+
var path8 = __toESM(require("path"));
|
|
404
|
+
var import_config9 = require("@ai-localize/config");
|
|
405
|
+
var import_scanner5 = require("@ai-localize/scanner");
|
|
406
|
+
var import_validators3 = require("@ai-localize/validators");
|
|
407
|
+
var import_reporting = require("@ai-localize/reporting");
|
|
408
|
+
function reportCommand() {
|
|
409
|
+
return new import_commander9.Command("report").description("Generate JSON and HTML localization reports").option("--cwd <path>", "Working directory", process.cwd()).option("--output-dir <path>", "Output directory for reports", ".ai-localize-reports").option("--no-html", "Skip HTML report generation").action(async (opts) => {
|
|
410
|
+
logger.header("ai-localize report");
|
|
411
|
+
const spinner = createSpinner("Loading configuration...").start();
|
|
412
|
+
try {
|
|
413
|
+
const cwd = opts.cwd;
|
|
414
|
+
const { config } = await (0, import_config9.loadConfig)(cwd);
|
|
415
|
+
spinner.succeed("Configuration loaded");
|
|
416
|
+
const ss = createSpinner("Scanning project...").start();
|
|
417
|
+
const scanner = new import_scanner5.ProjectScanner(config);
|
|
418
|
+
const scanResult = await scanner.scan();
|
|
419
|
+
ss.succeed("Scanned " + scanResult.scannedFiles + " files");
|
|
420
|
+
const vs = createSpinner("Validating locale files...").start();
|
|
421
|
+
const validator = new import_validators3.LocaleValidator({
|
|
422
|
+
localesDir: path8.resolve(cwd, config.localesDir),
|
|
423
|
+
sourceDir: path8.resolve(cwd, config.sourceDir),
|
|
424
|
+
defaultLanguage: config.defaultLanguage,
|
|
425
|
+
targetLanguages: config.targetLanguages
|
|
426
|
+
});
|
|
427
|
+
const validationResult = validator.validate();
|
|
428
|
+
vs.succeed("Validation complete");
|
|
429
|
+
const outDir = path8.resolve(cwd, opts.outputDir);
|
|
430
|
+
const rs = createSpinner("Building report...").start();
|
|
431
|
+
const report = (0, import_reporting.buildReport)({ scanResult, validationResult });
|
|
432
|
+
if (opts.html !== false) {
|
|
433
|
+
(0, import_reporting.generateHtmlReport)(report, outDir);
|
|
434
|
+
}
|
|
435
|
+
rs.succeed("Report generated at " + import_chalk10.default.cyan(outDir));
|
|
436
|
+
(0, import_reporting.printCliSummary)(report);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
spinner.fail("Report generation failed");
|
|
439
|
+
logger.error(String(err));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/commands/full-migrate.ts
|
|
446
|
+
var import_commander10 = require("commander");
|
|
447
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
448
|
+
var path9 = __toESM(require("path"));
|
|
449
|
+
var import_config10 = require("@ai-localize/config");
|
|
450
|
+
var import_scanner6 = require("@ai-localize/scanner");
|
|
451
|
+
var import_locale_engine2 = require("@ai-localize/locale-engine");
|
|
452
|
+
var import_codemods2 = require("@ai-localize/codemods");
|
|
453
|
+
var import_validators4 = require("@ai-localize/validators");
|
|
454
|
+
var import_reporting2 = require("@ai-localize/reporting");
|
|
455
|
+
function fullMigrateCommand() {
|
|
456
|
+
return new import_commander10.Command("full-migrate").description("Run full localization pipeline: scan -> extract -> codemod -> validate -> report").option("--cwd <path>", "Working directory", process.cwd()).option("--dry-run", "Preview changes without modifying files").option("--no-codemods", "Skip codemod phase").option("--no-report", "Skip report generation").action(async (opts) => {
|
|
457
|
+
logger.header("ai-localize full-migrate");
|
|
458
|
+
const cwd = opts.cwd;
|
|
459
|
+
const dryRun = opts.dryRun;
|
|
460
|
+
try {
|
|
461
|
+
const cs = createSpinner("Loading configuration...").start();
|
|
462
|
+
const { config } = await (0, import_config10.loadConfig)(cwd);
|
|
463
|
+
cs.succeed("Configuration loaded");
|
|
464
|
+
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
465
|
+
const scanner = new import_scanner6.ProjectScanner(config);
|
|
466
|
+
const scanResult = await scanner.scan();
|
|
467
|
+
ss.succeed("Found " + import_chalk11.default.cyan(String(scanResult.detectedTexts.length)) + " texts in " + scanResult.scannedFiles + " files");
|
|
468
|
+
const es = createSpinner("Extracting locale keys...").start();
|
|
469
|
+
const uniqueTexts = (0, import_locale_engine2.deduplicateTexts)(scanResult.detectedTexts);
|
|
470
|
+
const extractor = new import_locale_engine2.LocaleExtractor({
|
|
471
|
+
defaultLanguage: config.defaultLanguage,
|
|
472
|
+
targetLanguages: config.targetLanguages,
|
|
473
|
+
namespaceSplitting: true
|
|
474
|
+
});
|
|
475
|
+
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
476
|
+
es.succeed("Generated " + import_chalk11.default.green(String(keyCount)) + " locale keys");
|
|
477
|
+
if (!dryRun) {
|
|
478
|
+
const writer = new import_locale_engine2.LocaleWriter({ localesDir: path9.resolve(cwd, config.localesDir), merge: true });
|
|
479
|
+
writer.write(localeFiles);
|
|
480
|
+
}
|
|
481
|
+
if (opts.codemods !== false) {
|
|
482
|
+
const ms = createSpinner("Applying i18n codemods...").start();
|
|
483
|
+
const runner = new import_codemods2.CodemodRunner(config);
|
|
484
|
+
const codemodResult = await runner.run(uniqueTexts, { dryRun });
|
|
485
|
+
ms.succeed("Codemods: " + import_chalk11.default.green(String(codemodResult.totalReplacements)) + " replacements in " + codemodResult.changedFiles + " files");
|
|
486
|
+
}
|
|
487
|
+
const vs = createSpinner("Validating locale files...").start();
|
|
488
|
+
const validator = new import_validators4.LocaleValidator({
|
|
489
|
+
localesDir: path9.resolve(cwd, config.localesDir),
|
|
490
|
+
sourceDir: path9.resolve(cwd, config.sourceDir),
|
|
491
|
+
defaultLanguage: config.defaultLanguage,
|
|
492
|
+
targetLanguages: config.targetLanguages
|
|
493
|
+
});
|
|
494
|
+
const validationResult = validator.validate();
|
|
495
|
+
vs.succeed(validationResult.valid ? import_chalk11.default.green("Locale files valid!") : import_chalk11.default.yellow(validationResult.errors.length + " errors, " + validationResult.warnings.length + " warnings"));
|
|
496
|
+
if (opts.report !== false) {
|
|
497
|
+
const report = (0, import_reporting2.buildReport)({ scanResult, validationResult });
|
|
498
|
+
(0, import_reporting2.printCliSummary)(report);
|
|
499
|
+
}
|
|
500
|
+
logger.success("Full migration complete!");
|
|
501
|
+
} catch (err) {
|
|
502
|
+
logger.error("Migration failed: " + String(err));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/cli.ts
|
|
509
|
+
var program = new import_commander11.Command();
|
|
510
|
+
program.name("ai-localize").description(import_chalk12.default.cyan("ai-localize-core") + " \u2014 Deterministic localization + CloudFront migration platform").version(import_shared5.PLATFORM_VERSION, "-v, --version");
|
|
511
|
+
program.addCommand(initCommand());
|
|
512
|
+
program.addCommand(scanCommand());
|
|
513
|
+
program.addCommand(extractCommand());
|
|
514
|
+
program.addCommand(validateCommand());
|
|
515
|
+
program.addCommand(cleanupCommand());
|
|
516
|
+
program.addCommand(migrateCdnCommand());
|
|
517
|
+
program.addCommand(uploadAssetsCommand());
|
|
518
|
+
program.addCommand(replaceCdnCommand());
|
|
519
|
+
program.addCommand(reportCommand());
|
|
520
|
+
program.addCommand(fullMigrateCommand());
|
|
521
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-localize-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for ai-localize-core: scan, extract, validate, migrate CDN",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ai-localize": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/cli.js",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"commander": "^12.0.0",
|
|
11
|
+
"chalk": "^5.3.0",
|
|
12
|
+
"ora": "^8.0.1",
|
|
13
|
+
"inquirer": "^9.2.12",
|
|
14
|
+
"ai-localize-codemods": "1.0.0",
|
|
15
|
+
"ai-localize-shared": "1.0.0",
|
|
16
|
+
"ai-localize-config": "1.0.0",
|
|
17
|
+
"ai-localize-framework-detectors": "1.0.0",
|
|
18
|
+
"ai-localize-scanner": "1.0.0",
|
|
19
|
+
"ai-localize-aws-cloudfront": "1.0.0",
|
|
20
|
+
"ai-localize-reporting": "1.0.0",
|
|
21
|
+
"ai-localize-validators": "1.0.0",
|
|
22
|
+
"ai-localize-locale-engine": "1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/inquirer": "^9.0.7",
|
|
26
|
+
"tsup": "^8.0.1",
|
|
27
|
+
"typescript": "^5.3.3",
|
|
28
|
+
"vitest": "^1.2.1"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/cli.ts --format cjs --env.NODE_ENV production",
|
|
36
|
+
"dev": "tsup src/cli.ts --format cjs --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"lint": "eslint src --ext .ts"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { PLATFORM_VERSION } from '@ai-localize/shared';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { scanCommand } from './commands/scan.js';
|
|
6
|
+
import { extractCommand } from './commands/extract.js';
|
|
7
|
+
import { validateCommand } from './commands/validate.js';
|
|
8
|
+
import { cleanupCommand } from './commands/cleanup.js';
|
|
9
|
+
import { migrateCdnCommand } from './commands/migrate-cdn.js';
|
|
10
|
+
import { uploadAssetsCommand } from './commands/upload-assets.js';
|
|
11
|
+
import { replaceCdnCommand } from './commands/replace-cdn.js';
|
|
12
|
+
import { reportCommand } from './commands/report.js';
|
|
13
|
+
import { fullMigrateCommand } from './commands/full-migrate.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('ai-localize')
|
|
19
|
+
.description(chalk.cyan('ai-localize-core') + ' — Deterministic localization + CloudFront migration platform')
|
|
20
|
+
.version(PLATFORM_VERSION, '-v, --version');
|
|
21
|
+
|
|
22
|
+
program.addCommand(initCommand());
|
|
23
|
+
program.addCommand(scanCommand());
|
|
24
|
+
program.addCommand(extractCommand());
|
|
25
|
+
program.addCommand(validateCommand());
|
|
26
|
+
program.addCommand(cleanupCommand());
|
|
27
|
+
program.addCommand(migrateCdnCommand());
|
|
28
|
+
program.addCommand(uploadAssetsCommand());
|
|
29
|
+
program.addCommand(replaceCdnCommand());
|
|
30
|
+
program.addCommand(reportCommand());
|
|
31
|
+
program.addCommand(fullMigrateCommand());
|
|
32
|
+
|
|
33
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
7
|
+
import { loadConfig } from '@ai-localize/config';
|
|
8
|
+
import { UnusedKeyValidator } from '@ai-localize/validators';
|
|
9
|
+
import { readJsonSafe, writeJson } from '@ai-localize/shared';
|
|
10
|
+
|
|
11
|
+
export function cleanupCommand(): Command {
|
|
12
|
+
return new Command('cleanup')
|
|
13
|
+
.description('Remove unused locale keys from translation files')
|
|
14
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
15
|
+
.option('--dry-run', 'Preview changes without modifying files')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
logger.header('ai-localize cleanup');
|
|
18
|
+
const spinner = createSpinner('Validating keys...').start();
|
|
19
|
+
try {
|
|
20
|
+
const cwd = opts.cwd as string;
|
|
21
|
+
const { config } = await loadConfig(cwd);
|
|
22
|
+
const localesDir = path.resolve(cwd, config.localesDir);
|
|
23
|
+
const sourceDir = path.resolve(cwd, config.sourceDir);
|
|
24
|
+
|
|
25
|
+
const validator = new UnusedKeyValidator(localesDir, sourceDir, config.defaultLanguage);
|
|
26
|
+
const { unusedKeys } = validator.validate();
|
|
27
|
+
spinner.succeed('Found ' + chalk.yellow(String(unusedKeys.length)) + ' unused keys');
|
|
28
|
+
|
|
29
|
+
if (unusedKeys.length === 0) { logger.success('No unused keys found!'); return; }
|
|
30
|
+
unusedKeys.slice(0, 20).forEach((k) => logger.dim('- ' + k));
|
|
31
|
+
|
|
32
|
+
if (!opts.dryRun) {
|
|
33
|
+
const defaultDir = path.join(localesDir, config.defaultLanguage);
|
|
34
|
+
const nsFiles = fs.readdirSync(defaultDir).filter((f) => f.endsWith('.json'));
|
|
35
|
+
let removed = 0;
|
|
36
|
+
for (const nsFile of nsFiles) {
|
|
37
|
+
const ns = nsFile.replace('.json', '');
|
|
38
|
+
const fp = path.join(defaultDir, nsFile);
|
|
39
|
+
const entries = readJsonSafe<Record<string, string>>(fp) || {};
|
|
40
|
+
for (const key of unusedKeys) {
|
|
41
|
+
const lk = key.startsWith(ns + '.') ? key.slice(ns.length + 1) : null;
|
|
42
|
+
if (lk && lk in entries) { delete entries[lk]; removed++; }
|
|
43
|
+
}
|
|
44
|
+
writeJson(fp, entries);
|
|
45
|
+
}
|
|
46
|
+
logger.success('Removed ' + removed + ' unused keys');
|
|
47
|
+
} else {
|
|
48
|
+
logger.info('Dry run - ' + unusedKeys.length + ' keys would be removed');
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.fail('Cleanup failed');
|
|
52
|
+
logger.error(String(err));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { deduplicateTexts, LocaleExtractor, LocaleWriter } from '@ai-localize/locale-engine';
|
|
9
|
+
|
|
10
|
+
export function extractCommand(): Command {
|
|
11
|
+
return new Command('extract')
|
|
12
|
+
.description('Extract hardcoded text to locale JSON files')
|
|
13
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
14
|
+
.option('--dry-run', 'Preview changes without modifying files')
|
|
15
|
+
.option('--no-merge', 'Overwrite existing keys')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
logger.header('ai-localize extract');
|
|
18
|
+
const cwd = opts.cwd as string;
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const { config } = await loadConfig(cwd);
|
|
22
|
+
spinner.succeed('Configuration loaded');
|
|
23
|
+
|
|
24
|
+
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
25
|
+
const scanner = new ProjectScanner(config);
|
|
26
|
+
const scanResult = await scanner.scan();
|
|
27
|
+
ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
|
|
28
|
+
|
|
29
|
+
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
30
|
+
logger.info('Unique texts: ' + chalk.cyan(String(uniqueTexts.length)));
|
|
31
|
+
|
|
32
|
+
const extractor = new LocaleExtractor({
|
|
33
|
+
defaultLanguage: config.defaultLanguage,
|
|
34
|
+
targetLanguages: config.targetLanguages,
|
|
35
|
+
namespaceSplitting: true,
|
|
36
|
+
});
|
|
37
|
+
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
38
|
+
logger.info('Keys generated: ' + chalk.green(String(keyCount)));
|
|
39
|
+
logger.info('Namespaces: ' + chalk.cyan(namespaces.join(', ')));
|
|
40
|
+
|
|
41
|
+
if (!opts.dryRun) {
|
|
42
|
+
const localesDir = path.resolve(cwd, config.localesDir);
|
|
43
|
+
const writer = new LocaleWriter({ localesDir, merge: opts.merge !== false });
|
|
44
|
+
const { written, created, merged } = writer.write(localeFiles);
|
|
45
|
+
logger.success('Wrote ' + written.length + ' locale files (' + created.length + ' new, ' + merged.length + ' merged)');
|
|
46
|
+
} else {
|
|
47
|
+
logger.info('Dry run - no files written');
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail('Extraction failed');
|
|
51
|
+
logger.error(String(err));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { deduplicateTexts, LocaleExtractor, LocaleWriter } from '@ai-localize/locale-engine';
|
|
9
|
+
import { CodemodRunner } from '@ai-localize/codemods';
|
|
10
|
+
import { LocaleValidator } from '@ai-localize/validators';
|
|
11
|
+
import { buildReport, generateHtmlReport, printCliSummary } from '@ai-localize/reporting';
|
|
12
|
+
|
|
13
|
+
export function fullMigrateCommand(): Command {
|
|
14
|
+
return new Command('full-migrate')
|
|
15
|
+
.description('Run full localization pipeline: scan -> extract -> codemod -> validate -> report')
|
|
16
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
17
|
+
.option('--dry-run', 'Preview changes without modifying files')
|
|
18
|
+
.option('--no-codemods', 'Skip codemod phase')
|
|
19
|
+
.option('--no-report', 'Skip report generation')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
logger.header('ai-localize full-migrate');
|
|
22
|
+
const cwd = opts.cwd as string;
|
|
23
|
+
const dryRun = opts.dryRun as boolean;
|
|
24
|
+
try {
|
|
25
|
+
const cs = createSpinner('Loading configuration...').start();
|
|
26
|
+
const { config } = await loadConfig(cwd);
|
|
27
|
+
cs.succeed('Configuration loaded');
|
|
28
|
+
|
|
29
|
+
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
30
|
+
const scanner = new ProjectScanner(config);
|
|
31
|
+
const scanResult = await scanner.scan();
|
|
32
|
+
ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
|
|
33
|
+
|
|
34
|
+
const es = createSpinner('Extracting locale keys...').start();
|
|
35
|
+
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
36
|
+
const extractor = new LocaleExtractor({
|
|
37
|
+
defaultLanguage: config.defaultLanguage,
|
|
38
|
+
targetLanguages: config.targetLanguages,
|
|
39
|
+
namespaceSplitting: true,
|
|
40
|
+
});
|
|
41
|
+
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
42
|
+
es.succeed('Generated ' + chalk.green(String(keyCount)) + ' locale keys');
|
|
43
|
+
|
|
44
|
+
if (!dryRun) {
|
|
45
|
+
const writer = new LocaleWriter({ localesDir: path.resolve(cwd, config.localesDir), merge: true });
|
|
46
|
+
writer.write(localeFiles);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (opts.codemods !== false) {
|
|
50
|
+
const ms = createSpinner('Applying i18n codemods...').start();
|
|
51
|
+
const runner = new CodemodRunner(config);
|
|
52
|
+
const codemodResult = await runner.run(uniqueTexts, { dryRun });
|
|
53
|
+
ms.succeed('Codemods: ' + chalk.green(String(codemodResult.totalReplacements)) + ' replacements in ' + codemodResult.changedFiles + ' files');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const vs = createSpinner('Validating locale files...').start();
|
|
57
|
+
const validator = new LocaleValidator({
|
|
58
|
+
localesDir: path.resolve(cwd, config.localesDir),
|
|
59
|
+
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
60
|
+
defaultLanguage: config.defaultLanguage,
|
|
61
|
+
targetLanguages: config.targetLanguages,
|
|
62
|
+
});
|
|
63
|
+
const validationResult = validator.validate();
|
|
64
|
+
vs.succeed(validationResult.valid ? chalk.green('Locale files valid!') : chalk.yellow(validationResult.errors.length + ' errors, ' + validationResult.warnings.length + ' warnings'));
|
|
65
|
+
|
|
66
|
+
if (opts.report !== false) {
|
|
67
|
+
const report = buildReport({ scanResult, validationResult });
|
|
68
|
+
printCliSummary(report);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.success('Full migration complete!');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error('Migration failed: ' + String(err));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
5
|
+
import { writeDefaultConfig } from '@ai-localize/config';
|
|
6
|
+
import { detectFramework } from '@ai-localize/framework-detectors';
|
|
7
|
+
|
|
8
|
+
export function initCommand(): Command {
|
|
9
|
+
return new Command('init')
|
|
10
|
+
.description('Initialize ai-localize configuration in the current project')
|
|
11
|
+
.option('-f, --framework <type>', 'Override framework detection')
|
|
12
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
logger.header('ai-localize init');
|
|
15
|
+
const spinner = createSpinner('Detecting framework...').start();
|
|
16
|
+
try {
|
|
17
|
+
const cwd = opts.cwd as string;
|
|
18
|
+
let framework = opts.framework as string;
|
|
19
|
+
if (!framework) {
|
|
20
|
+
framework = detectFramework(cwd);
|
|
21
|
+
spinner.succeed('Detected framework: ' + chalk.cyan(framework));
|
|
22
|
+
} else {
|
|
23
|
+
spinner.succeed('Using framework: ' + chalk.cyan(framework));
|
|
24
|
+
}
|
|
25
|
+
const configPath = writeDefaultConfig(cwd, framework);
|
|
26
|
+
logger.success('Config created: ' + chalk.cyan(configPath));
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
spinner.fail('Initialization failed');
|
|
29
|
+
logger.error(err.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { CdnMigrator } from '@ai-localize/aws-cloudfront';
|
|
9
|
+
|
|
10
|
+
export function migrateCdnCommand(): Command {
|
|
11
|
+
return new Command('migrate-cdn')
|
|
12
|
+
.description('Migrate legacy CDN URLs to CloudFront')
|
|
13
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
14
|
+
.option('--assets-dir <path>', 'Local assets directory to upload')
|
|
15
|
+
.option('--dry-run', 'Preview changes without executing')
|
|
16
|
+
.option('--invalidate', 'Invalidate CloudFront cache after upload')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
logger.header('ai-localize migrate-cdn');
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const cwd = opts.cwd as string;
|
|
22
|
+
const { config } = await loadConfig(cwd);
|
|
23
|
+
if (!config.aws?.bucket) {
|
|
24
|
+
spinner.fail('AWS configuration missing');
|
|
25
|
+
logger.error('Set aws.bucket and aws.distributionId in ai-localize.config.json');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
spinner.succeed('Configuration loaded');
|
|
29
|
+
|
|
30
|
+
const ss = createSpinner('Scanning for legacy CDN URLs...').start();
|
|
31
|
+
const scanner = new ProjectScanner(config);
|
|
32
|
+
const scanResult = await scanner.scan();
|
|
33
|
+
ss.succeed('Found ' + chalk.yellow(String(scanResult.legacyCdnUrls.length)) + ' legacy CDN URLs');
|
|
34
|
+
|
|
35
|
+
if (scanResult.legacyCdnUrls.length === 0) { logger.success('No legacy CDN URLs found!'); return; }
|
|
36
|
+
|
|
37
|
+
const migrator = new CdnMigrator(config.aws!);
|
|
38
|
+
const result = await migrator.migrate({
|
|
39
|
+
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
40
|
+
assetsDir: path.resolve(cwd, opts.assetsDir as string),
|
|
41
|
+
legacyCdnUrls: scanResult.legacyCdnUrls,
|
|
42
|
+
dryRun: opts.dryRun as boolean,
|
|
43
|
+
invalidateCache: opts.invalidate as boolean,
|
|
44
|
+
onProgress: (step) => logger.step(step),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
logger.success('Migration complete in ' + result.duration + 'ms');
|
|
48
|
+
logger.info(' Assets uploaded: ' + chalk.green(String(result.uploadedAssets.length)));
|
|
49
|
+
logger.info(' URLs replaced: ' + chalk.green(String(result.replacedUrls)));
|
|
50
|
+
if (result.invalidationId) logger.info(' Invalidation ID: ' + chalk.cyan(result.invalidationId));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.fail('CDN migration failed');
|
|
53
|
+
logger.error(String(err));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { readJsonSafe } from '@ai-localize/shared';
|
|
9
|
+
import type { CloudFrontAsset } from '@ai-localize/shared';
|
|
10
|
+
import { batchReplaceCdnUrls } from '@ai-localize/codemods';
|
|
11
|
+
|
|
12
|
+
export function replaceCdnCommand(): Command {
|
|
13
|
+
return new Command('replace-cdn')
|
|
14
|
+
.description('Replace legacy CDN URLs with CloudFront URLs in source files')
|
|
15
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
16
|
+
.option('--manifest <path>', 'Path to upload manifest JSON')
|
|
17
|
+
.option('--dry-run', 'Preview changes without executing')
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
logger.header('ai-localize replace-cdn');
|
|
20
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
21
|
+
try {
|
|
22
|
+
const cwd = opts.cwd as string;
|
|
23
|
+
const { config } = await loadConfig(cwd);
|
|
24
|
+
spinner.succeed('Configuration loaded');
|
|
25
|
+
|
|
26
|
+
let assets: CloudFrontAsset[] = [];
|
|
27
|
+
if (opts.manifest) {
|
|
28
|
+
assets = readJsonSafe<CloudFrontAsset[]>(path.resolve(cwd, opts.manifest as string)) || [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ss = createSpinner('Scanning for legacy CDN URLs...').start();
|
|
32
|
+
const scanner = new ProjectScanner(config);
|
|
33
|
+
const scanResult = await scanner.scan();
|
|
34
|
+
ss.succeed('Found ' + chalk.yellow(String(scanResult.legacyCdnUrls.length)) + ' legacy CDN URLs');
|
|
35
|
+
|
|
36
|
+
if (scanResult.legacyCdnUrls.length === 0) { logger.success('No legacy CDN URLs found!'); return; }
|
|
37
|
+
|
|
38
|
+
if (!opts.dryRun) {
|
|
39
|
+
const rs = createSpinner('Replacing URLs...').start();
|
|
40
|
+
const replacedCount = await batchReplaceCdnUrls(
|
|
41
|
+
path.resolve(cwd, config.sourceDir),
|
|
42
|
+
scanResult.legacyCdnUrls,
|
|
43
|
+
assets
|
|
44
|
+
);
|
|
45
|
+
rs.succeed('Replaced ' + chalk.green(String(replacedCount)) + ' URLs');
|
|
46
|
+
} else {
|
|
47
|
+
logger.info('Dry run - no URLs replaced');
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail('CDN replacement failed');
|
|
51
|
+
logger.error(String(err));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { LocaleValidator } from '@ai-localize/validators';
|
|
9
|
+
import { buildReport, generateHtmlReport, printCliSummary } from '@ai-localize/reporting';
|
|
10
|
+
|
|
11
|
+
export function reportCommand(): Command {
|
|
12
|
+
return new Command('report')
|
|
13
|
+
.description('Generate JSON and HTML localization reports')
|
|
14
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
15
|
+
.option('--output-dir <path>', 'Output directory for reports', '.ai-localize-reports')
|
|
16
|
+
.option('--no-html', 'Skip HTML report generation')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
logger.header('ai-localize report');
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const cwd = opts.cwd as string;
|
|
22
|
+
const { config } = await loadConfig(cwd);
|
|
23
|
+
spinner.succeed('Configuration loaded');
|
|
24
|
+
|
|
25
|
+
const ss = createSpinner('Scanning project...').start();
|
|
26
|
+
const scanner = new ProjectScanner(config);
|
|
27
|
+
const scanResult = await scanner.scan();
|
|
28
|
+
ss.succeed('Scanned ' + scanResult.scannedFiles + ' files');
|
|
29
|
+
|
|
30
|
+
const vs = createSpinner('Validating locale files...').start();
|
|
31
|
+
const validator = new LocaleValidator({
|
|
32
|
+
localesDir: path.resolve(cwd, config.localesDir),
|
|
33
|
+
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
34
|
+
defaultLanguage: config.defaultLanguage,
|
|
35
|
+
targetLanguages: config.targetLanguages,
|
|
36
|
+
});
|
|
37
|
+
const validationResult = validator.validate();
|
|
38
|
+
vs.succeed('Validation complete');
|
|
39
|
+
|
|
40
|
+
const outDir = path.resolve(cwd, opts.outputDir as string);
|
|
41
|
+
const rs = createSpinner('Building report...').start();
|
|
42
|
+
const report = buildReport({ scanResult, validationResult });
|
|
43
|
+
|
|
44
|
+
if (opts.html !== false) {
|
|
45
|
+
generateHtmlReport(report, outDir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
rs.succeed('Report generated at ' + chalk.cyan(outDir));
|
|
49
|
+
printCliSummary(report);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.fail('Report generation failed');
|
|
52
|
+
logger.error(String(err));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { ProjectScanner, GitScanner } from '@ai-localize/scanner';
|
|
8
|
+
import { writeJson } from '@ai-localize/shared';
|
|
9
|
+
|
|
10
|
+
export function scanCommand(): Command {
|
|
11
|
+
return new Command('scan')
|
|
12
|
+
.description('Scan project for hardcoded texts and asset references')
|
|
13
|
+
.option('--incremental', 'Scan only changed files based on git diff')
|
|
14
|
+
.option('--staged', 'Scan only git staged files')
|
|
15
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
16
|
+
.option('--output <path>', 'Output JSON file path')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
logger.header('ai-localize scan');
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const cwd = opts.cwd as string;
|
|
22
|
+
const { config } = await loadConfig(cwd);
|
|
23
|
+
spinner.succeed('Configuration loaded');
|
|
24
|
+
|
|
25
|
+
let files: string[] | undefined;
|
|
26
|
+
if (opts.staged || opts.incremental) {
|
|
27
|
+
const git = new GitScanner(cwd);
|
|
28
|
+
files = opts.staged ? git.getStagedFiles() : git.getChangedFiles();
|
|
29
|
+
logger.info('Changed files: ' + chalk.cyan(String(files.length)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const scanSpinner = createSpinner('Scanning files...').start();
|
|
33
|
+
const scanner = new ProjectScanner(config);
|
|
34
|
+
const result = await scanner.scan({ files });
|
|
35
|
+
scanSpinner.succeed('Scanned ' + chalk.cyan(String(result.scannedFiles)) + ' files in ' + chalk.cyan(result.duration + 'ms'));
|
|
36
|
+
|
|
37
|
+
logger.info('Hardcoded texts: ' + chalk.yellow(String(result.detectedTexts.length)));
|
|
38
|
+
logger.info('Asset references: ' + chalk.blue(String(result.assets.length)));
|
|
39
|
+
logger.info('Legacy CDN URLs: ' + chalk.red(String(result.legacyCdnUrls.length)));
|
|
40
|
+
|
|
41
|
+
if (opts.output) {
|
|
42
|
+
const outPath = path.resolve(cwd, opts.output as string);
|
|
43
|
+
writeJson(outPath, result);
|
|
44
|
+
logger.success('Results saved to ' + chalk.cyan(outPath));
|
|
45
|
+
}
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
spinner.fail('Scan failed');
|
|
48
|
+
logger.error(err.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { S3Uploader } from '@ai-localize/aws-cloudfront';
|
|
8
|
+
import { writeJson } from '@ai-localize/shared';
|
|
9
|
+
|
|
10
|
+
export function uploadAssetsCommand(): Command {
|
|
11
|
+
return new Command('upload-assets')
|
|
12
|
+
.description('Upload local assets to AWS S3')
|
|
13
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
14
|
+
.option('--assets-dir <path>', 'Directory containing assets to upload')
|
|
15
|
+
.option('--force', 'Force upload even if file already exists with same hash')
|
|
16
|
+
.option('--output <path>', 'Output manifest JSON path')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
logger.header('ai-localize upload-assets');
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const cwd = opts.cwd as string;
|
|
22
|
+
const { config } = await loadConfig(cwd);
|
|
23
|
+
if (!config.aws?.bucket) {
|
|
24
|
+
spinner.fail('AWS configuration missing');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
spinner.succeed('Configuration loaded');
|
|
28
|
+
|
|
29
|
+
const uploader = new S3Uploader(config.aws!);
|
|
30
|
+
const assetsDir = path.resolve(cwd, opts.assetsDir as string);
|
|
31
|
+
const uploadSpinner = createSpinner('Uploading assets...').start();
|
|
32
|
+
const result = await uploader.uploadDirectory({
|
|
33
|
+
assetsDir,
|
|
34
|
+
force: opts.force as boolean,
|
|
35
|
+
onProgress: (_a, done, total) => { uploadSpinner.text = 'Uploading ' + done + '/' + total; },
|
|
36
|
+
});
|
|
37
|
+
uploadSpinner.succeed('Uploaded ' + chalk.green(String(result.uploaded.length)) + ' (' + result.skipped.length + ' skipped)');
|
|
38
|
+
|
|
39
|
+
if (opts.output) {
|
|
40
|
+
const outPath = path.resolve(cwd, opts.output as string);
|
|
41
|
+
writeJson(outPath, [...result.uploaded, ...result.skipped]);
|
|
42
|
+
logger.success('Manifest saved to ' + chalk.cyan(outPath));
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
spinner.fail('Upload failed');
|
|
46
|
+
logger.error(String(err));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { createSpinner } from '../utils/spinner.js';
|
|
6
|
+
import { loadConfig } from '@ai-localize/config';
|
|
7
|
+
import { LocaleValidator } from '@ai-localize/validators';
|
|
8
|
+
|
|
9
|
+
export function validateCommand(): Command {
|
|
10
|
+
return new Command('validate')
|
|
11
|
+
.description('Validate locale files for missing/duplicate/unused keys')
|
|
12
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
13
|
+
.option('--no-unused', 'Skip unused key check')
|
|
14
|
+
.option('--no-duplicates', 'Skip duplicate key check')
|
|
15
|
+
.option('--no-placeholders', 'Skip placeholder check')
|
|
16
|
+
.option('--fail-on-warning', 'Exit with error if warnings exist')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
logger.header('ai-localize validate');
|
|
19
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
+
try {
|
|
21
|
+
const cwd = opts.cwd as string;
|
|
22
|
+
const { config } = await loadConfig(cwd);
|
|
23
|
+
spinner.succeed('Configuration loaded');
|
|
24
|
+
|
|
25
|
+
const vs = createSpinner('Validating locale files...').start();
|
|
26
|
+
const validator = new LocaleValidator({
|
|
27
|
+
localesDir: path.resolve(cwd, config.localesDir),
|
|
28
|
+
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
29
|
+
defaultLanguage: config.defaultLanguage,
|
|
30
|
+
targetLanguages: config.targetLanguages,
|
|
31
|
+
checkUnused: opts.unused !== false,
|
|
32
|
+
checkDuplicates: opts.duplicates !== false,
|
|
33
|
+
checkPlaceholders: opts.placeholders !== false,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = validator.validate();
|
|
37
|
+
|
|
38
|
+
if (result.valid) {
|
|
39
|
+
vs.succeed(chalk.green('Locale files are valid!'));
|
|
40
|
+
} else {
|
|
41
|
+
vs.fail(chalk.red(`Validation failed with ${result.errors.length} errors and ${result.warnings.length} warnings.`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
result.errors.forEach((err) => logger.error(`[${err.type}] ${err.message}`));
|
|
45
|
+
result.warnings.forEach((warn) => logger.warn(`[${warn.type}] ${warn.message}`));
|
|
46
|
+
|
|
47
|
+
if (!result.valid || (opts.failOnWarning && result.warnings.length > 0)) {
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.fail('Validation failed');
|
|
52
|
+
logger.error(String(err));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg: string) => console.log(chalk.blue('i'), msg),
|
|
5
|
+
success: (msg: string) => console.log(chalk.green('v'), msg),
|
|
6
|
+
warn: (msg: string) => console.log(chalk.yellow('!'), msg),
|
|
7
|
+
error: (msg: string) => console.error(chalk.red('x'), msg),
|
|
8
|
+
step: (msg: string) => console.log(chalk.cyan('>'), msg),
|
|
9
|
+
dim: (msg: string) => console.log(chalk.dim(msg)),
|
|
10
|
+
header: (msg: string) => console.log('\n' + chalk.bold.cyan(msg) + '\n'),
|
|
11
|
+
};
|