ai-localize-cli 2.0.0 → 2.0.1
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/CHANGELOG.md +16 -0
- package/dist/cli.js +101 -23
- package/package.json +10 -10
- package/src/commands/extract.ts +36 -17
- package/src/commands/full-migrate.ts +59 -23
- package/src/commands/report.ts +32 -18
- package/src/commands/scan.ts +77 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# ai-localize-cli
|
|
2
2
|
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix config schema validation (aws optional/nullable, framework type), ignore className CSS values in AST scanner, add includePatterns config support for selective file scanning, add --extract-cdns flag to scan command to dump CDN URLs to a JSON file
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- ai-localize-scanner@2.0.1
|
|
10
|
+
- ai-localize-config@2.0.1
|
|
11
|
+
- ai-localize-shared@2.0.1
|
|
12
|
+
- ai-localize-aws-cloudfront@2.0.1
|
|
13
|
+
- ai-localize-codemods@2.0.1
|
|
14
|
+
- ai-localize-framework-detectors@2.0.1
|
|
15
|
+
- ai-localize-locale-engine@2.0.1
|
|
16
|
+
- ai-localize-reporting@2.0.1
|
|
17
|
+
- ai-localize-validators@2.0.1
|
|
18
|
+
|
|
3
19
|
## 2.0.0
|
|
4
20
|
|
|
5
21
|
### Major Changes
|
package/dist/cli.js
CHANGED
|
@@ -80,11 +80,12 @@ function initCommand() {
|
|
|
80
80
|
var import_commander2 = require("commander");
|
|
81
81
|
var import_chalk3 = __toESM(require("chalk"));
|
|
82
82
|
var path = __toESM(require("path"));
|
|
83
|
+
var fs = __toESM(require("fs"));
|
|
83
84
|
var import_ai_localize_config2 = require("ai-localize-config");
|
|
84
85
|
var import_ai_localize_scanner = require("ai-localize-scanner");
|
|
85
86
|
var import_ai_localize_shared = require("ai-localize-shared");
|
|
86
87
|
function scanCommand() {
|
|
87
|
-
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) => {
|
|
88
|
+
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 for full scan results").option("--extract-cdns <path>", "Extract all discovered CDN/legacy URLs to a JSON file (e.g. cdn-urls.json)").action(async (opts) => {
|
|
88
89
|
logger.header("ai-localize scan");
|
|
89
90
|
const spinner = createSpinner("Loading configuration...").start();
|
|
90
91
|
try {
|
|
@@ -109,6 +110,38 @@ function scanCommand() {
|
|
|
109
110
|
(0, import_ai_localize_shared.writeJson)(outPath, result);
|
|
110
111
|
logger.success("Results saved to " + import_chalk3.default.cyan(outPath));
|
|
111
112
|
}
|
|
113
|
+
if (opts.extractCdns) {
|
|
114
|
+
const cdnOutPath = path.resolve(cwd, opts.extractCdns);
|
|
115
|
+
const cdnList = {
|
|
116
|
+
summary: {
|
|
117
|
+
totalLegacyCdnUrls: result.legacyCdnUrls.length,
|
|
118
|
+
totalAssetReferences: result.assets.length,
|
|
119
|
+
scannedAt: result.timestamp
|
|
120
|
+
},
|
|
121
|
+
legacyCdnUrls: result.legacyCdnUrls.map((cdn) => ({
|
|
122
|
+
url: cdn.url,
|
|
123
|
+
assetPath: cdn.assetPath,
|
|
124
|
+
filePath: cdn.filePath,
|
|
125
|
+
line: cdn.line
|
|
126
|
+
})),
|
|
127
|
+
assetReferences: result.assets.map((asset) => ({
|
|
128
|
+
assetPath: asset.assetPath,
|
|
129
|
+
assetType: asset.assetType,
|
|
130
|
+
referenceType: asset.referenceType,
|
|
131
|
+
filePath: asset.filePath,
|
|
132
|
+
line: asset.line
|
|
133
|
+
}))
|
|
134
|
+
};
|
|
135
|
+
const cdnOutDir = path.dirname(cdnOutPath);
|
|
136
|
+
if (!fs.existsSync(cdnOutDir)) {
|
|
137
|
+
fs.mkdirSync(cdnOutDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
fs.writeFileSync(cdnOutPath, JSON.stringify(cdnList, null, 2), "utf-8");
|
|
140
|
+
logger.success("CDN URLs extracted to " + import_chalk3.default.cyan(cdnOutPath));
|
|
141
|
+
logger.info(
|
|
142
|
+
" Legacy CDN URLs: " + import_chalk3.default.red(String(result.legacyCdnUrls.length)) + " Asset References: " + import_chalk3.default.blue(String(result.assets.length))
|
|
143
|
+
);
|
|
144
|
+
}
|
|
112
145
|
} catch (err) {
|
|
113
146
|
spinner.fail("Scan failed");
|
|
114
147
|
logger.error(err.message);
|
|
@@ -132,6 +165,8 @@ function extractCommand() {
|
|
|
132
165
|
try {
|
|
133
166
|
const { config } = await (0, import_ai_localize_config3.loadConfig)(cwd);
|
|
134
167
|
spinner.succeed("Configuration loaded");
|
|
168
|
+
const structure = config.localeStructure ?? "nested";
|
|
169
|
+
logger.info("Locale structure: " + import_chalk4.default.cyan(structure));
|
|
135
170
|
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
136
171
|
const scanner = new import_ai_localize_scanner2.ProjectScanner(config);
|
|
137
172
|
const scanResult = await scanner.scan();
|
|
@@ -141,18 +176,32 @@ function extractCommand() {
|
|
|
141
176
|
const extractor = new import_ai_localize_locale_engine.LocaleExtractor({
|
|
142
177
|
defaultLanguage: config.defaultLanguage,
|
|
143
178
|
targetLanguages: config.targetLanguages,
|
|
144
|
-
namespaceSplitting:
|
|
179
|
+
namespaceSplitting: structure === "nested"
|
|
180
|
+
// flat mode does not need namespace splitting
|
|
145
181
|
});
|
|
146
182
|
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
147
183
|
logger.info("Keys generated: " + import_chalk4.default.green(String(keyCount)));
|
|
148
|
-
|
|
184
|
+
if (structure === "nested") {
|
|
185
|
+
logger.info("Namespaces: " + import_chalk4.default.cyan(namespaces.join(", ")));
|
|
186
|
+
}
|
|
149
187
|
if (!opts.dryRun) {
|
|
150
188
|
const localesDir = path2.resolve(cwd, config.localesDir);
|
|
151
|
-
const writer = new import_ai_localize_locale_engine.LocaleWriter({
|
|
189
|
+
const writer = new import_ai_localize_locale_engine.LocaleWriter({
|
|
190
|
+
localesDir,
|
|
191
|
+
merge: opts.merge !== false,
|
|
192
|
+
localeStructure: structure
|
|
193
|
+
});
|
|
152
194
|
const { written, created, merged } = writer.write(localeFiles);
|
|
153
|
-
logger.success(
|
|
195
|
+
logger.success(
|
|
196
|
+
"Wrote " + written.length + " locale files (" + created.length + " new, " + merged.length + " merged)"
|
|
197
|
+
);
|
|
198
|
+
written.forEach((f) => logger.info(" " + import_chalk4.default.gray(f)));
|
|
154
199
|
} else {
|
|
155
|
-
logger.info("Dry run
|
|
200
|
+
logger.info("Dry run \u2014 no files written");
|
|
201
|
+
localeFiles.forEach((lf) => {
|
|
202
|
+
const label = structure === "flat" ? `${lf.language}.json` : `${lf.language}/${lf.namespace}.json`;
|
|
203
|
+
logger.info(" " + import_chalk4.default.gray(label) + " \u2014 " + Object.keys(lf.entries).length + " keys");
|
|
204
|
+
});
|
|
156
205
|
}
|
|
157
206
|
} catch (err) {
|
|
158
207
|
spinner.fail("Extraction failed");
|
|
@@ -209,7 +258,7 @@ function validateCommand() {
|
|
|
209
258
|
var import_commander5 = require("commander");
|
|
210
259
|
var import_chalk6 = __toESM(require("chalk"));
|
|
211
260
|
var path4 = __toESM(require("path"));
|
|
212
|
-
var
|
|
261
|
+
var fs2 = __toESM(require("fs"));
|
|
213
262
|
var import_ai_localize_config5 = require("ai-localize-config");
|
|
214
263
|
var import_ai_localize_validators2 = require("ai-localize-validators");
|
|
215
264
|
var import_ai_localize_shared2 = require("ai-localize-shared");
|
|
@@ -232,7 +281,7 @@ function cleanupCommand() {
|
|
|
232
281
|
unusedKeys.slice(0, 20).forEach((k) => logger.dim("- " + k));
|
|
233
282
|
if (!opts.dryRun) {
|
|
234
283
|
const defaultDir = path4.join(localesDir, config.defaultLanguage);
|
|
235
|
-
const nsFiles =
|
|
284
|
+
const nsFiles = fs2.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
|
|
236
285
|
let removed = 0;
|
|
237
286
|
for (const nsFile of nsFiles) {
|
|
238
287
|
const ns = nsFile.replace(".json", "");
|
|
@@ -407,7 +456,7 @@ var import_ai_localize_scanner5 = require("ai-localize-scanner");
|
|
|
407
456
|
var import_ai_localize_validators3 = require("ai-localize-validators");
|
|
408
457
|
var import_ai_localize_reporting = require("ai-localize-reporting");
|
|
409
458
|
function reportCommand() {
|
|
410
|
-
return new import_commander9.Command("report").description("Generate
|
|
459
|
+
return new import_commander9.Command("report").description("Generate an HTML localization report with full details").option("--cwd <path>", "Working directory", process.cwd()).option("--output-dir <path>", "Directory where the HTML report is saved", ".ai-localize-reports").option("--filename <name>", "Report filename (default: report-<timestamp>.html)").option("--no-open", "Do not log the file path after generation").action(async (opts) => {
|
|
411
460
|
logger.header("ai-localize report");
|
|
412
461
|
const spinner = createSpinner("Loading configuration...").start();
|
|
413
462
|
try {
|
|
@@ -417,7 +466,7 @@ function reportCommand() {
|
|
|
417
466
|
const ss = createSpinner("Scanning project...").start();
|
|
418
467
|
const scanner = new import_ai_localize_scanner5.ProjectScanner(config);
|
|
419
468
|
const scanResult = await scanner.scan();
|
|
420
|
-
ss.succeed("Scanned " + scanResult.scannedFiles + " files");
|
|
469
|
+
ss.succeed("Scanned " + import_chalk10.default.cyan(String(scanResult.scannedFiles)) + " files");
|
|
421
470
|
const vs = createSpinner("Validating locale files...").start();
|
|
422
471
|
const validator = new import_ai_localize_validators3.LocaleValidator({
|
|
423
472
|
localesDir: path8.resolve(cwd, config.localesDir),
|
|
@@ -426,15 +475,21 @@ function reportCommand() {
|
|
|
426
475
|
targetLanguages: config.targetLanguages
|
|
427
476
|
});
|
|
428
477
|
const validationResult = validator.validate();
|
|
429
|
-
vs.succeed(
|
|
430
|
-
|
|
478
|
+
vs.succeed(
|
|
479
|
+
validationResult.valid ? import_chalk10.default.green("Locale files valid") : import_chalk10.default.yellow(validationResult.errors.length + " errors, " + validationResult.warnings.length + " warnings")
|
|
480
|
+
);
|
|
431
481
|
const rs = createSpinner("Building report...").start();
|
|
432
482
|
const report = (0, import_ai_localize_reporting.buildReport)({ scanResult, validationResult });
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
483
|
+
const outDir = path8.resolve(cwd, opts.outputDir);
|
|
484
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
485
|
+
const filename = opts.filename || "report-" + timestamp + ".html";
|
|
486
|
+
const htmlPath = path8.join(outDir, filename);
|
|
487
|
+
(0, import_ai_localize_reporting.generateHtmlReport)(report, htmlPath);
|
|
488
|
+
rs.succeed("HTML report written to " + import_chalk10.default.cyan(htmlPath));
|
|
437
489
|
(0, import_ai_localize_reporting.printCliSummary)(report);
|
|
490
|
+
logger.info(
|
|
491
|
+
"\n Open the report in your browser:\n " + import_chalk10.default.underline("file://" + htmlPath)
|
|
492
|
+
);
|
|
438
493
|
} catch (err) {
|
|
439
494
|
spinner.fail("Report generation failed");
|
|
440
495
|
logger.error(String(err));
|
|
@@ -454,7 +509,7 @@ var import_ai_localize_codemods2 = require("ai-localize-codemods");
|
|
|
454
509
|
var import_ai_localize_validators4 = require("ai-localize-validators");
|
|
455
510
|
var import_ai_localize_reporting2 = require("ai-localize-reporting");
|
|
456
511
|
function fullMigrateCommand() {
|
|
457
|
-
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) => {
|
|
512
|
+
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").option("--report-dir <path>", "Directory where the HTML report is saved", ".ai-localize-reports").action(async (opts) => {
|
|
458
513
|
logger.header("ai-localize full-migrate");
|
|
459
514
|
const cwd = opts.cwd;
|
|
460
515
|
const dryRun = opts.dryRun;
|
|
@@ -462,28 +517,38 @@ function fullMigrateCommand() {
|
|
|
462
517
|
const cs = createSpinner("Loading configuration...").start();
|
|
463
518
|
const { config } = await (0, import_ai_localize_config10.loadConfig)(cwd);
|
|
464
519
|
cs.succeed("Configuration loaded");
|
|
520
|
+
const structure = config.localeStructure ?? "nested";
|
|
465
521
|
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
466
522
|
const scanner = new import_ai_localize_scanner6.ProjectScanner(config);
|
|
467
523
|
const scanResult = await scanner.scan();
|
|
468
|
-
ss.succeed(
|
|
524
|
+
ss.succeed(
|
|
525
|
+
"Found " + import_chalk11.default.cyan(String(scanResult.detectedTexts.length)) + " texts in " + scanResult.scannedFiles + " files"
|
|
526
|
+
);
|
|
469
527
|
const es = createSpinner("Extracting locale keys...").start();
|
|
470
528
|
const uniqueTexts = (0, import_ai_localize_locale_engine2.deduplicateTexts)(scanResult.detectedTexts);
|
|
471
529
|
const extractor = new import_ai_localize_locale_engine2.LocaleExtractor({
|
|
472
530
|
defaultLanguage: config.defaultLanguage,
|
|
473
531
|
targetLanguages: config.targetLanguages,
|
|
474
|
-
|
|
532
|
+
// Flat layout merges all keys into one file per language — no namespace splitting
|
|
533
|
+
namespaceSplitting: structure === "nested"
|
|
475
534
|
});
|
|
476
535
|
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
477
536
|
es.succeed("Generated " + import_chalk11.default.green(String(keyCount)) + " locale keys");
|
|
478
537
|
if (!dryRun) {
|
|
479
|
-
const writer = new import_ai_localize_locale_engine2.LocaleWriter({
|
|
538
|
+
const writer = new import_ai_localize_locale_engine2.LocaleWriter({
|
|
539
|
+
localesDir: path9.resolve(cwd, config.localesDir),
|
|
540
|
+
merge: true,
|
|
541
|
+
localeStructure: structure
|
|
542
|
+
});
|
|
480
543
|
writer.write(localeFiles);
|
|
481
544
|
}
|
|
482
545
|
if (opts.codemods !== false) {
|
|
483
546
|
const ms = createSpinner("Applying i18n codemods...").start();
|
|
484
|
-
const runner = new import_ai_localize_codemods2.CodemodRunner(config);
|
|
547
|
+
const runner = new import_ai_localize_codemods2.CodemodRunner(config, cwd);
|
|
485
548
|
const codemodResult = await runner.run(uniqueTexts, { dryRun });
|
|
486
|
-
ms.succeed(
|
|
549
|
+
ms.succeed(
|
|
550
|
+
"Codemods: " + import_chalk11.default.green(String(codemodResult.totalReplacements)) + " replacements in " + codemodResult.changedFiles + " files"
|
|
551
|
+
);
|
|
487
552
|
}
|
|
488
553
|
const vs = createSpinner("Validating locale files...").start();
|
|
489
554
|
const validator = new import_ai_localize_validators4.LocaleValidator({
|
|
@@ -493,10 +558,23 @@ function fullMigrateCommand() {
|
|
|
493
558
|
targetLanguages: config.targetLanguages
|
|
494
559
|
});
|
|
495
560
|
const validationResult = validator.validate();
|
|
496
|
-
vs.succeed(
|
|
561
|
+
vs.succeed(
|
|
562
|
+
validationResult.valid ? import_chalk11.default.green("Locale files valid!") : import_chalk11.default.yellow(
|
|
563
|
+
validationResult.errors.length + " errors, " + validationResult.warnings.length + " warnings"
|
|
564
|
+
)
|
|
565
|
+
);
|
|
497
566
|
if (opts.report !== false) {
|
|
567
|
+
const rs = createSpinner("Generating report...").start();
|
|
498
568
|
const report = (0, import_ai_localize_reporting2.buildReport)({ scanResult, validationResult });
|
|
569
|
+
const reportDir = path9.resolve(cwd, opts.reportDir);
|
|
570
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
571
|
+
const htmlPath = path9.join(reportDir, "report-" + timestamp + ".html");
|
|
572
|
+
(0, import_ai_localize_reporting2.generateHtmlReport)(report, htmlPath);
|
|
573
|
+
rs.succeed("Report saved to " + import_chalk11.default.cyan(htmlPath));
|
|
499
574
|
(0, import_ai_localize_reporting2.printCliSummary)(report);
|
|
575
|
+
logger.info(
|
|
576
|
+
"\n View report:\n " + import_chalk11.default.underline("file://" + htmlPath)
|
|
577
|
+
);
|
|
500
578
|
}
|
|
501
579
|
logger.success("Full migration complete!");
|
|
502
580
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "CLI for ai-localize-core: scan, extract, validate, migrate CDN",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-localize": "./dist/cli.js"
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"chalk": "^5.3.0",
|
|
12
12
|
"ora": "^8.0.1",
|
|
13
13
|
"inquirer": "^9.2.12",
|
|
14
|
-
"ai-localize-
|
|
15
|
-
"ai-localize-
|
|
16
|
-
"ai-localize-
|
|
17
|
-
"ai-localize-
|
|
18
|
-
"ai-localize-
|
|
19
|
-
"ai-localize-
|
|
20
|
-
"ai-localize-
|
|
21
|
-
"ai-localize-
|
|
22
|
-
"ai-localize-reporting": "2.0.
|
|
14
|
+
"ai-localize-shared": "2.0.1",
|
|
15
|
+
"ai-localize-framework-detectors": "2.0.1",
|
|
16
|
+
"ai-localize-config": "2.0.1",
|
|
17
|
+
"ai-localize-scanner": "2.0.1",
|
|
18
|
+
"ai-localize-codemods": "2.0.1",
|
|
19
|
+
"ai-localize-locale-engine": "2.0.1",
|
|
20
|
+
"ai-localize-aws-cloudfront": "2.0.1",
|
|
21
|
+
"ai-localize-validators": "2.0.1",
|
|
22
|
+
"ai-localize-reporting": "2.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/inquirer": "^9.0.7",
|
package/src/commands/extract.ts
CHANGED
|
@@ -17,37 +17,56 @@ export function extractCommand(): Command {
|
|
|
17
17
|
logger.header('ai-localize extract');
|
|
18
18
|
const cwd = opts.cwd as string;
|
|
19
19
|
const spinner = createSpinner('Loading configuration...').start();
|
|
20
|
-
|
|
20
|
+
try {
|
|
21
21
|
const { config } = await loadConfig(cwd);
|
|
22
22
|
spinner.succeed('Configuration loaded');
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Log locale structure in use
|
|
25
|
+
const structure = config.localeStructure ?? 'nested';
|
|
26
|
+
logger.info('Locale structure: ' + chalk.cyan(structure));
|
|
27
|
+
|
|
28
|
+
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
25
29
|
const scanner = new ProjectScanner(config);
|
|
26
30
|
const scanResult = await scanner.scan();
|
|
27
|
-
|
|
31
|
+
ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
34
|
+
logger.info('Unique texts: ' + chalk.cyan(String(uniqueTexts.length)));
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
namespaceSplitting:
|
|
36
|
-
|
|
36
|
+
const extractor = new LocaleExtractor({
|
|
37
|
+
defaultLanguage: config.defaultLanguage,
|
|
38
|
+
targetLanguages: config.targetLanguages,
|
|
39
|
+
namespaceSplitting: structure === 'nested', // flat mode does not need namespace splitting
|
|
40
|
+
});
|
|
37
41
|
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
38
42
|
logger.info('Keys generated: ' + chalk.green(String(keyCount)));
|
|
39
|
-
|
|
43
|
+
if (structure === 'nested') {
|
|
44
|
+
logger.info('Namespaces: ' + chalk.cyan(namespaces.join(', ')));
|
|
45
|
+
}
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
if (!opts.dryRun) {
|
|
42
48
|
const localesDir = path.resolve(cwd, config.localesDir);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const writer = new LocaleWriter({
|
|
50
|
+
localesDir,
|
|
51
|
+
merge: opts.merge !== false,
|
|
52
|
+
localeStructure: structure,
|
|
53
|
+
});
|
|
54
|
+
const { written, created, merged } = writer.write(localeFiles);
|
|
55
|
+
logger.success(
|
|
56
|
+
'Wrote ' + written.length + ' locale files (' + created.length + ' new, ' + merged.length + ' merged)'
|
|
57
|
+
);
|
|
58
|
+
written.forEach((f) => logger.info(' ' + chalk.gray(f)));
|
|
46
59
|
} else {
|
|
47
|
-
logger.info('Dry run
|
|
60
|
+
logger.info('Dry run — no files written');
|
|
61
|
+
localeFiles.forEach((lf) => {
|
|
62
|
+
const label = structure === 'flat'
|
|
63
|
+
? `${lf.language}.json`
|
|
64
|
+
: `${lf.language}/${lf.namespace}.json`;
|
|
65
|
+
logger.info(' ' + chalk.gray(label) + ' — ' + Object.keys(lf.entries).length + ' keys');
|
|
66
|
+
});
|
|
48
67
|
}
|
|
49
68
|
} catch (err) {
|
|
50
|
-
|
|
69
|
+
spinner.fail('Extraction failed');
|
|
51
70
|
logger.error(String(err));
|
|
52
71
|
process.exit(1);
|
|
53
72
|
}
|
|
@@ -17,8 +17,9 @@ export function fullMigrateCommand(): Command {
|
|
|
17
17
|
.option('--dry-run', 'Preview changes without modifying files')
|
|
18
18
|
.option('--no-codemods', 'Skip codemod phase')
|
|
19
19
|
.option('--no-report', 'Skip report generation')
|
|
20
|
-
.
|
|
21
|
-
|
|
20
|
+
.option('--report-dir <path>', 'Directory where the HTML report is saved', '.ai-localize-reports')
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
logger.header('ai-localize full-migrate');
|
|
22
23
|
const cwd = opts.cwd as string;
|
|
23
24
|
const dryRun = opts.dryRun as boolean;
|
|
24
25
|
try {
|
|
@@ -26,49 +27,84 @@ export function fullMigrateCommand(): Command {
|
|
|
26
27
|
const { config } = await loadConfig(cwd);
|
|
27
28
|
cs.succeed('Configuration loaded');
|
|
28
29
|
|
|
30
|
+
const structure = config.localeStructure ?? 'nested';
|
|
31
|
+
|
|
29
32
|
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
30
33
|
const scanner = new ProjectScanner(config);
|
|
31
34
|
const scanResult = await scanner.scan();
|
|
32
|
-
ss.succeed(
|
|
35
|
+
ss.succeed(
|
|
36
|
+
'Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) +
|
|
37
|
+
' texts in ' + scanResult.scannedFiles + ' files'
|
|
38
|
+
);
|
|
33
39
|
|
|
34
40
|
const es = createSpinner('Extracting locale keys...').start();
|
|
35
|
-
|
|
41
|
+
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
36
42
|
const extractor = new LocaleExtractor({
|
|
37
43
|
defaultLanguage: config.defaultLanguage,
|
|
38
44
|
targetLanguages: config.targetLanguages,
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
// Flat layout merges all keys into one file per language — no namespace splitting
|
|
46
|
+
namespaceSplitting: structure === 'nested',
|
|
47
|
+
});
|
|
41
48
|
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
42
49
|
es.succeed('Generated ' + chalk.green(String(keyCount)) + ' locale keys');
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
if (!dryRun) {
|
|
52
|
+
const writer = new LocaleWriter({
|
|
53
|
+
localesDir: path.resolve(cwd, config.localesDir),
|
|
54
|
+
merge: true,
|
|
55
|
+
localeStructure: structure,
|
|
56
|
+
});
|
|
46
57
|
writer.write(localeFiles);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
if (opts.codemods !== false) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
const ms = createSpinner('Applying i18n codemods...').start();
|
|
62
|
+
// Pass cwd so CodemodRunner can compute per-file relative import paths
|
|
63
|
+
// when codemods.importPackage is a local project path (e.g. "src/Locales/translate").
|
|
64
|
+
const runner = new CodemodRunner(config, cwd);
|
|
65
|
+
const codemodResult = await runner.run(uniqueTexts, { dryRun });
|
|
66
|
+
ms.succeed(
|
|
67
|
+
'Codemods: ' + chalk.green(String(codemodResult.totalReplacements)) +
|
|
68
|
+
' replacements in ' + codemodResult.changedFiles + ' files'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
55
71
|
|
|
56
|
-
|
|
72
|
+
const vs = createSpinner('Validating locale files...').start();
|
|
57
73
|
const validator = new LocaleValidator({
|
|
58
|
-
|
|
74
|
+
localesDir: path.resolve(cwd, config.localesDir),
|
|
59
75
|
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
defaultLanguage: config.defaultLanguage,
|
|
77
|
+
targetLanguages: config.targetLanguages,
|
|
62
78
|
});
|
|
63
79
|
const validationResult = validator.validate();
|
|
64
|
-
vs.succeed(
|
|
80
|
+
vs.succeed(
|
|
81
|
+
validationResult.valid
|
|
82
|
+
? chalk.green('Locale files valid!')
|
|
83
|
+
: chalk.yellow(
|
|
84
|
+
validationResult.errors.length + ' errors, ' +
|
|
85
|
+
validationResult.warnings.length + ' warnings'
|
|
86
|
+
)
|
|
87
|
+
);
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
if (opts.report !== false) {
|
|
90
|
+
const rs = createSpinner('Generating report...').start();
|
|
91
|
+
const report = buildReport({ scanResult, validationResult });
|
|
92
|
+
|
|
93
|
+
// Always write an HTML report file
|
|
94
|
+
const reportDir = path.resolve(cwd, opts.reportDir as string);
|
|
95
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
96
|
+
const htmlPath = path.join(reportDir, 'report-' + timestamp + '.html');
|
|
97
|
+
generateHtmlReport(report, htmlPath);
|
|
98
|
+
rs.succeed('Report saved to ' + chalk.cyan(htmlPath));
|
|
99
|
+
|
|
100
|
+
printCliSummary(report);
|
|
101
|
+
logger.info(
|
|
102
|
+
'\n View report:\n' +
|
|
103
|
+
' ' + chalk.underline('file://' + htmlPath)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
logger.success('Full migration complete!');
|
|
72
108
|
} catch (err) {
|
|
73
109
|
logger.error('Migration failed: ' + String(err));
|
|
74
110
|
process.exit(1);
|
package/src/commands/report.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { buildReport, generateHtmlReport, printCliSummary } from 'ai-localize-re
|
|
|
10
10
|
|
|
11
11
|
export function reportCommand(): Command {
|
|
12
12
|
return new Command('report')
|
|
13
|
-
.description('Generate
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.option('--
|
|
13
|
+
.description('Generate an HTML localization report with full details')
|
|
14
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
15
|
+
.option('--output-dir <path>', 'Directory where the HTML report is saved', '.ai-localize-reports')
|
|
16
|
+
.option('--filename <name>', 'Report filename (default: report-<timestamp>.html)')
|
|
17
|
+
.option('--no-open', 'Do not log the file path after generation')
|
|
17
18
|
.action(async (opts) => {
|
|
18
19
|
logger.header('ai-localize report');
|
|
19
20
|
const spinner = createSpinner('Loading configuration...').start();
|
|
@@ -24,10 +25,10 @@ export function reportCommand(): Command {
|
|
|
24
25
|
|
|
25
26
|
const ss = createSpinner('Scanning project...').start();
|
|
26
27
|
const scanner = new ProjectScanner(config);
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const scanResult = await scanner.scan();
|
|
29
|
+
ss.succeed('Scanned ' + chalk.cyan(String(scanResult.scannedFiles)) + ' files');
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
const vs = createSpinner('Validating locale files...').start();
|
|
31
32
|
const validator = new LocaleValidator({
|
|
32
33
|
localesDir: path.resolve(cwd, config.localesDir),
|
|
33
34
|
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
@@ -35,22 +36,35 @@ export function reportCommand(): Command {
|
|
|
35
36
|
targetLanguages: config.targetLanguages,
|
|
36
37
|
});
|
|
37
38
|
const validationResult = validator.validate();
|
|
38
|
-
vs.succeed(
|
|
39
|
+
vs.succeed(
|
|
40
|
+
validationResult.valid
|
|
41
|
+
? chalk.green('Locale files valid')
|
|
42
|
+
: chalk.yellow(validationResult.errors.length + ' errors, ' + validationResult.warnings.length + ' warnings')
|
|
43
|
+
);
|
|
39
44
|
|
|
40
|
-
const outDir = path.resolve(cwd, opts.outputDir as string);
|
|
41
45
|
const rs = createSpinner('Building report...').start();
|
|
42
|
-
|
|
46
|
+
const report = buildReport({ scanResult, validationResult });
|
|
47
|
+
|
|
48
|
+
// Resolve output path — always a .html file, never a bare directory
|
|
49
|
+
const outDir = path.resolve(cwd, opts.outputDir as string);
|
|
50
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
51
|
+
const filename = (opts.filename as string | undefined) || ('report-' + timestamp + '.html');
|
|
52
|
+
const htmlPath = path.join(outDir, filename);
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
rs.succeed('Report generated at ' + chalk.cyan(outDir));
|
|
54
|
+
generateHtmlReport(report, htmlPath);
|
|
55
|
+
rs.succeed('HTML report written to ' + chalk.cyan(htmlPath));
|
|
56
|
+
|
|
57
|
+
// Print CLI summary to terminal as well
|
|
49
58
|
printCliSummary(report);
|
|
50
|
-
|
|
59
|
+
|
|
60
|
+
logger.info(
|
|
61
|
+
'\n Open the report in your browser:\n' +
|
|
62
|
+
' ' + chalk.underline('file://' + htmlPath)
|
|
63
|
+
);
|
|
64
|
+
} catch (err) {
|
|
51
65
|
spinner.fail('Report generation failed');
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
logger.error(String(err));
|
|
67
|
+
process.exit(1);
|
|
54
68
|
}
|
|
55
69
|
});
|
|
56
70
|
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
4
5
|
import { logger } from '../utils/logger.js';
|
|
5
6
|
import { createSpinner } from '../utils/spinner.js';
|
|
6
7
|
import { loadConfig } from 'ai-localize-config';
|
|
@@ -13,40 +14,101 @@ export function scanCommand(): Command {
|
|
|
13
14
|
.option('--incremental', 'Scan only changed files based on git diff')
|
|
14
15
|
.option('--staged', 'Scan only git staged files')
|
|
15
16
|
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
16
|
-
.option('--output <path>', 'Output JSON file path')
|
|
17
|
+
.option('--output <path>', 'Output JSON file path for full scan results')
|
|
18
|
+
.option('--extract-cdns <path>', 'Extract all discovered CDN/legacy URLs to a JSON file (e.g. cdn-urls.json)')
|
|
17
19
|
.action(async (opts) => {
|
|
18
20
|
logger.header('ai-localize scan');
|
|
19
|
-
|
|
21
|
+
const spinner = createSpinner('Loading configuration...').start();
|
|
20
22
|
try {
|
|
21
23
|
const cwd = opts.cwd as string;
|
|
22
24
|
const { config } = await loadConfig(cwd);
|
|
23
25
|
spinner.succeed('Configuration loaded');
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
let files: string[] | undefined;
|
|
26
28
|
if (opts.staged || opts.incremental) {
|
|
27
|
-
|
|
29
|
+
const git = new GitScanner(cwd);
|
|
28
30
|
files = opts.staged ? git.getStagedFiles() : git.getChangedFiles();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
logger.info('Changed files: ' + chalk.cyan(String(files.length)));
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
const scanSpinner = createSpinner('Scanning files...').start();
|
|
33
35
|
const scanner = new ProjectScanner(config);
|
|
34
36
|
const result = await scanner.scan({ files });
|
|
35
|
-
|
|
37
|
+
scanSpinner.succeed('Scanned ' + chalk.cyan(String(result.scannedFiles)) + ' files in ' + chalk.cyan(result.duration + 'ms'));
|
|
36
38
|
|
|
37
39
|
logger.info('Hardcoded texts: ' + chalk.yellow(String(result.detectedTexts.length)));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
logger.info('Asset references: ' + chalk.blue(String(result.assets.length)));
|
|
41
|
+
logger.info('Legacy CDN URLs: ' + chalk.red(String(result.legacyCdnUrls.length)));
|
|
40
42
|
|
|
41
43
|
if (opts.output) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const outPath = path.resolve(cwd, opts.output as string);
|
|
45
|
+
writeJson(outPath, result);
|
|
46
|
+
logger.success('Results saved to ' + chalk.cyan(outPath));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Extract CDN URLs to a dedicated file
|
|
50
|
+
if (opts.extractCdns) {
|
|
51
|
+
const cdnOutPath = path.resolve(cwd, opts.extractCdns as string);
|
|
52
|
+
|
|
53
|
+
// Build a structured CDN list similar to locale files
|
|
54
|
+
const cdnList: {
|
|
55
|
+
summary: {
|
|
56
|
+
totalLegacyCdnUrls: number;
|
|
57
|
+
totalAssetReferences: number;
|
|
58
|
+
scannedAt: string;
|
|
59
|
+
};
|
|
60
|
+
legacyCdnUrls: Array<{
|
|
61
|
+
url: string;
|
|
62
|
+
assetPath: string;
|
|
63
|
+
filePath: string;
|
|
64
|
+
line: number;
|
|
65
|
+
}>;
|
|
66
|
+
assetReferences: Array<{
|
|
67
|
+
assetPath: string;
|
|
68
|
+
assetType: string;
|
|
69
|
+
referenceType: string;
|
|
70
|
+
filePath: string;
|
|
71
|
+
line: number;
|
|
72
|
+
}>;
|
|
73
|
+
} = {
|
|
74
|
+
summary: {
|
|
75
|
+
totalLegacyCdnUrls: result.legacyCdnUrls.length,
|
|
76
|
+
totalAssetReferences: result.assets.length,
|
|
77
|
+
scannedAt: result.timestamp,
|
|
78
|
+
},
|
|
79
|
+
legacyCdnUrls: result.legacyCdnUrls.map((cdn) => ({
|
|
80
|
+
url: cdn.url,
|
|
81
|
+
assetPath: cdn.assetPath,
|
|
82
|
+
filePath: cdn.filePath,
|
|
83
|
+
line: cdn.line,
|
|
84
|
+
})),
|
|
85
|
+
assetReferences: result.assets.map((asset) => ({
|
|
86
|
+
assetPath: asset.assetPath,
|
|
87
|
+
assetType: asset.assetType,
|
|
88
|
+
referenceType: asset.referenceType,
|
|
89
|
+
filePath: asset.filePath,
|
|
90
|
+
line: asset.line,
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Ensure output directory exists
|
|
95
|
+
const cdnOutDir = path.dirname(cdnOutPath);
|
|
96
|
+
if (!fs.existsSync(cdnOutDir)) {
|
|
97
|
+
fs.mkdirSync(cdnOutDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(cdnOutPath, JSON.stringify(cdnList, null, 2), 'utf-8');
|
|
100
|
+
logger.success('CDN URLs extracted to ' + chalk.cyan(cdnOutPath));
|
|
101
|
+
logger.info(
|
|
102
|
+
' Legacy CDN URLs: ' +
|
|
103
|
+
chalk.red(String(result.legacyCdnUrls.length)) +
|
|
104
|
+
' Asset References: ' +
|
|
105
|
+
chalk.blue(String(result.assets.length))
|
|
106
|
+
);
|
|
45
107
|
}
|
|
46
108
|
} catch (err: any) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
109
|
+
spinner.fail('Scan failed');
|
|
110
|
+
logger.error(err.message);
|
|
111
|
+
process.exit(1);
|
|
50
112
|
}
|
|
51
113
|
});
|
|
52
114
|
}
|