ai-localize-cli 2.0.0 → 2.0.3
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 +59 -0
- package/dist/cli.js +112 -24
- package/package.json +10 -10
- package/src/commands/extract.ts +51 -12
- 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,64 @@
|
|
|
1
1
|
# ai-localize-cli
|
|
2
2
|
|
|
3
|
+
## 2.0.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **`keyStyle` config option** — `extract` and `full-migrate` commands now respect `keyStyle: "screaming_snake"` in `ai-localize.config.json`; all scanned hardcoded strings are extracted with UPPER_SNAKE_CASE keys matching the text value (e.g. `"Save Changes"` -> key `SAVE_CHANGES`, value `"Save Changes"`) instead of the default hierarchical path-based key style
|
|
8
|
+
- **`staticKeys` config option** — `extract` and `full-migrate` commands now inject `staticKeys` entries from config into every generated locale file regardless of scanning results; useful for enum labels, status codes, and other constant strings not present as literals in source files
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- ai-localize-scanner@2.0.3
|
|
14
|
+
- ai-localize-config@2.0.3
|
|
15
|
+
- ai-localize-shared@2.0.3
|
|
16
|
+
- ai-localize-aws-cloudfront@2.0.3
|
|
17
|
+
- ai-localize-codemods@2.0.3
|
|
18
|
+
- ai-localize-framework-detectors@2.0.3
|
|
19
|
+
- ai-localize-locale-engine@2.0.3
|
|
20
|
+
- ai-localize-reporting@2.0.3
|
|
21
|
+
- ai-localize-validators@2.0.3
|
|
22
|
+
|
|
23
|
+
## 2.0.2
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- **Flat locale layout in `extract` and `full-migrate` commands** — `--locale-structure flat` flag (or `localeStructure: "flat"` in config) now writes one `<lang>.json` per language with all namespace keys merged
|
|
28
|
+
- **Rich CLI report output** — `report` command now renders a structured terminal dashboard with coverage bars, top-file charts, missing-translation rankings, AST-context distribution, AI Insights, and actionable recommended next steps
|
|
29
|
+
- **HTML analytics dashboard** — `report --format html` now generates a fully self-contained interactive dashboard (light/dark theme, sortable tables, SVG donut chart, CSV/JSON export, print/PDF)
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- **Bug fix — locale key prefix** — resolved an issue where locale keys included ancestor path segments when `sourceRoot` was an absolute path
|
|
34
|
+
- **Bug fix — codemods config wired through** — `codemods` block in `ai-localize.config.json` is now applied by all codemod commands instead of being silently ignored
|
|
35
|
+
- Updated dependencies
|
|
36
|
+
- ai-localize-scanner@2.0.2
|
|
37
|
+
- ai-localize-config@2.0.2
|
|
38
|
+
- ai-localize-shared@2.0.2
|
|
39
|
+
- ai-localize-aws-cloudfront@2.0.2
|
|
40
|
+
- ai-localize-codemods@2.0.2
|
|
41
|
+
- ai-localize-framework-detectors@2.0.2
|
|
42
|
+
- ai-localize-locale-engine@2.0.2
|
|
43
|
+
- ai-localize-reporting@2.0.2
|
|
44
|
+
- ai-localize-validators@2.0.2
|
|
45
|
+
|
|
46
|
+
## 2.0.1
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- 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
|
|
51
|
+
- Updated dependencies
|
|
52
|
+
- ai-localize-scanner@2.0.1
|
|
53
|
+
- ai-localize-config@2.0.1
|
|
54
|
+
- ai-localize-shared@2.0.1
|
|
55
|
+
- ai-localize-aws-cloudfront@2.0.1
|
|
56
|
+
- ai-localize-codemods@2.0.1
|
|
57
|
+
- ai-localize-framework-detectors@2.0.1
|
|
58
|
+
- ai-localize-locale-engine@2.0.1
|
|
59
|
+
- ai-localize-reporting@2.0.1
|
|
60
|
+
- ai-localize-validators@2.0.1
|
|
61
|
+
|
|
3
62
|
## 2.0.0
|
|
4
63
|
|
|
5
64
|
### 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,15 @@ 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));
|
|
170
|
+
const staticKeys = config.staticKeys ?? {};
|
|
171
|
+
const staticKeyCount = Object.keys(staticKeys).length;
|
|
172
|
+
if (staticKeyCount > 0) {
|
|
173
|
+
logger.info(
|
|
174
|
+
"Static keys: " + import_chalk4.default.cyan(String(staticKeyCount)) + " (" + Object.keys(staticKeys).join(", ") + ")"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
135
177
|
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
136
178
|
const scanner = new import_ai_localize_scanner2.ProjectScanner(config);
|
|
137
179
|
const scanResult = await scanner.scan();
|
|
@@ -141,18 +183,35 @@ function extractCommand() {
|
|
|
141
183
|
const extractor = new import_ai_localize_locale_engine.LocaleExtractor({
|
|
142
184
|
defaultLanguage: config.defaultLanguage,
|
|
143
185
|
targetLanguages: config.targetLanguages,
|
|
144
|
-
namespaceSplitting:
|
|
186
|
+
namespaceSplitting: structure === "nested",
|
|
187
|
+
// flat mode does not need namespace splitting
|
|
188
|
+
staticKeys
|
|
145
189
|
});
|
|
146
190
|
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
147
|
-
logger.info(
|
|
148
|
-
|
|
191
|
+
logger.info(
|
|
192
|
+
"Keys generated: " + import_chalk4.default.green(String(keyCount)) + (staticKeyCount > 0 ? import_chalk4.default.dim(" (+ " + staticKeyCount + " static)") : "")
|
|
193
|
+
);
|
|
194
|
+
if (structure === "nested") {
|
|
195
|
+
logger.info("Namespaces: " + import_chalk4.default.cyan(namespaces.join(", ")));
|
|
196
|
+
}
|
|
149
197
|
if (!opts.dryRun) {
|
|
150
198
|
const localesDir = path2.resolve(cwd, config.localesDir);
|
|
151
|
-
const writer = new import_ai_localize_locale_engine.LocaleWriter({
|
|
199
|
+
const writer = new import_ai_localize_locale_engine.LocaleWriter({
|
|
200
|
+
localesDir,
|
|
201
|
+
merge: opts.merge !== false,
|
|
202
|
+
localeStructure: structure
|
|
203
|
+
});
|
|
152
204
|
const { written, created, merged } = writer.write(localeFiles);
|
|
153
|
-
logger.success(
|
|
205
|
+
logger.success(
|
|
206
|
+
"Wrote " + written.length + " locale files (" + created.length + " new, " + merged.length + " merged)"
|
|
207
|
+
);
|
|
208
|
+
written.forEach((f) => logger.info(" " + import_chalk4.default.gray(f)));
|
|
154
209
|
} else {
|
|
155
|
-
logger.info("Dry run
|
|
210
|
+
logger.info("Dry run \u2014 no files written");
|
|
211
|
+
localeFiles.forEach((lf) => {
|
|
212
|
+
const label = structure === "flat" ? `${lf.language}.json` : `${lf.language}/${lf.namespace}.json`;
|
|
213
|
+
logger.info(" " + import_chalk4.default.gray(label) + " \u2014 " + Object.keys(lf.entries).length + " keys");
|
|
214
|
+
});
|
|
156
215
|
}
|
|
157
216
|
} catch (err) {
|
|
158
217
|
spinner.fail("Extraction failed");
|
|
@@ -209,7 +268,7 @@ function validateCommand() {
|
|
|
209
268
|
var import_commander5 = require("commander");
|
|
210
269
|
var import_chalk6 = __toESM(require("chalk"));
|
|
211
270
|
var path4 = __toESM(require("path"));
|
|
212
|
-
var
|
|
271
|
+
var fs2 = __toESM(require("fs"));
|
|
213
272
|
var import_ai_localize_config5 = require("ai-localize-config");
|
|
214
273
|
var import_ai_localize_validators2 = require("ai-localize-validators");
|
|
215
274
|
var import_ai_localize_shared2 = require("ai-localize-shared");
|
|
@@ -232,7 +291,7 @@ function cleanupCommand() {
|
|
|
232
291
|
unusedKeys.slice(0, 20).forEach((k) => logger.dim("- " + k));
|
|
233
292
|
if (!opts.dryRun) {
|
|
234
293
|
const defaultDir = path4.join(localesDir, config.defaultLanguage);
|
|
235
|
-
const nsFiles =
|
|
294
|
+
const nsFiles = fs2.readdirSync(defaultDir).filter((f) => f.endsWith(".json"));
|
|
236
295
|
let removed = 0;
|
|
237
296
|
for (const nsFile of nsFiles) {
|
|
238
297
|
const ns = nsFile.replace(".json", "");
|
|
@@ -407,7 +466,7 @@ var import_ai_localize_scanner5 = require("ai-localize-scanner");
|
|
|
407
466
|
var import_ai_localize_validators3 = require("ai-localize-validators");
|
|
408
467
|
var import_ai_localize_reporting = require("ai-localize-reporting");
|
|
409
468
|
function reportCommand() {
|
|
410
|
-
return new import_commander9.Command("report").description("Generate
|
|
469
|
+
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
470
|
logger.header("ai-localize report");
|
|
412
471
|
const spinner = createSpinner("Loading configuration...").start();
|
|
413
472
|
try {
|
|
@@ -417,7 +476,7 @@ function reportCommand() {
|
|
|
417
476
|
const ss = createSpinner("Scanning project...").start();
|
|
418
477
|
const scanner = new import_ai_localize_scanner5.ProjectScanner(config);
|
|
419
478
|
const scanResult = await scanner.scan();
|
|
420
|
-
ss.succeed("Scanned " + scanResult.scannedFiles + " files");
|
|
479
|
+
ss.succeed("Scanned " + import_chalk10.default.cyan(String(scanResult.scannedFiles)) + " files");
|
|
421
480
|
const vs = createSpinner("Validating locale files...").start();
|
|
422
481
|
const validator = new import_ai_localize_validators3.LocaleValidator({
|
|
423
482
|
localesDir: path8.resolve(cwd, config.localesDir),
|
|
@@ -426,15 +485,21 @@ function reportCommand() {
|
|
|
426
485
|
targetLanguages: config.targetLanguages
|
|
427
486
|
});
|
|
428
487
|
const validationResult = validator.validate();
|
|
429
|
-
vs.succeed(
|
|
430
|
-
|
|
488
|
+
vs.succeed(
|
|
489
|
+
validationResult.valid ? import_chalk10.default.green("Locale files valid") : import_chalk10.default.yellow(validationResult.errors.length + " errors, " + validationResult.warnings.length + " warnings")
|
|
490
|
+
);
|
|
431
491
|
const rs = createSpinner("Building report...").start();
|
|
432
492
|
const report = (0, import_ai_localize_reporting.buildReport)({ scanResult, validationResult });
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
493
|
+
const outDir = path8.resolve(cwd, opts.outputDir);
|
|
494
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
495
|
+
const filename = opts.filename || "report-" + timestamp + ".html";
|
|
496
|
+
const htmlPath = path8.join(outDir, filename);
|
|
497
|
+
(0, import_ai_localize_reporting.generateHtmlReport)(report, htmlPath);
|
|
498
|
+
rs.succeed("HTML report written to " + import_chalk10.default.cyan(htmlPath));
|
|
437
499
|
(0, import_ai_localize_reporting.printCliSummary)(report);
|
|
500
|
+
logger.info(
|
|
501
|
+
"\n Open the report in your browser:\n " + import_chalk10.default.underline("file://" + htmlPath)
|
|
502
|
+
);
|
|
438
503
|
} catch (err) {
|
|
439
504
|
spinner.fail("Report generation failed");
|
|
440
505
|
logger.error(String(err));
|
|
@@ -454,7 +519,7 @@ var import_ai_localize_codemods2 = require("ai-localize-codemods");
|
|
|
454
519
|
var import_ai_localize_validators4 = require("ai-localize-validators");
|
|
455
520
|
var import_ai_localize_reporting2 = require("ai-localize-reporting");
|
|
456
521
|
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) => {
|
|
522
|
+
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
523
|
logger.header("ai-localize full-migrate");
|
|
459
524
|
const cwd = opts.cwd;
|
|
460
525
|
const dryRun = opts.dryRun;
|
|
@@ -462,28 +527,38 @@ function fullMigrateCommand() {
|
|
|
462
527
|
const cs = createSpinner("Loading configuration...").start();
|
|
463
528
|
const { config } = await (0, import_ai_localize_config10.loadConfig)(cwd);
|
|
464
529
|
cs.succeed("Configuration loaded");
|
|
530
|
+
const structure = config.localeStructure ?? "nested";
|
|
465
531
|
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
466
532
|
const scanner = new import_ai_localize_scanner6.ProjectScanner(config);
|
|
467
533
|
const scanResult = await scanner.scan();
|
|
468
|
-
ss.succeed(
|
|
534
|
+
ss.succeed(
|
|
535
|
+
"Found " + import_chalk11.default.cyan(String(scanResult.detectedTexts.length)) + " texts in " + scanResult.scannedFiles + " files"
|
|
536
|
+
);
|
|
469
537
|
const es = createSpinner("Extracting locale keys...").start();
|
|
470
538
|
const uniqueTexts = (0, import_ai_localize_locale_engine2.deduplicateTexts)(scanResult.detectedTexts);
|
|
471
539
|
const extractor = new import_ai_localize_locale_engine2.LocaleExtractor({
|
|
472
540
|
defaultLanguage: config.defaultLanguage,
|
|
473
541
|
targetLanguages: config.targetLanguages,
|
|
474
|
-
|
|
542
|
+
// Flat layout merges all keys into one file per language — no namespace splitting
|
|
543
|
+
namespaceSplitting: structure === "nested"
|
|
475
544
|
});
|
|
476
545
|
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
477
546
|
es.succeed("Generated " + import_chalk11.default.green(String(keyCount)) + " locale keys");
|
|
478
547
|
if (!dryRun) {
|
|
479
|
-
const writer = new import_ai_localize_locale_engine2.LocaleWriter({
|
|
548
|
+
const writer = new import_ai_localize_locale_engine2.LocaleWriter({
|
|
549
|
+
localesDir: path9.resolve(cwd, config.localesDir),
|
|
550
|
+
merge: true,
|
|
551
|
+
localeStructure: structure
|
|
552
|
+
});
|
|
480
553
|
writer.write(localeFiles);
|
|
481
554
|
}
|
|
482
555
|
if (opts.codemods !== false) {
|
|
483
556
|
const ms = createSpinner("Applying i18n codemods...").start();
|
|
484
|
-
const runner = new import_ai_localize_codemods2.CodemodRunner(config);
|
|
557
|
+
const runner = new import_ai_localize_codemods2.CodemodRunner(config, cwd);
|
|
485
558
|
const codemodResult = await runner.run(uniqueTexts, { dryRun });
|
|
486
|
-
ms.succeed(
|
|
559
|
+
ms.succeed(
|
|
560
|
+
"Codemods: " + import_chalk11.default.green(String(codemodResult.totalReplacements)) + " replacements in " + codemodResult.changedFiles + " files"
|
|
561
|
+
);
|
|
487
562
|
}
|
|
488
563
|
const vs = createSpinner("Validating locale files...").start();
|
|
489
564
|
const validator = new import_ai_localize_validators4.LocaleValidator({
|
|
@@ -493,10 +568,23 @@ function fullMigrateCommand() {
|
|
|
493
568
|
targetLanguages: config.targetLanguages
|
|
494
569
|
});
|
|
495
570
|
const validationResult = validator.validate();
|
|
496
|
-
vs.succeed(
|
|
571
|
+
vs.succeed(
|
|
572
|
+
validationResult.valid ? import_chalk11.default.green("Locale files valid!") : import_chalk11.default.yellow(
|
|
573
|
+
validationResult.errors.length + " errors, " + validationResult.warnings.length + " warnings"
|
|
574
|
+
)
|
|
575
|
+
);
|
|
497
576
|
if (opts.report !== false) {
|
|
577
|
+
const rs = createSpinner("Generating report...").start();
|
|
498
578
|
const report = (0, import_ai_localize_reporting2.buildReport)({ scanResult, validationResult });
|
|
579
|
+
const reportDir = path9.resolve(cwd, opts.reportDir);
|
|
580
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
581
|
+
const htmlPath = path9.join(reportDir, "report-" + timestamp + ".html");
|
|
582
|
+
(0, import_ai_localize_reporting2.generateHtmlReport)(report, htmlPath);
|
|
583
|
+
rs.succeed("Report saved to " + import_chalk11.default.cyan(htmlPath));
|
|
499
584
|
(0, import_ai_localize_reporting2.printCliSummary)(report);
|
|
585
|
+
logger.info(
|
|
586
|
+
"\n View report:\n " + import_chalk11.default.underline("file://" + htmlPath)
|
|
587
|
+
);
|
|
500
588
|
}
|
|
501
589
|
logger.success("Full migration complete!");
|
|
502
590
|
} 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.3",
|
|
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-
|
|
14
|
+
"ai-localize-framework-detectors": "2.0.3",
|
|
15
|
+
"ai-localize-shared": "2.0.3",
|
|
16
|
+
"ai-localize-config": "2.0.3",
|
|
17
|
+
"ai-localize-scanner": "2.0.3",
|
|
18
|
+
"ai-localize-aws-cloudfront": "2.0.3",
|
|
19
|
+
"ai-localize-validators": "2.0.3",
|
|
20
|
+
"ai-localize-locale-engine": "2.0.3",
|
|
21
|
+
"ai-localize-reporting": "2.0.3",
|
|
22
|
+
"ai-localize-codemods": "2.0.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/inquirer": "^9.0.7",
|
package/src/commands/extract.ts
CHANGED
|
@@ -21,30 +21,69 @@ export function extractCommand(): Command {
|
|
|
21
21
|
const { config } = await loadConfig(cwd);
|
|
22
22
|
spinner.succeed('Configuration loaded');
|
|
23
23
|
|
|
24
|
+
// Log locale structure in use
|
|
25
|
+
const structure = config.localeStructure ?? 'nested';
|
|
26
|
+
logger.info('Locale structure: ' + chalk.cyan(structure));
|
|
27
|
+
|
|
28
|
+
// Log static keys if configured
|
|
29
|
+
const staticKeys = config.staticKeys ?? {};
|
|
30
|
+
const staticKeyCount = Object.keys(staticKeys).length;
|
|
31
|
+
if (staticKeyCount > 0) {
|
|
32
|
+
logger.info(
|
|
33
|
+
'Static keys: ' +
|
|
34
|
+
chalk.cyan(String(staticKeyCount)) +
|
|
35
|
+
' (' +
|
|
36
|
+
Object.keys(staticKeys).join(', ') +
|
|
37
|
+
')'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
25
|
-
|
|
42
|
+
const scanner = new ProjectScanner(config);
|
|
26
43
|
const scanResult = await scanner.scan();
|
|
27
|
-
|
|
44
|
+
ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
47
|
+
logger.info('Unique texts: ' + chalk.cyan(String(uniqueTexts.length)));
|
|
31
48
|
|
|
32
49
|
const extractor = new LocaleExtractor({
|
|
33
|
-
|
|
50
|
+
defaultLanguage: config.defaultLanguage,
|
|
34
51
|
targetLanguages: config.targetLanguages,
|
|
35
|
-
|
|
52
|
+
namespaceSplitting: structure === 'nested', // flat mode does not need namespace splitting
|
|
53
|
+
staticKeys,
|
|
36
54
|
});
|
|
37
55
|
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
38
|
-
logger.info(
|
|
39
|
-
|
|
56
|
+
logger.info(
|
|
57
|
+
'Keys generated: ' +
|
|
58
|
+
chalk.green(String(keyCount)) +
|
|
59
|
+
(staticKeyCount > 0
|
|
60
|
+
? chalk.dim(' (+ ' + staticKeyCount + ' static)')
|
|
61
|
+
: '')
|
|
62
|
+
);
|
|
63
|
+
if (structure === 'nested') {
|
|
64
|
+
logger.info('Namespaces: ' + chalk.cyan(namespaces.join(', ')));
|
|
65
|
+
}
|
|
40
66
|
|
|
41
67
|
if (!opts.dryRun) {
|
|
42
68
|
const localesDir = path.resolve(cwd, config.localesDir);
|
|
43
|
-
const writer = new LocaleWriter({
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
const writer = new LocaleWriter({
|
|
70
|
+
localesDir,
|
|
71
|
+
merge: opts.merge !== false,
|
|
72
|
+
localeStructure: structure,
|
|
73
|
+
});
|
|
74
|
+
const { written, created, merged } = writer.write(localeFiles);
|
|
75
|
+
logger.success(
|
|
76
|
+
'Wrote ' + written.length + ' locale files (' + created.length + ' new, ' + merged.length + ' merged)'
|
|
77
|
+
);
|
|
78
|
+
written.forEach((f) => logger.info(' ' + chalk.gray(f)));
|
|
46
79
|
} else {
|
|
47
|
-
logger.info('Dry run
|
|
80
|
+
logger.info('Dry run — no files written');
|
|
81
|
+
localeFiles.forEach((lf) => {
|
|
82
|
+
const label = structure === 'flat'
|
|
83
|
+
? `${lf.language}.json`
|
|
84
|
+
: `${lf.language}/${lf.namespace}.json`;
|
|
85
|
+
logger.info(' ' + chalk.gray(label) + ' — ' + Object.keys(lf.entries).length + ' keys');
|
|
86
|
+
});
|
|
48
87
|
}
|
|
49
88
|
} catch (err) {
|
|
50
89
|
spinner.fail('Extraction failed');
|
|
@@ -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
|
}
|