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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the preflight onboarding check command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
// We'll test the core validation logic by importing the module
|
|
11
|
+
// For now, test the file structure and utilities
|
|
12
|
+
|
|
13
|
+
describe('preflight command', () => {
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18nsmith-preflight-test-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('config file detection', () => {
|
|
25
|
+
it('should detect when config file exists', async () => {
|
|
26
|
+
const config = {
|
|
27
|
+
sourceLanguage: 'en',
|
|
28
|
+
targetLanguages: ['fr', 'de'],
|
|
29
|
+
localesDir: 'locales',
|
|
30
|
+
include: ['src/**/*.tsx'],
|
|
31
|
+
};
|
|
32
|
+
await fs.writeFile(path.join(tmpDir, 'i18n.config.json'), JSON.stringify(config));
|
|
33
|
+
|
|
34
|
+
const configPath = path.join(tmpDir, 'i18n.config.json');
|
|
35
|
+
const exists = await fs.access(configPath).then(() => true).catch(() => false);
|
|
36
|
+
|
|
37
|
+
expect(exists).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect when config file is missing', async () => {
|
|
41
|
+
const configPath = path.join(tmpDir, 'i18n.config.json');
|
|
42
|
+
const exists = await fs.access(configPath).then(() => true).catch(() => false);
|
|
43
|
+
|
|
44
|
+
expect(exists).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('locales directory validation', () => {
|
|
49
|
+
it('should detect when locales directory exists', async () => {
|
|
50
|
+
await fs.mkdir(path.join(tmpDir, 'locales'));
|
|
51
|
+
|
|
52
|
+
const localesPath = path.join(tmpDir, 'locales');
|
|
53
|
+
const stats = await fs.stat(localesPath);
|
|
54
|
+
|
|
55
|
+
expect(stats.isDirectory()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should detect when locales directory is missing', async () => {
|
|
59
|
+
const localesPath = path.join(tmpDir, 'locales');
|
|
60
|
+
const exists = await fs.access(localesPath).then(() => true).catch(() => false);
|
|
61
|
+
|
|
62
|
+
expect(exists).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('source locale validation', () => {
|
|
67
|
+
it('should detect valid source locale file', async () => {
|
|
68
|
+
await fs.mkdir(path.join(tmpDir, 'locales'));
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
path.join(tmpDir, 'locales', 'en.json'),
|
|
71
|
+
JSON.stringify({ 'hello.world': 'Hello World' })
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const localePath = path.join(tmpDir, 'locales', 'en.json');
|
|
75
|
+
const content = await fs.readFile(localePath, 'utf8');
|
|
76
|
+
const data = JSON.parse(content);
|
|
77
|
+
|
|
78
|
+
expect(data).toHaveProperty('hello.world');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should detect invalid JSON in locale file', async () => {
|
|
82
|
+
await fs.mkdir(path.join(tmpDir, 'locales'));
|
|
83
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), '{ invalid json }');
|
|
84
|
+
|
|
85
|
+
const localePath = path.join(tmpDir, 'locales', 'en.json');
|
|
86
|
+
const content = await fs.readFile(localePath, 'utf8');
|
|
87
|
+
|
|
88
|
+
expect(() => JSON.parse(content)).toThrow();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('write permission checks', () => {
|
|
93
|
+
it('should be able to write to temp directory', async () => {
|
|
94
|
+
const testFile = path.join(tmpDir, '.write-test');
|
|
95
|
+
await fs.writeFile(testFile, 'test');
|
|
96
|
+
const content = await fs.readFile(testFile, 'utf8');
|
|
97
|
+
await fs.unlink(testFile);
|
|
98
|
+
|
|
99
|
+
expect(content).toBe('test');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('config validation', () => {
|
|
104
|
+
it('should validate source language is present', () => {
|
|
105
|
+
const config = {
|
|
106
|
+
sourceLanguage: 'en',
|
|
107
|
+
targetLanguages: ['fr'],
|
|
108
|
+
localesDir: 'locales',
|
|
109
|
+
include: ['src/**/*.tsx'],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(config.sourceLanguage).toBe('en');
|
|
113
|
+
expect(config.sourceLanguage.length).toBeGreaterThanOrEqual(2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should detect missing target languages', () => {
|
|
117
|
+
const config = {
|
|
118
|
+
sourceLanguage: 'en',
|
|
119
|
+
targetLanguages: [] as string[],
|
|
120
|
+
localesDir: 'locales',
|
|
121
|
+
include: ['src/**/*.tsx'],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
expect(config.targetLanguages.length).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should detect node_modules in include patterns', () => {
|
|
128
|
+
const config = {
|
|
129
|
+
sourceLanguage: 'en',
|
|
130
|
+
targetLanguages: ['fr'],
|
|
131
|
+
localesDir: 'locales',
|
|
132
|
+
include: ['src/**/*.tsx', 'node_modules/**/*.tsx'],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const hasNodeModules = config.include.some(p => p.includes('node_modules'));
|
|
136
|
+
expect(hasNodeModules).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight check command - validates environment before running i18nsmith operations.
|
|
3
|
+
* This is the onboarding wizard that helps users ensure their setup is correct.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import fg from 'fast-glob';
|
|
11
|
+
import { loadConfigWithMeta, diagnoseWorkspace, I18nConfig } from '@i18nsmith/core';
|
|
12
|
+
import { hasDependency, readPackageJson } from '../utils/pkg.js';
|
|
13
|
+
|
|
14
|
+
interface PreflightResult {
|
|
15
|
+
passed: boolean;
|
|
16
|
+
checks: PreflightCheck[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PreflightCheck {
|
|
20
|
+
name: string;
|
|
21
|
+
status: 'pass' | 'fail' | 'warn';
|
|
22
|
+
message: string;
|
|
23
|
+
suggestion?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PreflightOptions {
|
|
27
|
+
config?: string;
|
|
28
|
+
fix?: boolean;
|
|
29
|
+
json?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerPreflight(program: Command) {
|
|
33
|
+
program
|
|
34
|
+
.command('preflight')
|
|
35
|
+
.description('Validate i18nsmith setup before running operations (onboarding wizard)')
|
|
36
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
|
|
37
|
+
.option('--fix', 'Attempt to fix issues automatically', false)
|
|
38
|
+
.option('--json', 'Output results as JSON', false)
|
|
39
|
+
.action(async (options: PreflightOptions) => {
|
|
40
|
+
const result = await runPreflightChecks(options);
|
|
41
|
+
|
|
42
|
+
if (options.json) {
|
|
43
|
+
console.log(JSON.stringify(result, null, 2));
|
|
44
|
+
process.exitCode = result.passed ? 0 : 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
printPreflightResults(result);
|
|
49
|
+
process.exitCode = result.passed ? 0 : 1;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runPreflightChecks(options: PreflightOptions): Promise<PreflightResult> {
|
|
54
|
+
const checks: PreflightCheck[] = [];
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
|
|
57
|
+
// 1. Check config file exists
|
|
58
|
+
const configCheck = await checkConfigFile(options.config ?? 'i18n.config.json', cwd);
|
|
59
|
+
checks.push(configCheck);
|
|
60
|
+
|
|
61
|
+
if (configCheck.status === 'fail') {
|
|
62
|
+
return { passed: false, checks };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Load config for remaining checks
|
|
66
|
+
let config: I18nConfig;
|
|
67
|
+
let projectRoot: string;
|
|
68
|
+
try {
|
|
69
|
+
const result = await loadConfigWithMeta(options.config);
|
|
70
|
+
config = result.config;
|
|
71
|
+
projectRoot = result.projectRoot;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
checks.push({
|
|
74
|
+
name: 'Config Parse',
|
|
75
|
+
status: 'fail',
|
|
76
|
+
message: `Failed to parse config: ${(err as Error).message}`,
|
|
77
|
+
suggestion: 'Check that i18n.config.json contains valid JSON',
|
|
78
|
+
});
|
|
79
|
+
return { passed: false, checks };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Check include patterns match files
|
|
83
|
+
const includeCheck = await checkIncludePatterns(config, projectRoot);
|
|
84
|
+
checks.push(includeCheck);
|
|
85
|
+
|
|
86
|
+
// 3. Check locales directory
|
|
87
|
+
const localesDirCheck = await checkLocalesDir(config, projectRoot, options.fix);
|
|
88
|
+
checks.push(localesDirCheck);
|
|
89
|
+
|
|
90
|
+
// 4. Check source locale file
|
|
91
|
+
const sourceLocaleCheck = await checkSourceLocale(config, projectRoot, options.fix);
|
|
92
|
+
checks.push(sourceLocaleCheck);
|
|
93
|
+
|
|
94
|
+
// 5. Check adapter dependencies
|
|
95
|
+
const adapterCheck = await checkAdapterDependencies(config, projectRoot);
|
|
96
|
+
checks.push(adapterCheck);
|
|
97
|
+
|
|
98
|
+
// 6. Check for write permissions
|
|
99
|
+
const permissionCheck = await checkWritePermissions(config, projectRoot);
|
|
100
|
+
checks.push(permissionCheck);
|
|
101
|
+
|
|
102
|
+
// 7. Check for common configuration issues
|
|
103
|
+
const configValidationChecks = await validateConfigOptions(config);
|
|
104
|
+
checks.push(...configValidationChecks);
|
|
105
|
+
|
|
106
|
+
const passed = checks.every((c) => c.status !== 'fail');
|
|
107
|
+
return { passed, checks };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function checkConfigFile(configPath: string, cwd: string): Promise<PreflightCheck> {
|
|
111
|
+
const absolutePath = path.isAbsolute(configPath) ? configPath : path.resolve(cwd, configPath);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await fs.access(absolutePath);
|
|
115
|
+
return {
|
|
116
|
+
name: 'Config File',
|
|
117
|
+
status: 'pass',
|
|
118
|
+
message: `Found config at ${path.relative(cwd, absolutePath) || configPath}`,
|
|
119
|
+
};
|
|
120
|
+
} catch {
|
|
121
|
+
// Try to find config up the directory tree
|
|
122
|
+
let searchDir = cwd;
|
|
123
|
+
let parentDir = path.dirname(searchDir);
|
|
124
|
+
let found = false;
|
|
125
|
+
let foundPath = '';
|
|
126
|
+
|
|
127
|
+
while (searchDir !== parentDir) {
|
|
128
|
+
const testPath = path.join(searchDir, 'i18n.config.json');
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(testPath);
|
|
131
|
+
found = true;
|
|
132
|
+
foundPath = testPath;
|
|
133
|
+
break;
|
|
134
|
+
} catch {
|
|
135
|
+
searchDir = parentDir;
|
|
136
|
+
parentDir = path.dirname(searchDir);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (found) {
|
|
141
|
+
return {
|
|
142
|
+
name: 'Config File',
|
|
143
|
+
status: 'warn',
|
|
144
|
+
message: `Config found at ${path.relative(cwd, foundPath)} (not in current directory)`,
|
|
145
|
+
suggestion: 'You may be in a subdirectory. Run commands from the project root or use -c flag.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
name: 'Config File',
|
|
151
|
+
status: 'fail',
|
|
152
|
+
message: `Config file not found: ${configPath}`,
|
|
153
|
+
suggestion: 'Run "i18nsmith init" to create a configuration file',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function checkIncludePatterns(config: I18nConfig, projectRoot: string): Promise<PreflightCheck> {
|
|
159
|
+
try {
|
|
160
|
+
const patterns = config.include.map((p) =>
|
|
161
|
+
path.isAbsolute(p) ? p : path.join(projectRoot, p)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const files = await fg(patterns, {
|
|
165
|
+
ignore: config.exclude ?? [],
|
|
166
|
+
cwd: projectRoot,
|
|
167
|
+
absolute: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (files.length === 0) {
|
|
171
|
+
return {
|
|
172
|
+
name: 'Source Files',
|
|
173
|
+
status: 'fail',
|
|
174
|
+
message: 'No source files match the include patterns',
|
|
175
|
+
suggestion: `Check your "include" patterns: ${config.include.join(', ')}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
name: 'Source Files',
|
|
181
|
+
status: 'pass',
|
|
182
|
+
message: `Found ${files.length} source file(s) to scan`,
|
|
183
|
+
};
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
name: 'Source Files',
|
|
187
|
+
status: 'fail',
|
|
188
|
+
message: `Error matching include patterns: ${(err as Error).message}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function checkLocalesDir(
|
|
194
|
+
config: I18nConfig,
|
|
195
|
+
projectRoot: string,
|
|
196
|
+
autoFix?: boolean
|
|
197
|
+
): Promise<PreflightCheck> {
|
|
198
|
+
const localesDir = path.resolve(projectRoot, config.localesDir ?? 'locales');
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const stats = await fs.stat(localesDir);
|
|
202
|
+
if (!stats.isDirectory()) {
|
|
203
|
+
return {
|
|
204
|
+
name: 'Locales Directory',
|
|
205
|
+
status: 'fail',
|
|
206
|
+
message: `${config.localesDir} exists but is not a directory`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
name: 'Locales Directory',
|
|
211
|
+
status: 'pass',
|
|
212
|
+
message: `Locales directory exists: ${config.localesDir}`,
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
if (autoFix) {
|
|
216
|
+
try {
|
|
217
|
+
await fs.mkdir(localesDir, { recursive: true });
|
|
218
|
+
return {
|
|
219
|
+
name: 'Locales Directory',
|
|
220
|
+
status: 'pass',
|
|
221
|
+
message: `Created locales directory: ${config.localesDir}`,
|
|
222
|
+
};
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return {
|
|
225
|
+
name: 'Locales Directory',
|
|
226
|
+
status: 'fail',
|
|
227
|
+
message: `Failed to create locales directory: ${(err as Error).message}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
name: 'Locales Directory',
|
|
234
|
+
status: 'warn',
|
|
235
|
+
message: `Locales directory does not exist: ${config.localesDir}`,
|
|
236
|
+
suggestion: 'Run with --fix to create it, or run "i18nsmith sync --write"',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function checkSourceLocale(
|
|
242
|
+
config: I18nConfig,
|
|
243
|
+
projectRoot: string,
|
|
244
|
+
autoFix?: boolean
|
|
245
|
+
): Promise<PreflightCheck> {
|
|
246
|
+
const localesDir = path.resolve(projectRoot, config.localesDir ?? 'locales');
|
|
247
|
+
const sourceLocalePath = path.join(localesDir, `${config.sourceLanguage}.json`);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const content = await fs.readFile(sourceLocalePath, 'utf8');
|
|
251
|
+
JSON.parse(content); // Validate JSON
|
|
252
|
+
return {
|
|
253
|
+
name: 'Source Locale',
|
|
254
|
+
status: 'pass',
|
|
255
|
+
message: `Source locale file exists: ${config.sourceLanguage}.json`,
|
|
256
|
+
};
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const error = err as NodeJS.ErrnoException;
|
|
259
|
+
if (error.code === 'ENOENT') {
|
|
260
|
+
if (autoFix) {
|
|
261
|
+
try {
|
|
262
|
+
await fs.mkdir(localesDir, { recursive: true });
|
|
263
|
+
await fs.writeFile(sourceLocalePath, '{}');
|
|
264
|
+
return {
|
|
265
|
+
name: 'Source Locale',
|
|
266
|
+
status: 'pass',
|
|
267
|
+
message: `Created source locale file: ${config.sourceLanguage}.json`,
|
|
268
|
+
};
|
|
269
|
+
} catch (fixErr) {
|
|
270
|
+
return {
|
|
271
|
+
name: 'Source Locale',
|
|
272
|
+
status: 'fail',
|
|
273
|
+
message: `Failed to create source locale: ${(fixErr as Error).message}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
name: 'Source Locale',
|
|
280
|
+
status: 'warn',
|
|
281
|
+
message: `Source locale file does not exist: ${config.sourceLanguage}.json`,
|
|
282
|
+
suggestion: 'Run with --fix or "i18nsmith sync --write" to create it',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
name: 'Source Locale',
|
|
288
|
+
status: 'fail',
|
|
289
|
+
message: `Source locale file is invalid: ${error.message}`,
|
|
290
|
+
suggestion: 'Check that the file contains valid JSON',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function checkAdapterDependencies(
|
|
296
|
+
config: I18nConfig,
|
|
297
|
+
projectRoot: string
|
|
298
|
+
): Promise<PreflightCheck> {
|
|
299
|
+
const adapterModule = config.translationAdapter?.module ?? 'react-i18next';
|
|
300
|
+
|
|
301
|
+
// Skip check for custom adapters (local paths)
|
|
302
|
+
if (adapterModule.startsWith('.') || adapterModule.startsWith('@/') || adapterModule.startsWith('~/')) {
|
|
303
|
+
return {
|
|
304
|
+
name: 'Adapter Dependencies',
|
|
305
|
+
status: 'pass',
|
|
306
|
+
message: `Using custom adapter: ${adapterModule}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const pkg = await readPackageJson(projectRoot);
|
|
312
|
+
|
|
313
|
+
if (adapterModule === 'react-i18next') {
|
|
314
|
+
const hasReactI18next = hasDependency(pkg, 'react-i18next');
|
|
315
|
+
const hasI18next = hasDependency(pkg, 'i18next');
|
|
316
|
+
|
|
317
|
+
if (!hasReactI18next || !hasI18next) {
|
|
318
|
+
const missing = [];
|
|
319
|
+
if (!hasReactI18next) missing.push('react-i18next');
|
|
320
|
+
if (!hasI18next) missing.push('i18next');
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
name: 'Adapter Dependencies',
|
|
324
|
+
status: 'warn',
|
|
325
|
+
message: `Missing adapter dependencies: ${missing.join(', ')}`,
|
|
326
|
+
suggestion: `Run: npm install ${missing.join(' ')}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
name: 'Adapter Dependencies',
|
|
332
|
+
status: 'pass',
|
|
333
|
+
message: 'react-i18next and i18next are installed',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for other adapter modules
|
|
338
|
+
if (hasDependency(pkg, adapterModule)) {
|
|
339
|
+
return {
|
|
340
|
+
name: 'Adapter Dependencies',
|
|
341
|
+
status: 'pass',
|
|
342
|
+
message: `Adapter dependency installed: ${adapterModule}`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
name: 'Adapter Dependencies',
|
|
348
|
+
status: 'warn',
|
|
349
|
+
message: `Adapter module not found in dependencies: ${adapterModule}`,
|
|
350
|
+
suggestion: `Run: npm install ${adapterModule}`,
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
return {
|
|
354
|
+
name: 'Adapter Dependencies',
|
|
355
|
+
status: 'warn',
|
|
356
|
+
message: 'Could not read package.json to check dependencies',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function checkWritePermissions(
|
|
362
|
+
config: I18nConfig,
|
|
363
|
+
projectRoot: string
|
|
364
|
+
): Promise<PreflightCheck> {
|
|
365
|
+
const localesDir = path.resolve(projectRoot, config.localesDir ?? 'locales');
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
// Try to access the locales directory
|
|
369
|
+
await fs.access(localesDir);
|
|
370
|
+
|
|
371
|
+
// Try to create a test file
|
|
372
|
+
const testFile = path.join(localesDir, '.i18nsmith-permission-test');
|
|
373
|
+
try {
|
|
374
|
+
await fs.writeFile(testFile, '');
|
|
375
|
+
await fs.unlink(testFile);
|
|
376
|
+
return {
|
|
377
|
+
name: 'Write Permissions',
|
|
378
|
+
status: 'pass',
|
|
379
|
+
message: 'Write access to locales directory confirmed',
|
|
380
|
+
};
|
|
381
|
+
} catch {
|
|
382
|
+
return {
|
|
383
|
+
name: 'Write Permissions',
|
|
384
|
+
status: 'fail',
|
|
385
|
+
message: 'No write access to locales directory',
|
|
386
|
+
suggestion: 'Check file permissions on the locales directory',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
// Directory doesn't exist yet, check parent
|
|
391
|
+
const parentDir = path.dirname(localesDir);
|
|
392
|
+
try {
|
|
393
|
+
const testFile = path.join(parentDir, '.i18nsmith-permission-test');
|
|
394
|
+
await fs.writeFile(testFile, '');
|
|
395
|
+
await fs.unlink(testFile);
|
|
396
|
+
return {
|
|
397
|
+
name: 'Write Permissions',
|
|
398
|
+
status: 'pass',
|
|
399
|
+
message: 'Write access to project directory confirmed',
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return {
|
|
403
|
+
name: 'Write Permissions',
|
|
404
|
+
status: 'fail',
|
|
405
|
+
message: 'No write access to project directory',
|
|
406
|
+
suggestion: 'Check file permissions',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function validateConfigOptions(config: I18nConfig): Promise<PreflightCheck[]> {
|
|
413
|
+
const checks: PreflightCheck[] = [];
|
|
414
|
+
|
|
415
|
+
// Check source language
|
|
416
|
+
if (!config.sourceLanguage || config.sourceLanguage.length < 2) {
|
|
417
|
+
checks.push({
|
|
418
|
+
name: 'Source Language',
|
|
419
|
+
status: 'fail',
|
|
420
|
+
message: 'Invalid source language code',
|
|
421
|
+
suggestion: 'Use a valid language code like "en", "fr", "de"',
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
checks.push({
|
|
425
|
+
name: 'Source Language',
|
|
426
|
+
status: 'pass',
|
|
427
|
+
message: `Source language: ${config.sourceLanguage}`,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check target languages
|
|
432
|
+
if (!config.targetLanguages || config.targetLanguages.length === 0) {
|
|
433
|
+
checks.push({
|
|
434
|
+
name: 'Target Languages',
|
|
435
|
+
status: 'warn',
|
|
436
|
+
message: 'No target languages configured',
|
|
437
|
+
suggestion: 'Add target languages to translate to in your config',
|
|
438
|
+
});
|
|
439
|
+
} else {
|
|
440
|
+
checks.push({
|
|
441
|
+
name: 'Target Languages',
|
|
442
|
+
status: 'pass',
|
|
443
|
+
message: `Target languages: ${config.targetLanguages.join(', ')}`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check for common pattern mistakes
|
|
448
|
+
if (config.include.some((p) => p.includes('node_modules'))) {
|
|
449
|
+
checks.push({
|
|
450
|
+
name: 'Include Patterns',
|
|
451
|
+
status: 'warn',
|
|
452
|
+
message: 'Include patterns contain node_modules',
|
|
453
|
+
suggestion: 'Add "node_modules/**" to exclude patterns instead',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return checks;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function printPreflightResults(result: PreflightResult) {
|
|
461
|
+
console.log(chalk.blue('\nš i18nsmith Preflight Check\n'));
|
|
462
|
+
|
|
463
|
+
for (const check of result.checks) {
|
|
464
|
+
const icon = check.status === 'pass' ? 'ā' : check.status === 'warn' ? 'ā ' : 'ā';
|
|
465
|
+
const color = check.status === 'pass' ? chalk.green : check.status === 'warn' ? chalk.yellow : chalk.red;
|
|
466
|
+
|
|
467
|
+
console.log(color(`${icon} ${check.name}: ${check.message}`));
|
|
468
|
+
|
|
469
|
+
if (check.suggestion) {
|
|
470
|
+
console.log(chalk.gray(` āā ${check.suggestion}`));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
console.log('');
|
|
475
|
+
|
|
476
|
+
if (result.passed) {
|
|
477
|
+
console.log(chalk.green('ā
All preflight checks passed! You are ready to use i18nsmith.\n'));
|
|
478
|
+
console.log(chalk.gray('Next steps:'));
|
|
479
|
+
console.log(chalk.gray(' ⢠Run "i18nsmith scan" to find translatable strings'));
|
|
480
|
+
console.log(chalk.gray(' ⢠Run "i18nsmith sync" to check locale file drift'));
|
|
481
|
+
console.log(chalk.gray(' ⢠Run "i18nsmith transform" to inject translations'));
|
|
482
|
+
} else {
|
|
483
|
+
console.log(chalk.red('ā Some preflight checks failed. Please fix the issues above.\n'));
|
|
484
|
+
console.log(chalk.gray('Tips:'));
|
|
485
|
+
console.log(chalk.gray(' ⢠Run "i18nsmith init" to create a new config'));
|
|
486
|
+
console.log(chalk.gray(' ⢠Run "i18nsmith preflight --fix" to auto-fix some issues'));
|
|
487
|
+
}
|
|
488
|
+
}
|