i18nsmith 0.1.9 → 0.2.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/rename.d.ts.map +1 -1
- package/dist/commands/review.d.ts +4 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.test.d.ts +2 -0
- package/dist/commands/review.test.d.ts.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/commands/translate/index.d.ts.map +1 -1
- package/dist/commands/translate/types.d.ts +2 -0
- package/dist/commands/translate/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1119 -383
- package/dist/test-helpers/ensure-cli-built.d.ts +2 -0
- package/dist/test-helpers/ensure-cli-built.d.ts.map +1 -0
- package/dist/utils/preview.d.ts +13 -0
- package/dist/utils/preview.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/commands/debug-patterns.test.ts +6 -1
- package/src/commands/rename.ts +35 -3
- package/src/commands/review.test.ts +12 -0
- package/src/commands/review.ts +228 -0
- package/src/commands/sync-seed.test.ts +9 -3
- package/src/commands/sync.ts +104 -10
- package/src/commands/transform.ts +31 -4
- package/src/commands/translate/index.ts +23 -3
- package/src/commands/translate/types.ts +2 -0
- package/src/commands/translate.test.ts +9 -3
- package/src/e2e.test.ts +3 -11
- package/src/index.ts +2 -0
- package/src/integration.test.ts +12 -1
- package/src/test-helpers/ensure-cli-built.ts +32 -0
- package/src/utils/preview.ts +126 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
|
+
import path from 'path';
|
|
7
8
|
import type { Command } from 'commander';
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_PLACEHOLDER_FORMATS,
|
|
@@ -19,6 +20,7 @@ import type { TranslateCommandOptions, TranslateSummary, ProviderSettings } from
|
|
|
19
20
|
import { emitTranslateOutput, maybePrintEstimate } from './reporter.js';
|
|
20
21
|
import { handleCsvExport, handleCsvImport } from './csv-handler.js';
|
|
21
22
|
import { executeTranslations } from './executor.js';
|
|
23
|
+
import { applyPreviewFile, writePreviewFile } from '../../utils/preview.js';
|
|
22
24
|
|
|
23
25
|
// Re-export types for external use
|
|
24
26
|
export * from './types.js';
|
|
@@ -98,7 +100,14 @@ export function registerTranslate(program: Command): void {
|
|
|
98
100
|
.option('--strict-placeholders', 'Fail if translated output has placeholder mismatches (for CI)', false)
|
|
99
101
|
.option('--export <path>', 'Export missing translations to a CSV file for external translation')
|
|
100
102
|
.option('--import <path>', 'Import translations from a CSV file and merge into locale files')
|
|
103
|
+
.option('--preview-output <path>', 'Write preview summary (JSON) to a file (implies dry-run)')
|
|
104
|
+
.option('--apply-preview <path>', 'Apply a previously saved translate preview JSON file safely')
|
|
101
105
|
.action(async (options: TranslateCommandOptions) => {
|
|
106
|
+
if (options.applyPreview) {
|
|
107
|
+
await applyPreviewFile('translate', options.applyPreview, ['--yes']);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
// Handle CSV export mode
|
|
103
112
|
if (options.export) {
|
|
104
113
|
await handleCsvExport(options);
|
|
@@ -111,9 +120,14 @@ export function registerTranslate(program: Command): void {
|
|
|
111
120
|
return;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
123
|
+
const previewMode = Boolean(options.previewOutput);
|
|
124
|
+
const writeEnabled = Boolean(options.write) && !previewMode;
|
|
125
|
+
if (previewMode && options.write) {
|
|
126
|
+
console.log(chalk.yellow('Preview requested; ignoring --write and running in dry-run mode.'));
|
|
127
|
+
}
|
|
128
|
+
options.write = writeEnabled;
|
|
129
|
+
|
|
130
|
+
console.log(chalk.blue(writeEnabled ? 'Translating locale files...' : 'Planning translations (dry-run)...'));
|
|
117
131
|
|
|
118
132
|
try {
|
|
119
133
|
const config = await loadConfig(options.config);
|
|
@@ -142,6 +156,12 @@ export function registerTranslate(program: Command): void {
|
|
|
142
156
|
};
|
|
143
157
|
|
|
144
158
|
if (!options.write) {
|
|
159
|
+
if (previewMode && options.previewOutput) {
|
|
160
|
+
const savedPath = await writePreviewFile('translate', summary, options.previewOutput);
|
|
161
|
+
console.log(chalk.green(`Preview written to ${path.relative(process.cwd(), savedPath)}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
if (options.estimate && providerSettings.name !== 'manual') {
|
|
146
166
|
await maybePrintEstimate(plan, providerSettings);
|
|
147
167
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import { ensureCliBuilt } from '../test-helpers/ensure-cli-built';
|
|
7
|
+
|
|
8
|
+
const CLI_PATH = path.resolve(__dirname, '..', '..', 'dist', 'index.js');
|
|
6
9
|
|
|
7
10
|
describe('translate command', () => {
|
|
8
11
|
let tempDir: string;
|
|
9
12
|
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
await ensureCliBuilt(CLI_PATH);
|
|
15
|
+
});
|
|
16
|
+
|
|
10
17
|
beforeEach(async () => {
|
|
11
18
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18nsmith-translate-test-'));
|
|
12
19
|
|
|
@@ -55,9 +62,8 @@ export function App() {
|
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
const runCli = (args: string): string => {
|
|
58
|
-
const cliPath = path.resolve(__dirname, '..', '..', 'dist', 'index.js');
|
|
59
65
|
try {
|
|
60
|
-
return execSync(`node ${
|
|
66
|
+
return execSync(`node ${CLI_PATH} ${args}`, {
|
|
61
67
|
cwd: tempDir,
|
|
62
68
|
encoding: 'utf8',
|
|
63
69
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/src/e2e.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { ensureCliBuilt } from './test-helpers/ensure-cli-built';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -84,17 +85,8 @@ function extractJson<T>(output: string): T {
|
|
|
84
85
|
return JSON.parse(jsonMatch[0]);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
beforeAll(() => {
|
|
88
|
-
|
|
89
|
-
const result = spawnSync('pnpm', ['build'], {
|
|
90
|
-
cwd: cliRoot,
|
|
91
|
-
stdio: 'inherit',
|
|
92
|
-
env: { ...process.env },
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (result.status !== 0) {
|
|
96
|
-
throw new Error('Failed to build CLI before running E2E tests');
|
|
97
|
-
}
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
await ensureCliBuilt(CLI_PATH);
|
|
98
90
|
});
|
|
99
91
|
|
|
100
92
|
describe('E2E Fixture Tests', () => {
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { registerBackup } from './commands/backup.js';
|
|
|
15
15
|
import { registerRename } from './commands/rename.js';
|
|
16
16
|
import { registerInstallHooks } from './commands/install-hooks.js';
|
|
17
17
|
import { registerConfig } from './commands/config.js';
|
|
18
|
+
import { registerReview } from './commands/review.js';
|
|
18
19
|
|
|
19
20
|
export const program = new Command();
|
|
20
21
|
|
|
@@ -38,6 +39,7 @@ registerBackup(program);
|
|
|
38
39
|
registerRename(program);
|
|
39
40
|
registerInstallHooks(program);
|
|
40
41
|
registerConfig(program);
|
|
42
|
+
registerReview(program);
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
program.parse();
|
package/src/integration.test.ts
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* These tests run the actual CLI commands against real file systems
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { ensureCliBuilt } from './test-helpers/ensure-cli-built';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
15
16
|
|
|
16
17
|
// Get the correct CLI path regardless of where tests are run from
|
|
18
|
+
// During tests, __dirname points to src/, so we go up one level to find dist/
|
|
17
19
|
const CLI_PATH = path.resolve(__dirname, '../dist/index.js');
|
|
18
20
|
|
|
19
21
|
// Helper to run CLI commands
|
|
@@ -33,6 +35,11 @@ function runCli(
|
|
|
33
35
|
},
|
|
34
36
|
});
|
|
35
37
|
|
|
38
|
+
// Log errors for debugging
|
|
39
|
+
if (result.error) {
|
|
40
|
+
console.error('CLI execution error:', result.error);
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
const stdout = result.stdout ?? '';
|
|
37
44
|
const stderr = result.stderr ?? '';
|
|
38
45
|
|
|
@@ -56,6 +63,10 @@ function extractJson<T>(output: string): T {
|
|
|
56
63
|
describe('CLI Integration Tests', () => {
|
|
57
64
|
let tmpDir: string;
|
|
58
65
|
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
await ensureCliBuilt(CLI_PATH);
|
|
68
|
+
});
|
|
69
|
+
|
|
59
70
|
beforeEach(async () => {
|
|
60
71
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18nsmith-cli-integration-'));
|
|
61
72
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { stat } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTEMPTS = 20;
|
|
4
|
+
const DEFAULT_DELAY_MS = 300;
|
|
5
|
+
const MIN_FILE_SIZE_BYTES = 1024;
|
|
6
|
+
|
|
7
|
+
function sleep(ms: number) {
|
|
8
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function ensureCliBuilt(cliPath: string): Promise<void> {
|
|
12
|
+
const maxAttempts = Number(process.env.I18NSMITH_TEST_CLI_ATTEMPTS ?? DEFAULT_MAX_ATTEMPTS);
|
|
13
|
+
const delayMs = Number(process.env.I18NSMITH_TEST_CLI_DELAY_MS ?? DEFAULT_DELAY_MS);
|
|
14
|
+
|
|
15
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
16
|
+
try {
|
|
17
|
+
const stats = await stat(cliPath);
|
|
18
|
+
if (stats.size >= MIN_FILE_SIZE_BYTES) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore and retry
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await sleep(delayMs);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(
|
|
29
|
+
`CLI not found at ${cliPath} after ${maxAttempts} attempts. ` +
|
|
30
|
+
"Ensure 'pnpm build' completes successfully before running tests."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export interface PreviewPayload<TSummary> {
|
|
6
|
+
type: string;
|
|
7
|
+
version: number;
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
timestamp: string;
|
|
11
|
+
summary: TSummary;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PreviewKind = 'sync' | 'transform' | 'rename-key' | 'translate';
|
|
15
|
+
|
|
16
|
+
export async function writePreviewFile<TSummary>(
|
|
17
|
+
kind: PreviewKind,
|
|
18
|
+
summary: TSummary,
|
|
19
|
+
outputPath: string
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const resolvedPath = path.resolve(process.cwd(), outputPath);
|
|
22
|
+
const payload: PreviewPayload<TSummary> = {
|
|
23
|
+
type: `${kind}-preview`,
|
|
24
|
+
version: 1,
|
|
25
|
+
command: buildCommandString(),
|
|
26
|
+
args: process.argv.slice(2),
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
summary,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
32
|
+
await fs.writeFile(resolvedPath, JSON.stringify(payload, null, 2), 'utf8');
|
|
33
|
+
return resolvedPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readPreviewFile<TSummary>(
|
|
37
|
+
expectedKind: PreviewKind,
|
|
38
|
+
previewPath: string
|
|
39
|
+
): Promise<PreviewPayload<TSummary>> {
|
|
40
|
+
const resolved = path.resolve(process.cwd(), previewPath);
|
|
41
|
+
const raw = await fs.readFile(resolved, 'utf8');
|
|
42
|
+
const payload = JSON.parse(raw) as PreviewPayload<TSummary>;
|
|
43
|
+
if (!payload?.type?.startsWith(`${expectedKind}-preview`)) {
|
|
44
|
+
throw new Error(`Preview kind mismatch. Expected ${expectedKind}, got ${payload?.type ?? 'unknown'}.`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(payload.args) || payload.args.length === 0) {
|
|
47
|
+
throw new Error('Preview file is missing recorded CLI arguments.');
|
|
48
|
+
}
|
|
49
|
+
return payload;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function applyPreviewFile(
|
|
53
|
+
expectedKind: PreviewKind,
|
|
54
|
+
previewPath: string,
|
|
55
|
+
extraArgs: string[] = []
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
const payload = await readPreviewFile(expectedKind, previewPath);
|
|
58
|
+
const sanitizedArgs = sanitizePreviewArgs(payload.args);
|
|
59
|
+
const [command, ...rest] = sanitizedArgs;
|
|
60
|
+
if (!command) {
|
|
61
|
+
throw new Error('Preview file does not include the original command.');
|
|
62
|
+
}
|
|
63
|
+
if (command !== expectedKind) {
|
|
64
|
+
throw new Error(`Preview command mismatch. Expected ${expectedKind}, got ${command}.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const replayArgs = [command, ...rest];
|
|
68
|
+
if (!replayArgs.some((arg) => arg === '--write' || arg.startsWith('--write='))) {
|
|
69
|
+
replayArgs.push('--write');
|
|
70
|
+
}
|
|
71
|
+
for (const extra of extraArgs) {
|
|
72
|
+
if (!replayArgs.includes(extra)) {
|
|
73
|
+
replayArgs.push(extra);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`Applying preview from ${path.relative(process.cwd(), path.resolve(previewPath))}…`);
|
|
78
|
+
await spawnCli(replayArgs);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sanitizePreviewArgs(args: string[]): string[] {
|
|
82
|
+
const sanitized: string[] = [];
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
const token = args[i];
|
|
85
|
+
if (token === '--preview-output') {
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token?.startsWith('--preview-output=')) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
sanitized.push(token);
|
|
93
|
+
}
|
|
94
|
+
return sanitized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function spawnCli(args: string[]): Promise<void> {
|
|
98
|
+
const entry = process.argv[1];
|
|
99
|
+
if (!entry) {
|
|
100
|
+
return Promise.reject(new Error('Unable to determine CLI entry point for preview apply.'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const child = spawn(process.argv[0], [entry, ...args], {
|
|
105
|
+
stdio: 'inherit',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
child.on('exit', (code) => {
|
|
109
|
+
if (code === 0) {
|
|
110
|
+
resolve();
|
|
111
|
+
} else {
|
|
112
|
+
reject(new Error(`Preview apply command exited with code ${code ?? 'unknown'}`));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
child.on('error', (error) => {
|
|
117
|
+
reject(error);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildCommandString(): string {
|
|
123
|
+
// Ignore the node + script path, keep user arguments only
|
|
124
|
+
const args = process.argv.slice(2);
|
|
125
|
+
return args.length ? `i18nsmith ${args.join(' ')}` : 'i18nsmith';
|
|
126
|
+
}
|