ai-localize-cli 2.0.3 → 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/package.json CHANGED
@@ -1,25 +1,49 @@
1
1
  {
2
2
  "name": "ai-localize-cli",
3
- "version": "2.0.3",
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-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"
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);
@@ -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
- }
@@ -1,94 +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
- // 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
-
41
- const ss = createSpinner('Scanning for hardcoded text...').start();
42
- const scanner = new ProjectScanner(config);
43
- const scanResult = await scanner.scan();
44
- ss.succeed('Found ' + chalk.cyan(String(scanResult.detectedTexts.length)) + ' texts in ' + scanResult.scannedFiles + ' files');
45
-
46
- const uniqueTexts = deduplicateTexts(scanResult.detectedTexts);
47
- logger.info('Unique texts: ' + chalk.cyan(String(uniqueTexts.length)));
48
-
49
- const extractor = new LocaleExtractor({
50
- defaultLanguage: config.defaultLanguage,
51
- targetLanguages: config.targetLanguages,
52
- namespaceSplitting: structure === 'nested', // flat mode does not need namespace splitting
53
- staticKeys,
54
- });
55
- const { localeFiles, keyCount, namespaces } = extractor.extract(uniqueTexts);
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
- }
66
-
67
- if (!opts.dryRun) {
68
- const localesDir = path.resolve(cwd, config.localesDir);
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)));
79
- } else {
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
- });
87
- }
88
- } catch (err) {
89
- spinner.fail('Extraction failed');
90
- logger.error(String(err));
91
- process.exit(1);
92
- }
93
- });
94
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- };
@@ -1,5 +0,0 @@
1
- import ora, { type Ora } from 'ora';
2
-
3
- export function createSpinner(text: string): Ora {
4
- return ora({ text, color: 'cyan' });
5
- }
package/tsconfig.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "outDir": "./dist", "rootDir": "./src" },
4
- "include": ["src/**/*"],
5
- "exclude": ["node_modules", "dist"]
6
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'tsup';
2
-
3
- export default defineConfig({
4
- entry: ['src/cli.ts'],
5
- format: ['cjs'],
6
- target: 'node18',
7
- clean: true,
8
- banner: {
9
- js: '#!/usr/bin/env node',
10
- },
11
- });