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.
Files changed (213) hide show
  1. package/dist/commands/audit.d.ts +3 -0
  2. package/dist/commands/audit.d.ts.map +1 -0
  3. package/dist/commands/audit.js +180 -0
  4. package/dist/commands/audit.js.map +1 -0
  5. package/dist/commands/backup.d.ts +6 -0
  6. package/dist/commands/backup.d.ts.map +1 -0
  7. package/dist/commands/backup.js +85 -0
  8. package/dist/commands/backup.js.map +1 -0
  9. package/dist/commands/check.d.ts +3 -0
  10. package/dist/commands/check.d.ts.map +1 -0
  11. package/dist/commands/check.js +151 -0
  12. package/dist/commands/check.js.map +1 -0
  13. package/dist/commands/config.d.ts +3 -0
  14. package/dist/commands/config.d.ts.map +1 -0
  15. package/dist/commands/config.js +235 -0
  16. package/dist/commands/config.js.map +1 -0
  17. package/dist/commands/debug-patterns.d.ts +3 -0
  18. package/dist/commands/debug-patterns.d.ts.map +1 -0
  19. package/dist/commands/debug-patterns.js +192 -0
  20. package/dist/commands/debug-patterns.js.map +1 -0
  21. package/dist/commands/debug-patterns.test.d.ts +2 -0
  22. package/dist/commands/debug-patterns.test.d.ts.map +1 -0
  23. package/dist/commands/debug-patterns.test.js +109 -0
  24. package/dist/commands/debug-patterns.test.js.map +1 -0
  25. package/dist/commands/diagnose.d.ts +3 -0
  26. package/dist/commands/diagnose.d.ts.map +1 -0
  27. package/dist/commands/diagnose.js +117 -0
  28. package/dist/commands/diagnose.js.map +1 -0
  29. package/dist/commands/init.d.ts +8 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/init.js +450 -0
  32. package/dist/commands/init.js.map +1 -0
  33. package/dist/commands/init.test.d.ts +2 -0
  34. package/dist/commands/init.test.d.ts.map +1 -0
  35. package/dist/commands/init.test.js +74 -0
  36. package/dist/commands/init.test.js.map +1 -0
  37. package/dist/commands/install-hooks.d.ts +3 -0
  38. package/dist/commands/install-hooks.d.ts.map +1 -0
  39. package/dist/commands/install-hooks.js +52 -0
  40. package/dist/commands/install-hooks.js.map +1 -0
  41. package/dist/commands/preflight.d.ts +7 -0
  42. package/dist/commands/preflight.d.ts.map +1 -0
  43. package/dist/commands/preflight.js +417 -0
  44. package/dist/commands/preflight.js.map +1 -0
  45. package/dist/commands/preflight.test.d.ts +5 -0
  46. package/dist/commands/preflight.test.d.ts.map +1 -0
  47. package/dist/commands/preflight.test.js +108 -0
  48. package/dist/commands/preflight.test.js.map +1 -0
  49. package/dist/commands/rename.d.ts +6 -0
  50. package/dist/commands/rename.d.ts.map +1 -0
  51. package/dist/commands/rename.js +204 -0
  52. package/dist/commands/rename.js.map +1 -0
  53. package/dist/commands/scaffold-adapter.d.ts +3 -0
  54. package/dist/commands/scaffold-adapter.d.ts.map +1 -0
  55. package/dist/commands/scaffold-adapter.js +204 -0
  56. package/dist/commands/scaffold-adapter.js.map +1 -0
  57. package/dist/commands/scaffold-adapter.test.d.ts +2 -0
  58. package/dist/commands/scaffold-adapter.test.d.ts.map +1 -0
  59. package/dist/commands/scaffold-adapter.test.js +102 -0
  60. package/dist/commands/scaffold-adapter.test.js.map +1 -0
  61. package/dist/commands/scan.d.ts +3 -0
  62. package/dist/commands/scan.d.ts.map +1 -0
  63. package/dist/commands/scan.js +93 -0
  64. package/dist/commands/scan.js.map +1 -0
  65. package/dist/commands/sync-seed.test.d.ts +2 -0
  66. package/dist/commands/sync-seed.test.d.ts.map +1 -0
  67. package/dist/commands/sync-seed.test.js +86 -0
  68. package/dist/commands/sync-seed.test.js.map +1 -0
  69. package/dist/commands/sync.d.ts +3 -0
  70. package/dist/commands/sync.d.ts.map +1 -0
  71. package/dist/commands/sync.js +590 -0
  72. package/dist/commands/sync.js.map +1 -0
  73. package/dist/commands/transform.d.ts +3 -0
  74. package/dist/commands/transform.d.ts.map +1 -0
  75. package/dist/commands/transform.js +114 -0
  76. package/dist/commands/transform.js.map +1 -0
  77. package/dist/commands/translate/csv-handler.d.ts +21 -0
  78. package/dist/commands/translate/csv-handler.d.ts.map +1 -0
  79. package/dist/commands/translate/csv-handler.js +270 -0
  80. package/dist/commands/translate/csv-handler.js.map +1 -0
  81. package/dist/commands/translate/executor.d.ts +31 -0
  82. package/dist/commands/translate/executor.d.ts.map +1 -0
  83. package/dist/commands/translate/executor.js +117 -0
  84. package/dist/commands/translate/executor.js.map +1 -0
  85. package/dist/commands/translate/index.d.ts +10 -0
  86. package/dist/commands/translate/index.d.ts.map +1 -0
  87. package/dist/commands/translate/index.js +170 -0
  88. package/dist/commands/translate/index.js.map +1 -0
  89. package/dist/commands/translate/reporter.d.ts +29 -0
  90. package/dist/commands/translate/reporter.d.ts.map +1 -0
  91. package/dist/commands/translate/reporter.js +103 -0
  92. package/dist/commands/translate/reporter.js.map +1 -0
  93. package/dist/commands/translate/types.d.ts +50 -0
  94. package/dist/commands/translate/types.d.ts.map +1 -0
  95. package/dist/commands/translate/types.js +5 -0
  96. package/dist/commands/translate/types.js.map +1 -0
  97. package/dist/commands/translate.d.ts +7 -0
  98. package/dist/commands/translate.d.ts.map +1 -0
  99. package/dist/commands/translate.js +7 -0
  100. package/dist/commands/translate.js.map +1 -0
  101. package/dist/commands/translate.test.d.ts +2 -0
  102. package/dist/commands/translate.test.d.ts.map +1 -0
  103. package/dist/commands/translate.test.js +118 -0
  104. package/dist/commands/translate.test.js.map +1 -0
  105. package/dist/e2e.test.d.ts +6 -0
  106. package/dist/e2e.test.d.ts.map +1 -0
  107. package/dist/e2e.test.js +376 -0
  108. package/dist/e2e.test.js.map +1 -0
  109. package/dist/index.d.ts +4 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +39 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/integration.test.d.ts +6 -0
  114. package/dist/integration.test.d.ts.map +1 -0
  115. package/dist/integration.test.js +320 -0
  116. package/dist/integration.test.js.map +1 -0
  117. package/dist/utils/diagnostics-exit.d.ts +12 -0
  118. package/dist/utils/diagnostics-exit.d.ts.map +1 -0
  119. package/dist/utils/diagnostics-exit.js +49 -0
  120. package/dist/utils/diagnostics-exit.js.map +1 -0
  121. package/dist/utils/diagnostics-exit.test.d.ts +2 -0
  122. package/dist/utils/diagnostics-exit.test.d.ts.map +1 -0
  123. package/dist/utils/diagnostics-exit.test.js +40 -0
  124. package/dist/utils/diagnostics-exit.test.js.map +1 -0
  125. package/dist/utils/diff-utils.d.ts +4 -0
  126. package/dist/utils/diff-utils.d.ts.map +1 -0
  127. package/dist/utils/diff-utils.js +30 -0
  128. package/dist/utils/diff-utils.js.map +1 -0
  129. package/dist/utils/diff-utils.test.d.ts +2 -0
  130. package/dist/utils/diff-utils.test.d.ts.map +1 -0
  131. package/dist/utils/diff-utils.test.js +30 -0
  132. package/dist/utils/diff-utils.test.js.map +1 -0
  133. package/dist/utils/exit-codes.d.ts +142 -0
  134. package/dist/utils/exit-codes.d.ts.map +1 -0
  135. package/dist/utils/exit-codes.js +168 -0
  136. package/dist/utils/exit-codes.js.map +1 -0
  137. package/dist/utils/package-manager.d.ts +4 -0
  138. package/dist/utils/package-manager.d.ts.map +1 -0
  139. package/dist/utils/package-manager.js +40 -0
  140. package/dist/utils/package-manager.js.map +1 -0
  141. package/dist/utils/pkg.d.ts +3 -0
  142. package/dist/utils/pkg.d.ts.map +1 -0
  143. package/dist/utils/pkg.js +24 -0
  144. package/dist/utils/pkg.js.map +1 -0
  145. package/dist/utils/provider-injector.d.ts +36 -0
  146. package/dist/utils/provider-injector.d.ts.map +1 -0
  147. package/dist/utils/provider-injector.js +223 -0
  148. package/dist/utils/provider-injector.js.map +1 -0
  149. package/dist/utils/provider-injector.test.d.ts +2 -0
  150. package/dist/utils/provider-injector.test.d.ts.map +1 -0
  151. package/dist/utils/provider-injector.test.js +67 -0
  152. package/dist/utils/provider-injector.test.js.map +1 -0
  153. package/dist/utils/scaffold.d.ts +20 -0
  154. package/dist/utils/scaffold.d.ts.map +1 -0
  155. package/dist/utils/scaffold.js +197 -0
  156. package/dist/utils/scaffold.js.map +1 -0
  157. package/package.json +35 -0
  158. package/src/commands/audit.ts +234 -0
  159. package/src/commands/backup.ts +96 -0
  160. package/src/commands/check.ts +191 -0
  161. package/src/commands/config.ts +263 -0
  162. package/src/commands/debug-patterns.test.ts +134 -0
  163. package/src/commands/debug-patterns.ts +257 -0
  164. package/src/commands/diagnose.ts +136 -0
  165. package/src/commands/init.test.ts +82 -0
  166. package/src/commands/init.ts +536 -0
  167. package/src/commands/install-hooks.ts +66 -0
  168. package/src/commands/preflight.test.ts +139 -0
  169. package/src/commands/preflight.ts +488 -0
  170. package/src/commands/rename.ts +264 -0
  171. package/src/commands/scaffold-adapter.test.ts +110 -0
  172. package/src/commands/scaffold-adapter.ts +250 -0
  173. package/src/commands/scan.ts +125 -0
  174. package/src/commands/sync-seed.test.ts +116 -0
  175. package/src/commands/sync.ts +736 -0
  176. package/src/commands/transform.ts +151 -0
  177. package/src/commands/translate/README.md +75 -0
  178. package/src/commands/translate/csv-handler.ts +301 -0
  179. package/src/commands/translate/executor.ts +188 -0
  180. package/src/commands/translate/index.ts +220 -0
  181. package/src/commands/translate/reporter.ts +138 -0
  182. package/src/commands/translate/types.ts +56 -0
  183. package/src/commands/translate.test.ts +173 -0
  184. package/src/commands/translate.ts +6 -0
  185. package/src/e2e.test.ts +479 -0
  186. package/src/fixtures/README.md +61 -0
  187. package/src/fixtures/basic-react/i18n.config.json +15 -0
  188. package/src/fixtures/basic-react/locales/de.json +8 -0
  189. package/src/fixtures/basic-react/locales/en.json +8 -0
  190. package/src/fixtures/basic-react/locales/fr.json +8 -0
  191. package/src/fixtures/basic-react/src/App.tsx +15 -0
  192. package/src/fixtures/basic-react/src/Messages.tsx +12 -0
  193. package/src/fixtures/nested-locales/i18n.config.json +9 -0
  194. package/src/fixtures/nested-locales/locales/en.json +23 -0
  195. package/src/fixtures/nested-locales/locales/fr.json +23 -0
  196. package/src/fixtures/nested-locales/src/HomePage.tsx +13 -0
  197. package/src/fixtures/suspicious-keys/i18n.config.json +9 -0
  198. package/src/fixtures/suspicious-keys/locales/en.json +11 -0
  199. package/src/fixtures/suspicious-keys/locales/fr.json +11 -0
  200. package/src/fixtures/suspicious-keys/src/BadKeys.tsx +19 -0
  201. package/src/index.ts +43 -0
  202. package/src/integration.test.ts +438 -0
  203. package/src/utils/diagnostics-exit.test.ts +47 -0
  204. package/src/utils/diagnostics-exit.ts +63 -0
  205. package/src/utils/diff-utils.test.ts +36 -0
  206. package/src/utils/diff-utils.ts +42 -0
  207. package/src/utils/exit-codes.ts +201 -0
  208. package/src/utils/package-manager.ts +44 -0
  209. package/src/utils/pkg.ts +23 -0
  210. package/src/utils/provider-injector.test.ts +79 -0
  211. package/src/utils/provider-injector.ts +315 -0
  212. package/src/utils/scaffold.ts +240 -0
  213. 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
+ }