ai-localize-cli 2.0.1 → 2.0.4
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 +43 -0
- package/dist/cli.js +12 -2
- package/package.json +35 -11
- package/src/cli.ts +0 -33
- package/src/commands/cleanup.ts +0 -56
- package/src/commands/extract.ts +0 -74
- package/src/commands/full-migrate.ts +0 -113
- package/src/commands/init.ts +0 -33
- package/src/commands/migrate-cdn.ts +0 -57
- package/src/commands/replace-cdn.ts +0 -55
- package/src/commands/report.ts +0 -70
- package/src/commands/scan.ts +0 -114
- package/src/commands/upload-assets.ts +0 -50
- package/src/commands/validate.ts +0 -56
- package/src/utils/logger.ts +0 -11
- package/src/utils/spinner.ts +0 -5
- package/tsconfig.json +0 -6
- package/tsup.config.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
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
|
+
|
|
3
46
|
## 2.0.1
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -167,6 +167,13 @@ function extractCommand() {
|
|
|
167
167
|
spinner.succeed("Configuration loaded");
|
|
168
168
|
const structure = config.localeStructure ?? "nested";
|
|
169
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
|
+
}
|
|
170
177
|
const ss = createSpinner("Scanning for hardcoded text...").start();
|
|
171
178
|
const scanner = new import_ai_localize_scanner2.ProjectScanner(config);
|
|
172
179
|
const scanResult = await scanner.scan();
|
|
@@ -176,11 +183,14 @@ function extractCommand() {
|
|
|
176
183
|
const extractor = new import_ai_localize_locale_engine.LocaleExtractor({
|
|
177
184
|
defaultLanguage: config.defaultLanguage,
|
|
178
185
|
targetLanguages: config.targetLanguages,
|
|
179
|
-
namespaceSplitting: structure === "nested"
|
|
186
|
+
namespaceSplitting: structure === "nested",
|
|
180
187
|
// flat mode does not need namespace splitting
|
|
188
|
+
staticKeys
|
|
181
189
|
});
|
|
182
190
|
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
183
|
-
logger.info(
|
|
191
|
+
logger.info(
|
|
192
|
+
"Keys generated: " + import_chalk4.default.green(String(keyCount)) + (staticKeyCount > 0 ? import_chalk4.default.dim(" (+ " + staticKeyCount + " static)") : "")
|
|
193
|
+
);
|
|
184
194
|
if (structure === "nested") {
|
|
185
195
|
logger.info("Namespaces: " + import_chalk4.default.cyan(namespaces.join(", ")));
|
|
186
196
|
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-cli",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "CLI for ai-localize-core: scan, extract, validate, migrate CDN",
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "CLI for ai-localize-core: scan, extract, validate, codemod and migrate CDN",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-localize": "./dist/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "./dist/cli.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"i18n",
|
|
16
|
+
"localization",
|
|
17
|
+
"l10n",
|
|
18
|
+
"internationalization",
|
|
19
|
+
"ai-localize",
|
|
20
|
+
"cli",
|
|
21
|
+
"scan",
|
|
22
|
+
"extract",
|
|
23
|
+
"codemod",
|
|
24
|
+
"cdn",
|
|
25
|
+
"cloudfront",
|
|
26
|
+
"react",
|
|
27
|
+
"vue",
|
|
28
|
+
"angular"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
9
33
|
"dependencies": {
|
|
10
34
|
"commander": "^12.0.0",
|
|
11
35
|
"chalk": "^5.3.0",
|
|
12
36
|
"ora": "^8.0.1",
|
|
13
37
|
"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-aws-cloudfront": "2.0.
|
|
21
|
-
"ai-localize-
|
|
22
|
-
"ai-localize-reporting": "2.0.
|
|
38
|
+
"ai-localize-config": "2.0.4",
|
|
39
|
+
"ai-localize-scanner": "2.0.4",
|
|
40
|
+
"ai-localize-codemods": "2.0.4",
|
|
41
|
+
"ai-localize-framework-detectors": "2.0.4",
|
|
42
|
+
"ai-localize-shared": "2.0.4",
|
|
43
|
+
"ai-localize-validators": "2.0.4",
|
|
44
|
+
"ai-localize-aws-cloudfront": "2.0.4",
|
|
45
|
+
"ai-localize-locale-engine": "2.0.4",
|
|
46
|
+
"ai-localize-reporting": "2.0.4"
|
|
23
47
|
},
|
|
24
48
|
"devDependencies": {
|
|
25
49
|
"@types/inquirer": "^9.0.7",
|
package/src/cli.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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);
|
package/src/commands/cleanup.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/extract.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
// 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();
|
|
29
|
-
const scanner = new ProjectScanner(config);
|
|
30
|
-
const scanResult = await scanner.scan();
|
|
31
|
-
ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
|
|
32
|
-
|
|
33
|
-
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
34
|
-
logger.info('Unique texts: ' + chalk.cyan(String(uniqueTexts.length)));
|
|
35
|
-
|
|
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
|
-
});
|
|
41
|
-
const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
|
|
42
|
-
logger.info('Keys generated: ' + chalk.green(String(keyCount)));
|
|
43
|
-
if (structure === 'nested') {
|
|
44
|
-
logger.info('Namespaces: ' + chalk.cyan(namespaces.join(', ')));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!opts.dryRun) {
|
|
48
|
-
const localesDir = path.resolve(cwd, config.localesDir);
|
|
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)));
|
|
59
|
-
} else {
|
|
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
|
-
});
|
|
67
|
-
}
|
|
68
|
-
} catch (err) {
|
|
69
|
-
spinner.fail('Extraction failed');
|
|
70
|
-
logger.error(String(err));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
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
|
-
.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');
|
|
23
|
-
const cwd = opts.cwd as string;
|
|
24
|
-
const dryRun = opts.dryRun as boolean;
|
|
25
|
-
try {
|
|
26
|
-
const cs = createSpinner('Loading configuration...').start();
|
|
27
|
-
const { config } = await loadConfig(cwd);
|
|
28
|
-
cs.succeed('Configuration loaded');
|
|
29
|
-
|
|
30
|
-
const structure = config.localeStructure ?? 'nested';
|
|
31
|
-
|
|
32
|
-
const ss = createSpinner('Scanning for hardcoded text...').start();
|
|
33
|
-
const scanner = new ProjectScanner(config);
|
|
34
|
-
const scanResult = await scanner.scan();
|
|
35
|
-
ss.succeed(
|
|
36
|
-
'Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) +
|
|
37
|
-
' texts in ' + scanResult.scannedFiles + ' files'
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const es = createSpinner('Extracting locale keys...').start();
|
|
41
|
-
const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
|
|
42
|
-
const extractor = new LocaleExtractor({
|
|
43
|
-
defaultLanguage: config.defaultLanguage,
|
|
44
|
-
targetLanguages: config.targetLanguages,
|
|
45
|
-
// Flat layout merges all keys into one file per language — no namespace splitting
|
|
46
|
-
namespaceSplitting: structure === 'nested',
|
|
47
|
-
});
|
|
48
|
-
const { localeFiles, keyCount } = extractor.extract(uniqueTexts);
|
|
49
|
-
es.succeed('Generated ' + chalk.green(String(keyCount)) + ' locale keys');
|
|
50
|
-
|
|
51
|
-
if (!dryRun) {
|
|
52
|
-
const writer = new LocaleWriter({
|
|
53
|
-
localesDir: path.resolve(cwd, config.localesDir),
|
|
54
|
-
merge: true,
|
|
55
|
-
localeStructure: structure,
|
|
56
|
-
});
|
|
57
|
-
writer.write(localeFiles);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (opts.codemods !== false) {
|
|
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
|
-
}
|
|
71
|
-
|
|
72
|
-
const vs = createSpinner('Validating locale files...').start();
|
|
73
|
-
const validator = new LocaleValidator({
|
|
74
|
-
localesDir: path.resolve(cwd, config.localesDir),
|
|
75
|
-
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
76
|
-
defaultLanguage: config.defaultLanguage,
|
|
77
|
-
targetLanguages: config.targetLanguages,
|
|
78
|
-
});
|
|
79
|
-
const validationResult = validator.validate();
|
|
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
|
-
);
|
|
88
|
-
|
|
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
|
-
}
|
|
106
|
-
|
|
107
|
-
logger.success('Full migration complete!');
|
|
108
|
-
} catch (err) {
|
|
109
|
-
logger.error('Migration failed: ' + String(err));
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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).framework;
|
|
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
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/report.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
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 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')
|
|
18
|
-
.action(async (opts) => {
|
|
19
|
-
logger.header('ai-localize report');
|
|
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
|
-
const ss = createSpinner('Scanning project...').start();
|
|
27
|
-
const scanner = new ProjectScanner(config);
|
|
28
|
-
const scanResult = await scanner.scan();
|
|
29
|
-
ss.succeed('Scanned ' + chalk.cyan(String(scanResult.scannedFiles)) + ' files');
|
|
30
|
-
|
|
31
|
-
const vs = createSpinner('Validating locale files...').start();
|
|
32
|
-
const validator = new LocaleValidator({
|
|
33
|
-
localesDir: path.resolve(cwd, config.localesDir),
|
|
34
|
-
sourceDir: path.resolve(cwd, config.sourceDir),
|
|
35
|
-
defaultLanguage: config.defaultLanguage,
|
|
36
|
-
targetLanguages: config.targetLanguages,
|
|
37
|
-
});
|
|
38
|
-
const validationResult = validator.validate();
|
|
39
|
-
vs.succeed(
|
|
40
|
-
validationResult.valid
|
|
41
|
-
? chalk.green('Locale files valid')
|
|
42
|
-
: chalk.yellow(validationResult.errors.length + ' errors, ' + validationResult.warnings.length + ' warnings')
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const rs = createSpinner('Building report...').start();
|
|
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);
|
|
53
|
-
|
|
54
|
-
generateHtmlReport(report, htmlPath);
|
|
55
|
-
rs.succeed('HTML report written to ' + chalk.cyan(htmlPath));
|
|
56
|
-
|
|
57
|
-
// Print CLI summary to terminal as well
|
|
58
|
-
printCliSummary(report);
|
|
59
|
-
|
|
60
|
-
logger.info(
|
|
61
|
-
'\n Open the report in your browser:\n' +
|
|
62
|
-
' ' + chalk.underline('file://' + htmlPath)
|
|
63
|
-
);
|
|
64
|
-
} catch (err) {
|
|
65
|
-
spinner.fail('Report generation failed');
|
|
66
|
-
logger.error(String(err));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
package/src/commands/scan.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
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 { ProjectScanner, GitScanner } from 'ai-localize-scanner';
|
|
9
|
-
import { writeJson } from 'ai-localize-shared';
|
|
10
|
-
|
|
11
|
-
export function scanCommand(): Command {
|
|
12
|
-
return new Command('scan')
|
|
13
|
-
.description('Scan project for hardcoded texts and asset references')
|
|
14
|
-
.option('--incremental', 'Scan only changed files based on git diff')
|
|
15
|
-
.option('--staged', 'Scan only git staged files')
|
|
16
|
-
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
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)')
|
|
19
|
-
.action(async (opts) => {
|
|
20
|
-
logger.header('ai-localize scan');
|
|
21
|
-
const spinner = createSpinner('Loading configuration...').start();
|
|
22
|
-
try {
|
|
23
|
-
const cwd = opts.cwd as string;
|
|
24
|
-
const { config } = await loadConfig(cwd);
|
|
25
|
-
spinner.succeed('Configuration loaded');
|
|
26
|
-
|
|
27
|
-
let files: string[] | undefined;
|
|
28
|
-
if (opts.staged || opts.incremental) {
|
|
29
|
-
const git = new GitScanner(cwd);
|
|
30
|
-
files = opts.staged ? git.getStagedFiles() : git.getChangedFiles();
|
|
31
|
-
logger.info('Changed files: ' + chalk.cyan(String(files.length)));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const scanSpinner = createSpinner('Scanning files...').start();
|
|
35
|
-
const scanner = new ProjectScanner(config);
|
|
36
|
-
const result = await scanner.scan({ files });
|
|
37
|
-
scanSpinner.succeed('Scanned ' + chalk.cyan(String(result.scannedFiles)) + ' files in ' + chalk.cyan(result.duration + 'ms'));
|
|
38
|
-
|
|
39
|
-
logger.info('Hardcoded texts: ' + chalk.yellow(String(result.detectedTexts.length)));
|
|
40
|
-
logger.info('Asset references: ' + chalk.blue(String(result.assets.length)));
|
|
41
|
-
logger.info('Legacy CDN URLs: ' + chalk.red(String(result.legacyCdnUrls.length)));
|
|
42
|
-
|
|
43
|
-
if (opts.output) {
|
|
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
|
-
);
|
|
107
|
-
}
|
|
108
|
-
} catch (err: any) {
|
|
109
|
-
spinner.fail('Scan failed');
|
|
110
|
-
logger.error(err.message);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/validate.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/logger.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
};
|
package/src/utils/spinner.ts
DELETED
package/tsconfig.json
DELETED