i18nsmith 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +180 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/backup.d.ts +6 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +85 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +151 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +235 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/debug-patterns.d.ts +3 -0
- package/dist/commands/debug-patterns.d.ts.map +1 -0
- package/dist/commands/debug-patterns.js +192 -0
- package/dist/commands/debug-patterns.js.map +1 -0
- package/dist/commands/debug-patterns.test.d.ts +2 -0
- package/dist/commands/debug-patterns.test.d.ts.map +1 -0
- package/dist/commands/debug-patterns.test.js +109 -0
- package/dist/commands/debug-patterns.test.js.map +1 -0
- package/dist/commands/diagnose.d.ts +3 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +117 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +450 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +74 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/install-hooks.d.ts +3 -0
- package/dist/commands/install-hooks.d.ts.map +1 -0
- package/dist/commands/install-hooks.js +52 -0
- package/dist/commands/install-hooks.js.map +1 -0
- package/dist/commands/preflight.d.ts +7 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +417 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preflight.test.d.ts +5 -0
- package/dist/commands/preflight.test.d.ts.map +1 -0
- package/dist/commands/preflight.test.js +108 -0
- package/dist/commands/preflight.test.js.map +1 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.d.ts.map +1 -0
- package/dist/commands/rename.js +204 -0
- package/dist/commands/rename.js.map +1 -0
- package/dist/commands/scaffold-adapter.d.ts +3 -0
- package/dist/commands/scaffold-adapter.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.js +204 -0
- package/dist/commands/scaffold-adapter.js.map +1 -0
- package/dist/commands/scaffold-adapter.test.d.ts +2 -0
- package/dist/commands/scaffold-adapter.test.d.ts.map +1 -0
- package/dist/commands/scaffold-adapter.test.js +102 -0
- package/dist/commands/scaffold-adapter.test.js.map +1 -0
- package/dist/commands/scan.d.ts +3 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +93 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/sync-seed.test.d.ts +2 -0
- package/dist/commands/sync-seed.test.d.ts.map +1 -0
- package/dist/commands/sync-seed.test.js +86 -0
- package/dist/commands/sync-seed.test.js.map +1 -0
- package/dist/commands/sync.d.ts +3 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +590 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/transform.d.ts +3 -0
- package/dist/commands/transform.d.ts.map +1 -0
- package/dist/commands/transform.js +114 -0
- package/dist/commands/transform.js.map +1 -0
- package/dist/commands/translate/csv-handler.d.ts +21 -0
- package/dist/commands/translate/csv-handler.d.ts.map +1 -0
- package/dist/commands/translate/csv-handler.js +270 -0
- package/dist/commands/translate/csv-handler.js.map +1 -0
- package/dist/commands/translate/executor.d.ts +31 -0
- package/dist/commands/translate/executor.d.ts.map +1 -0
- package/dist/commands/translate/executor.js +117 -0
- package/dist/commands/translate/executor.js.map +1 -0
- package/dist/commands/translate/index.d.ts +10 -0
- package/dist/commands/translate/index.d.ts.map +1 -0
- package/dist/commands/translate/index.js +170 -0
- package/dist/commands/translate/index.js.map +1 -0
- package/dist/commands/translate/reporter.d.ts +29 -0
- package/dist/commands/translate/reporter.d.ts.map +1 -0
- package/dist/commands/translate/reporter.js +103 -0
- package/dist/commands/translate/reporter.js.map +1 -0
- package/dist/commands/translate/types.d.ts +50 -0
- package/dist/commands/translate/types.d.ts.map +1 -0
- package/dist/commands/translate/types.js +5 -0
- package/dist/commands/translate/types.js.map +1 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +7 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/translate.test.d.ts +2 -0
- package/dist/commands/translate.test.d.ts.map +1 -0
- package/dist/commands/translate.test.js +118 -0
- package/dist/commands/translate.test.js.map +1 -0
- package/dist/e2e.test.d.ts +6 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +376 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +6 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +320 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/utils/diagnostics-exit.d.ts +12 -0
- package/dist/utils/diagnostics-exit.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.js +49 -0
- package/dist/utils/diagnostics-exit.js.map +1 -0
- package/dist/utils/diagnostics-exit.test.d.ts +2 -0
- package/dist/utils/diagnostics-exit.test.d.ts.map +1 -0
- package/dist/utils/diagnostics-exit.test.js +40 -0
- package/dist/utils/diagnostics-exit.test.js.map +1 -0
- package/dist/utils/diff-utils.d.ts +4 -0
- package/dist/utils/diff-utils.d.ts.map +1 -0
- package/dist/utils/diff-utils.js +30 -0
- package/dist/utils/diff-utils.js.map +1 -0
- package/dist/utils/diff-utils.test.d.ts +2 -0
- package/dist/utils/diff-utils.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.test.js +30 -0
- package/dist/utils/diff-utils.test.js.map +1 -0
- package/dist/utils/exit-codes.d.ts +142 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +168 -0
- package/dist/utils/exit-codes.js.map +1 -0
- package/dist/utils/package-manager.d.ts +4 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +40 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/pkg.d.ts +3 -0
- package/dist/utils/pkg.d.ts.map +1 -0
- package/dist/utils/pkg.js +24 -0
- package/dist/utils/pkg.js.map +1 -0
- package/dist/utils/provider-injector.d.ts +36 -0
- package/dist/utils/provider-injector.d.ts.map +1 -0
- package/dist/utils/provider-injector.js +223 -0
- package/dist/utils/provider-injector.js.map +1 -0
- package/dist/utils/provider-injector.test.d.ts +2 -0
- package/dist/utils/provider-injector.test.d.ts.map +1 -0
- package/dist/utils/provider-injector.test.js +67 -0
- package/dist/utils/provider-injector.test.js.map +1 -0
- package/dist/utils/scaffold.d.ts +20 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +197 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/package.json +35 -0
- package/src/commands/audit.ts +234 -0
- package/src/commands/backup.ts +96 -0
- package/src/commands/check.ts +191 -0
- package/src/commands/config.ts +263 -0
- package/src/commands/debug-patterns.test.ts +134 -0
- package/src/commands/debug-patterns.ts +257 -0
- package/src/commands/diagnose.ts +136 -0
- package/src/commands/init.test.ts +82 -0
- package/src/commands/init.ts +536 -0
- package/src/commands/install-hooks.ts +66 -0
- package/src/commands/preflight.test.ts +139 -0
- package/src/commands/preflight.ts +488 -0
- package/src/commands/rename.ts +264 -0
- package/src/commands/scaffold-adapter.test.ts +110 -0
- package/src/commands/scaffold-adapter.ts +250 -0
- package/src/commands/scan.ts +125 -0
- package/src/commands/sync-seed.test.ts +116 -0
- package/src/commands/sync.ts +736 -0
- package/src/commands/transform.ts +151 -0
- package/src/commands/translate/README.md +75 -0
- package/src/commands/translate/csv-handler.ts +301 -0
- package/src/commands/translate/executor.ts +188 -0
- package/src/commands/translate/index.ts +220 -0
- package/src/commands/translate/reporter.ts +138 -0
- package/src/commands/translate/types.ts +56 -0
- package/src/commands/translate.test.ts +173 -0
- package/src/commands/translate.ts +6 -0
- package/src/e2e.test.ts +479 -0
- package/src/fixtures/README.md +61 -0
- package/src/fixtures/basic-react/i18n.config.json +15 -0
- package/src/fixtures/basic-react/locales/de.json +8 -0
- package/src/fixtures/basic-react/locales/en.json +8 -0
- package/src/fixtures/basic-react/locales/fr.json +8 -0
- package/src/fixtures/basic-react/src/App.tsx +15 -0
- package/src/fixtures/basic-react/src/Messages.tsx +12 -0
- package/src/fixtures/nested-locales/i18n.config.json +9 -0
- package/src/fixtures/nested-locales/locales/en.json +23 -0
- package/src/fixtures/nested-locales/locales/fr.json +23 -0
- package/src/fixtures/nested-locales/src/HomePage.tsx +13 -0
- package/src/fixtures/suspicious-keys/i18n.config.json +9 -0
- package/src/fixtures/suspicious-keys/locales/en.json +11 -0
- package/src/fixtures/suspicious-keys/locales/fr.json +11 -0
- package/src/fixtures/suspicious-keys/src/BadKeys.tsx +19 -0
- package/src/index.ts +43 -0
- package/src/integration.test.ts +438 -0
- package/src/utils/diagnostics-exit.test.ts +47 -0
- package/src/utils/diagnostics-exit.ts +63 -0
- package/src/utils/diff-utils.test.ts +36 -0
- package/src/utils/diff-utils.ts +42 -0
- package/src/utils/exit-codes.ts +201 -0
- package/src/utils/package-manager.ts +44 -0
- package/src/utils/pkg.ts +23 -0
- package/src/utils/provider-injector.test.ts +79 -0
- package/src/utils/provider-injector.ts +315 -0
- package/src/utils/scaffold.ts +240 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { listBackups, restoreBackup } from '@i18nsmith/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers backup-related commands (backup-list, backup-restore)
|
|
8
|
+
*/
|
|
9
|
+
export function registerBackup(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command('backup-list')
|
|
12
|
+
.description('List available locale file backups')
|
|
13
|
+
.option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
|
|
14
|
+
.action(async (options: { backupDir?: string }) => {
|
|
15
|
+
try {
|
|
16
|
+
const workspaceRoot = process.cwd();
|
|
17
|
+
const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
|
|
18
|
+
|
|
19
|
+
if (backups.length === 0) {
|
|
20
|
+
console.log(chalk.yellow('No backups found.'));
|
|
21
|
+
console.log(chalk.gray('Backups are created automatically when using --write --prune'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.blue(`Found ${backups.length} backup(s):\n`));
|
|
26
|
+
|
|
27
|
+
for (const backup of backups) {
|
|
28
|
+
const date = new Date(backup.createdAt);
|
|
29
|
+
const formattedDate = date.toLocaleString();
|
|
30
|
+
console.log(` ${chalk.cyan(backup.timestamp)} ${formattedDate} (${backup.fileCount} files)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.gray(`\nRestore a backup with: i18nsmith backup-restore <timestamp>`));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(chalk.red('Error listing backups:'), err instanceof Error ? err.message : err);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('backup-restore')
|
|
42
|
+
.description('Restore locale files from a previous backup')
|
|
43
|
+
.argument('<timestamp>', 'Backup timestamp (from backup-list) or "latest" for most recent')
|
|
44
|
+
.option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
|
|
45
|
+
.action(async (timestamp: string, options: { backupDir?: string }) => {
|
|
46
|
+
try {
|
|
47
|
+
const workspaceRoot = process.cwd();
|
|
48
|
+
const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
|
|
49
|
+
|
|
50
|
+
if (backups.length === 0) {
|
|
51
|
+
console.error(chalk.red('No backups found.'));
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let targetBackup = timestamp === 'latest'
|
|
57
|
+
? backups[0]
|
|
58
|
+
: backups.find((b) => b.timestamp === timestamp);
|
|
59
|
+
|
|
60
|
+
if (!targetBackup) {
|
|
61
|
+
console.error(chalk.red(`Backup not found: ${timestamp}`));
|
|
62
|
+
console.log(chalk.gray('Available backups:'));
|
|
63
|
+
for (const b of backups.slice(0, 5)) {
|
|
64
|
+
console.log(` ${b.timestamp}`);
|
|
65
|
+
}
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Confirm restore
|
|
71
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'confirmed',
|
|
75
|
+
message: `Restore ${targetBackup.fileCount} locale files from backup ${targetBackup.timestamp}? This will overwrite current locale files.`,
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
if (!confirmed) {
|
|
81
|
+
console.log(chalk.yellow('Restore cancelled.'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await restoreBackup(targetBackup.path, workspaceRoot);
|
|
86
|
+
|
|
87
|
+
console.log(chalk.green(`\n✅ ${result.summary}`));
|
|
88
|
+
for (const file of result.restored) {
|
|
89
|
+
console.log(chalk.gray(` Restored: ${file}`));
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(chalk.red('Error restoring backup:'), err instanceof Error ? err.message : err);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
import { loadConfigWithMeta, CheckRunner } from '@i18nsmith/core';
|
|
6
|
+
import type { CheckSummary } from '@i18nsmith/core';
|
|
7
|
+
import { printLocaleDiffs } from '../utils/diff-utils.js';
|
|
8
|
+
import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
|
|
9
|
+
import { CHECK_EXIT_CODES } from '../utils/exit-codes.js';
|
|
10
|
+
|
|
11
|
+
interface CheckCommandOptions {
|
|
12
|
+
config?: string;
|
|
13
|
+
json?: boolean;
|
|
14
|
+
target?: string[];
|
|
15
|
+
report?: string;
|
|
16
|
+
listFiles?: boolean;
|
|
17
|
+
include?: string[];
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
failOn?: 'none' | 'conflicts' | 'warnings';
|
|
20
|
+
assume?: string[];
|
|
21
|
+
assumeGlobs?: string[];
|
|
22
|
+
validateInterpolations?: boolean;
|
|
23
|
+
emptyValues?: boolean;
|
|
24
|
+
diff?: boolean;
|
|
25
|
+
invalidateCache?: boolean;
|
|
26
|
+
preferDiagnosticsExit?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const collectAssumedKeys = (value: string, previous: string[]) => {
|
|
30
|
+
const tokens = value
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((token) => token.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
return [...previous, ...tokens];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const collectTargetPatterns = (value: string | string[], previous: string[]) => {
|
|
38
|
+
const list = Array.isArray(value) ? value : [value];
|
|
39
|
+
const tokens = list
|
|
40
|
+
.flatMap((entry) => entry.split(','))
|
|
41
|
+
.map((token) => token.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
return [...previous, ...tokens];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function printCheckSummary(summary: CheckSummary) {
|
|
47
|
+
const report = summary.diagnostics;
|
|
48
|
+
console.log(chalk.green(`Locales directory: ${report.localesDir}`));
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.gray(
|
|
51
|
+
`Detected locales: ${report.detectedLocales.length ? report.detectedLocales.join(', ') : 'none'}`
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.gray(
|
|
56
|
+
`Runtime packages: ${report.runtimePackages.length ? report.runtimePackages.map((pkg) => pkg.name).join(', ') : 'none'}`
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
chalk.gray(
|
|
61
|
+
`Translation references scanned: ${summary.sync.references.length} across ${summary.sync.filesScanned} file${summary.sync.filesScanned === 1 ? '' : 's'}`
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (summary.actionableItems.length) {
|
|
66
|
+
console.log(chalk.blue('\nActionable items'));
|
|
67
|
+
summary.actionableItems.slice(0, 25).forEach((item) => {
|
|
68
|
+
const label = formatSeverityLabel(item.severity);
|
|
69
|
+
console.log(` • [${label}] ${item.message}`);
|
|
70
|
+
});
|
|
71
|
+
if (summary.actionableItems.length > 25) {
|
|
72
|
+
console.log(chalk.gray(` ...and ${summary.actionableItems.length - 25} more.`));
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
console.log(chalk.green('\nNo actionable issues detected.'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (summary.suggestedCommands.length) {
|
|
79
|
+
console.log(chalk.blue('\nSuggested commands'));
|
|
80
|
+
summary.suggestedCommands.forEach((suggestion) => {
|
|
81
|
+
const label = formatSeverityLabel(suggestion.severity);
|
|
82
|
+
console.log(` • [${label}] ${suggestion.label}`);
|
|
83
|
+
console.log(` ${chalk.cyan(suggestion.command)}`);
|
|
84
|
+
console.log(chalk.gray(` ${suggestion.reason}`));
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.gray('\nNo automated suggestions—review actionable items above.'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatSeverityLabel(severity: 'info' | 'warn' | 'error'): string {
|
|
92
|
+
if (severity === 'error') {
|
|
93
|
+
return chalk.red('ERROR');
|
|
94
|
+
}
|
|
95
|
+
if (severity === 'warn') {
|
|
96
|
+
return chalk.yellow('WARN');
|
|
97
|
+
}
|
|
98
|
+
return chalk.cyan('INFO');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function registerCheck(program: Command) {
|
|
102
|
+
program
|
|
103
|
+
.command('check')
|
|
104
|
+
.description('Run diagnostics plus a sync dry-run for a consolidated health report')
|
|
105
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
|
|
106
|
+
.option('--json', 'Print raw JSON results', false)
|
|
107
|
+
.option('--report <path>', 'Write JSON summary to a file (for CI or editors)')
|
|
108
|
+
.option('--fail-on <level>', 'Failure threshold: none | conflicts | warnings', 'conflicts')
|
|
109
|
+
.option('--assume <keys...>', 'List of runtime keys to assume (comma-separated)', collectAssumedKeys, [])
|
|
110
|
+
.option('--assume-globs <patterns...>', 'Glob patterns for dynamic key namespaces (e.g., errors.*, navigation.**)', collectTargetPatterns, [])
|
|
111
|
+
.option('--validate-interpolations', 'Validate interpolation placeholders across locales', false)
|
|
112
|
+
.option('--no-empty-values', 'Treat empty or placeholder locale values as failures')
|
|
113
|
+
.option('--diff', 'Include locale diff previews for missing/unused key fixes', false)
|
|
114
|
+
.option('--invalidate-cache', 'Ignore cached sync analysis and rescan all source files', false)
|
|
115
|
+
.option('--target <pattern...>', 'Limit translation reference scanning to specific files or patterns', collectTargetPatterns, [])
|
|
116
|
+
.option('--prefer-diagnostics-exit', 'Prefer diagnostics exit codes when --fail-on=conflicts and blocking conflicts exist', false)
|
|
117
|
+
.action(async (options: CheckCommandOptions) => {
|
|
118
|
+
console.log(chalk.blue('Running guided repository health check...'));
|
|
119
|
+
try {
|
|
120
|
+
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
121
|
+
|
|
122
|
+
// Inform user if config was found in a parent directory
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
if (projectRoot !== cwd) {
|
|
125
|
+
console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
|
|
126
|
+
console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Merge --assume-globs with config
|
|
130
|
+
if (options.assumeGlobs?.length) {
|
|
131
|
+
config.sync = config.sync ?? {};
|
|
132
|
+
config.sync.dynamicKeyGlobs = [
|
|
133
|
+
...(config.sync.dynamicKeyGlobs ?? []),
|
|
134
|
+
...options.assumeGlobs,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
const runner = new CheckRunner(config, { workspaceRoot: projectRoot });
|
|
138
|
+
const summary = await runner.run({
|
|
139
|
+
assumedKeys: options.assume,
|
|
140
|
+
validateInterpolations: options.validateInterpolations,
|
|
141
|
+
emptyValuePolicy: options.emptyValues === false ? 'fail' : undefined,
|
|
142
|
+
diff: options.diff,
|
|
143
|
+
targets: options.target,
|
|
144
|
+
invalidateCache: options.invalidateCache,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (options.report) {
|
|
148
|
+
const outputPath = path.resolve(process.cwd(), options.report);
|
|
149
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
150
|
+
await fs.writeFile(outputPath, JSON.stringify(summary, null, 2));
|
|
151
|
+
console.log(chalk.green(`Health report written to ${outputPath}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.json) {
|
|
155
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
156
|
+
} else {
|
|
157
|
+
printCheckSummary(summary);
|
|
158
|
+
if (options.diff) {
|
|
159
|
+
printLocaleDiffs(summary.sync.diffs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If diagnostics discovered blocking conflicts, prefer the diagnostics' exit
|
|
164
|
+
// signal so CI can branch on specific failure modes (missing source locale,
|
|
165
|
+
// invalid JSON, etc.). This mirrors `i18nsmith diagnose` behavior.
|
|
166
|
+
// Only when --prefer-diagnostics-exit is true and --fail-on=conflicts.
|
|
167
|
+
const diagExit = getDiagnosisExitSignal(summary.diagnostics);
|
|
168
|
+
if (diagExit && options.preferDiagnosticsExit && options.failOn === 'conflicts') {
|
|
169
|
+
console.error(chalk.red(`\nBlocking diagnostic conflict detected: ${diagExit.reason}`));
|
|
170
|
+
console.error(chalk.red(`Exit code ${diagExit.code}`));
|
|
171
|
+
process.exitCode = diagExit.code;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const failMode = (options.failOn ?? 'conflicts').toLowerCase();
|
|
176
|
+
const hasErrors = summary.actionableItems.some((item) => item.severity === 'error');
|
|
177
|
+
const hasWarnings = summary.actionableItems.some((item) => item.severity === 'warn');
|
|
178
|
+
|
|
179
|
+
if (failMode === 'conflicts' && hasErrors) {
|
|
180
|
+
console.error(chalk.red('\nBlocking issues detected. Resolve the actionable errors above.'));
|
|
181
|
+
process.exitCode = CHECK_EXIT_CODES.CONFLICTS;
|
|
182
|
+
} else if (failMode === 'warnings' && (hasErrors || hasWarnings)) {
|
|
183
|
+
console.error(chalk.red('\nWarnings detected. Use --fail-on conflicts to limit failures to blocking issues.'));
|
|
184
|
+
process.exitCode = CHECK_EXIT_CODES.WARNINGS;
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(chalk.red('Check failed:'), (error as Error).message);
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
import { loadConfigWithMeta, DEFAULT_CONFIG_FILENAME } from '@i18nsmith/core';
|
|
6
|
+
|
|
7
|
+
interface ConfigCommandOptions {
|
|
8
|
+
config?: string;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ConfigSetOptions extends ConfigCommandOptions {
|
|
13
|
+
key: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ConfigGetOptions extends ConfigCommandOptions {
|
|
18
|
+
key: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a dot-notation key path into segments.
|
|
23
|
+
* e.g., 'translationAdapter.module' => ['translationAdapter', 'module']
|
|
24
|
+
*/
|
|
25
|
+
function parseKeyPath(keyPath: string): string[] {
|
|
26
|
+
return keyPath.split('.').filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a nested value from an object using a key path.
|
|
31
|
+
*/
|
|
32
|
+
function getNestedValue(obj: Record<string, unknown>, keyPath: string[]): unknown {
|
|
33
|
+
let current: unknown = obj;
|
|
34
|
+
for (const key of keyPath) {
|
|
35
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
current = (current as Record<string, unknown>)[key];
|
|
39
|
+
}
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set a nested value in an object using a key path.
|
|
45
|
+
* Creates intermediate objects as needed.
|
|
46
|
+
*/
|
|
47
|
+
function setNestedValue(obj: Record<string, unknown>, keyPath: string[], value: unknown): void {
|
|
48
|
+
let current = obj;
|
|
49
|
+
for (let i = 0; i < keyPath.length - 1; i++) {
|
|
50
|
+
const key = keyPath[i];
|
|
51
|
+
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
52
|
+
current[key] = {};
|
|
53
|
+
}
|
|
54
|
+
current = current[key] as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
const finalKey = keyPath[keyPath.length - 1];
|
|
57
|
+
current[finalKey] = value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a value string into an appropriate type.
|
|
62
|
+
* Handles booleans, numbers, arrays, and strings.
|
|
63
|
+
*/
|
|
64
|
+
function parseValue(valueStr: string): unknown {
|
|
65
|
+
// Boolean
|
|
66
|
+
if (valueStr === 'true') return true;
|
|
67
|
+
if (valueStr === 'false') return false;
|
|
68
|
+
|
|
69
|
+
// Null
|
|
70
|
+
if (valueStr === 'null') return null;
|
|
71
|
+
|
|
72
|
+
// Number
|
|
73
|
+
if (/^-?\d+(\.\d+)?$/.test(valueStr)) {
|
|
74
|
+
return parseFloat(valueStr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// JSON array or object
|
|
78
|
+
if ((valueStr.startsWith('[') && valueStr.endsWith(']')) ||
|
|
79
|
+
(valueStr.startsWith('{') && valueStr.endsWith('}'))) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(valueStr);
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall through to string
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Remove surrounding quotes if present
|
|
88
|
+
if ((valueStr.startsWith('"') && valueStr.endsWith('"')) ||
|
|
89
|
+
(valueStr.startsWith("'") && valueStr.endsWith("'"))) {
|
|
90
|
+
return valueStr.slice(1, -1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return valueStr;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the raw config file content (unparsed JSON with comments preserved formatting).
|
|
98
|
+
*/
|
|
99
|
+
async function readRawConfig(configPath: string): Promise<{ content: string; parsed: Record<string, unknown> }> {
|
|
100
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
101
|
+
const parsed = JSON.parse(content);
|
|
102
|
+
return { content, parsed };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write config back to file with nice formatting.
|
|
107
|
+
*/
|
|
108
|
+
async function writeConfig(configPath: string, config: Record<string, unknown>): Promise<void> {
|
|
109
|
+
const content = JSON.stringify(config, null, 2);
|
|
110
|
+
await fs.writeFile(configPath, content, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function registerConfig(program: Command) {
|
|
114
|
+
const configCmd = program
|
|
115
|
+
.command('config')
|
|
116
|
+
.description('View or modify i18nsmith configuration');
|
|
117
|
+
|
|
118
|
+
// Subcommand: config get <key>
|
|
119
|
+
configCmd
|
|
120
|
+
.command('get <key>')
|
|
121
|
+
.description('Get a configuration value by key path (e.g., translationAdapter.module)')
|
|
122
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
123
|
+
.option('--json', 'Output as JSON', false)
|
|
124
|
+
.action(async (key: string, options: ConfigGetOptions) => {
|
|
125
|
+
try {
|
|
126
|
+
const { config, configPath } = await loadConfigWithMeta(options.config);
|
|
127
|
+
const keyPath = parseKeyPath(key);
|
|
128
|
+
const value = getNestedValue(config as unknown as Record<string, unknown>, keyPath);
|
|
129
|
+
|
|
130
|
+
if (value === undefined) {
|
|
131
|
+
console.log(chalk.yellow(`Key "${key}" not found in config`));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.json) {
|
|
137
|
+
console.log(JSON.stringify({ key, value }, null, 2));
|
|
138
|
+
} else {
|
|
139
|
+
if (typeof value === 'object' && value !== null) {
|
|
140
|
+
console.log(JSON.stringify(value, null, 2));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(String(value));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(chalk.red('Failed to get config:'), (error as Error).message);
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Subcommand: config set <key> <value>
|
|
152
|
+
configCmd
|
|
153
|
+
.command('set <key> <value>')
|
|
154
|
+
.description('Set a configuration value by key path (e.g., translationAdapter.module "src/i18n.ts")')
|
|
155
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
156
|
+
.option('--json', 'Output result as JSON', false)
|
|
157
|
+
.action(async (key: string, value: string, options: ConfigSetOptions) => {
|
|
158
|
+
try {
|
|
159
|
+
const { configPath } = await loadConfigWithMeta(options.config);
|
|
160
|
+
const { parsed } = await readRawConfig(configPath);
|
|
161
|
+
|
|
162
|
+
const keyPath = parseKeyPath(key);
|
|
163
|
+
const parsedValue = parseValue(value);
|
|
164
|
+
|
|
165
|
+
setNestedValue(parsed, keyPath, parsedValue);
|
|
166
|
+
|
|
167
|
+
await writeConfig(configPath, parsed);
|
|
168
|
+
|
|
169
|
+
if (options.json) {
|
|
170
|
+
console.log(JSON.stringify({ key, value: parsedValue, configPath }, null, 2));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(chalk.green(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`));
|
|
173
|
+
console.log(chalk.dim(` Updated: ${configPath}`));
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(chalk.red('Failed to set config:'), (error as Error).message);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Subcommand: config list
|
|
182
|
+
configCmd
|
|
183
|
+
.command('list')
|
|
184
|
+
.description('List all configuration values')
|
|
185
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
186
|
+
.option('--json', 'Output as JSON', false)
|
|
187
|
+
.action(async (options: ConfigCommandOptions) => {
|
|
188
|
+
try {
|
|
189
|
+
const { config, configPath } = await loadConfigWithMeta(options.config);
|
|
190
|
+
|
|
191
|
+
if (options.json) {
|
|
192
|
+
console.log(JSON.stringify(config, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.blue(`Configuration from: ${configPath}`));
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(JSON.stringify(config, null, 2));
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(chalk.red('Failed to read config:'), (error as Error).message);
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Subcommand: config path
|
|
205
|
+
configCmd
|
|
206
|
+
.command('path')
|
|
207
|
+
.description('Print the path to the active config file')
|
|
208
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
209
|
+
.action(async (options: ConfigCommandOptions) => {
|
|
210
|
+
try {
|
|
211
|
+
const { configPath } = await loadConfigWithMeta(options.config);
|
|
212
|
+
console.log(configPath);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(chalk.red('Failed to find config:'), (error as Error).message);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Subcommand: config init-adapter <path>
|
|
220
|
+
configCmd
|
|
221
|
+
.command('init-adapter <adapterPath>')
|
|
222
|
+
.description('Configure a translation adapter module in the config file')
|
|
223
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', DEFAULT_CONFIG_FILENAME)
|
|
224
|
+
.option('--hook <name>', 'Name of the translation hook (default: useTranslation)', 'useTranslation')
|
|
225
|
+
.option('--json', 'Output result as JSON', false)
|
|
226
|
+
.action(async (adapterPath: string, options: { config?: string; hook: string; json?: boolean }) => {
|
|
227
|
+
try {
|
|
228
|
+
const { configPath } = await loadConfigWithMeta(options.config);
|
|
229
|
+
const { parsed } = await readRawConfig(configPath);
|
|
230
|
+
|
|
231
|
+
// Resolve relative path
|
|
232
|
+
const projectRoot = path.dirname(configPath);
|
|
233
|
+
const relativePath = path.isAbsolute(adapterPath)
|
|
234
|
+
? path.relative(projectRoot, adapterPath)
|
|
235
|
+
: adapterPath;
|
|
236
|
+
|
|
237
|
+
// Update translationAdapter
|
|
238
|
+
if (!parsed.translationAdapter || typeof parsed.translationAdapter !== 'object') {
|
|
239
|
+
parsed.translationAdapter = {};
|
|
240
|
+
}
|
|
241
|
+
const adapter = parsed.translationAdapter as Record<string, unknown>;
|
|
242
|
+
adapter.module = relativePath;
|
|
243
|
+
adapter.hookName = options.hook;
|
|
244
|
+
|
|
245
|
+
await writeConfig(configPath, parsed);
|
|
246
|
+
|
|
247
|
+
if (options.json) {
|
|
248
|
+
console.log(JSON.stringify({
|
|
249
|
+
translationAdapter: parsed.translationAdapter,
|
|
250
|
+
configPath,
|
|
251
|
+
}, null, 2));
|
|
252
|
+
} else {
|
|
253
|
+
console.log(chalk.green('✓ Translation adapter configured:'));
|
|
254
|
+
console.log(chalk.dim(` module: ${relativePath}`));
|
|
255
|
+
console.log(chalk.dim(` hookName: ${options.hook}`));
|
|
256
|
+
console.log(chalk.dim(` Updated: ${configPath}`));
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(chalk.red('Failed to configure adapter:'), (error as Error).message);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const CLI_PATH = path.resolve(__dirname, '../../dist/index.js');
|
|
11
|
+
|
|
12
|
+
function runCli(args: string[], options: { cwd?: string } = {}) {
|
|
13
|
+
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
14
|
+
cwd: options.cwd ?? process.cwd(),
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout: 30000,
|
|
17
|
+
env: {
|
|
18
|
+
...process.env,
|
|
19
|
+
CI: 'true',
|
|
20
|
+
NO_COLOR: '1',
|
|
21
|
+
FORCE_COLOR: '0',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
stdout: result.stdout ?? '',
|
|
27
|
+
stderr: result.stderr ?? '',
|
|
28
|
+
output: (result.stdout ?? '') + (result.stderr ?? ''),
|
|
29
|
+
exitCode: result.status ?? 1,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('debug-patterns command', () => {
|
|
34
|
+
let tmpDir: string;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18nsmith-debug-patterns-'));
|
|
38
|
+
|
|
39
|
+
// Create a basic config
|
|
40
|
+
const config = {
|
|
41
|
+
sourceLanguage: 'en',
|
|
42
|
+
targetLanguages: ['fr'],
|
|
43
|
+
localesDir: 'locales',
|
|
44
|
+
include: ['src/**/*.tsx', 'src/**/*.ts'],
|
|
45
|
+
exclude: ['**/*.test.ts', '**/node_modules/**'],
|
|
46
|
+
};
|
|
47
|
+
await fs.writeFile(path.join(tmpDir, 'i18n.config.json'), JSON.stringify(config, null, 2));
|
|
48
|
+
|
|
49
|
+
// Create some source files
|
|
50
|
+
await fs.mkdir(path.join(tmpDir, 'src'));
|
|
51
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'App.tsx'), 'export function App() { return <div>Hello</div>; }');
|
|
52
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'utils.ts'), 'export const add = (a: number, b: number) => a + b;');
|
|
53
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'utils.test.ts'), 'test("add", () => expect(add(1, 2)).toBe(3));');
|
|
54
|
+
|
|
55
|
+
// Create locales directory
|
|
56
|
+
await fs.mkdir(path.join(tmpDir, 'locales'));
|
|
57
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), '{}');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should show include patterns with file counts', () => {
|
|
65
|
+
const result = runCli(['debug-patterns'], { cwd: tmpDir });
|
|
66
|
+
|
|
67
|
+
expect(result.exitCode).toBe(0);
|
|
68
|
+
expect(result.output).toContain('Include Patterns');
|
|
69
|
+
expect(result.output).toContain('src/**/*.tsx');
|
|
70
|
+
expect(result.output).toContain('src/**/*.ts');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should show exclude patterns', () => {
|
|
74
|
+
const result = runCli(['debug-patterns'], { cwd: tmpDir });
|
|
75
|
+
|
|
76
|
+
expect(result.output).toContain('Exclude Patterns');
|
|
77
|
+
expect(result.output).toContain('**/*.test.ts');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should show summary with effective file count', () => {
|
|
81
|
+
const result = runCli(['debug-patterns'], { cwd: tmpDir });
|
|
82
|
+
|
|
83
|
+
expect(result.output).toContain('Summary');
|
|
84
|
+
expect(result.output).toContain('Effective files to scan');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should list files in verbose mode', () => {
|
|
88
|
+
const result = runCli(['debug-patterns', '--verbose'], { cwd: tmpDir });
|
|
89
|
+
|
|
90
|
+
expect(result.output).toContain('App.tsx');
|
|
91
|
+
expect(result.output).toContain('utils.ts');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should output JSON when --json flag is used', () => {
|
|
95
|
+
const result = runCli(['debug-patterns', '--json'], { cwd: tmpDir });
|
|
96
|
+
|
|
97
|
+
// Extract JSON from output
|
|
98
|
+
const jsonMatch = result.stdout.match(/\{[\s\S]*\}/);
|
|
99
|
+
expect(jsonMatch).not.toBeNull();
|
|
100
|
+
|
|
101
|
+
const parsed = JSON.parse(jsonMatch![0]);
|
|
102
|
+
expect(parsed).toHaveProperty('includePatterns');
|
|
103
|
+
expect(parsed).toHaveProperty('excludePatterns');
|
|
104
|
+
expect(parsed).toHaveProperty('effectiveFiles');
|
|
105
|
+
expect(Array.isArray(parsed.effectiveFiles)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should detect unmatched patterns and suggest fixes', async () => {
|
|
109
|
+
// Update config with a pattern that won't match
|
|
110
|
+
const config = {
|
|
111
|
+
sourceLanguage: 'en',
|
|
112
|
+
targetLanguages: ['fr'],
|
|
113
|
+
localesDir: 'locales',
|
|
114
|
+
include: ['app/**/*.tsx', 'src/**/*.tsx'], // app/ doesn't exist
|
|
115
|
+
exclude: ['**/node_modules/**'],
|
|
116
|
+
};
|
|
117
|
+
await fs.writeFile(path.join(tmpDir, 'i18n.config.json'), JSON.stringify(config, null, 2));
|
|
118
|
+
|
|
119
|
+
const result = runCli(['debug-patterns'], { cwd: tmpDir });
|
|
120
|
+
|
|
121
|
+
// Should show the pattern that matched 0 files
|
|
122
|
+
expect(result.output).toContain('app/**/*.tsx');
|
|
123
|
+
expect(result.output).toContain('0 file(s)');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle missing config gracefully', async () => {
|
|
127
|
+
await fs.rm(path.join(tmpDir, 'i18n.config.json'));
|
|
128
|
+
|
|
129
|
+
const result = runCli(['debug-patterns'], { cwd: tmpDir });
|
|
130
|
+
|
|
131
|
+
expect(result.exitCode).toBe(1);
|
|
132
|
+
expect(result.output).toContain('failed');
|
|
133
|
+
});
|
|
134
|
+
});
|