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,257 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import { Command } from 'commander';
4
+ import fg from 'fast-glob';
5
+ import { loadConfigWithMeta } from '@i18nsmith/core';
6
+
7
+ interface DebugPatternsOptions {
8
+ config?: string;
9
+ json?: boolean;
10
+ verbose?: boolean;
11
+ }
12
+
13
+ interface PatternMatch {
14
+ pattern: string;
15
+ type: 'include' | 'exclude';
16
+ matchedFiles: string[];
17
+ matchCount: number;
18
+ }
19
+
20
+ interface DebugPatternsSummary {
21
+ projectRoot: string;
22
+ includePatterns: PatternMatch[];
23
+ excludePatterns: PatternMatch[];
24
+ totalIncluded: number;
25
+ totalExcluded: number;
26
+ effectiveFiles: string[];
27
+ unmatchedSuggestions: string[];
28
+ }
29
+
30
+ export function registerDebugPatterns(program: Command) {
31
+ program
32
+ .command('debug-patterns')
33
+ .description('Debug include/exclude glob patterns to understand file matching')
34
+ .option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
35
+ .option('--json', 'Print raw JSON results', false)
36
+ .option('--verbose', 'Show all matched files for each pattern', false)
37
+ .action(async (options: DebugPatternsOptions) => {
38
+ try {
39
+ const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
40
+
41
+ console.log(chalk.blue('Debugging glob patterns...'));
42
+ console.log(chalk.gray(`Config: ${path.relative(process.cwd(), configPath)}`));
43
+ console.log(chalk.gray(`Project root: ${projectRoot}\n`));
44
+
45
+ const includePatterns = config.include ?? ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'];
46
+ const excludePatterns = config.exclude ?? ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'];
47
+
48
+ const summary = await analyzePatterns(projectRoot, includePatterns, excludePatterns, options.verbose);
49
+
50
+ if (options.json) {
51
+ console.log(JSON.stringify(summary, null, 2));
52
+ return;
53
+ }
54
+
55
+ printPatternAnalysis(summary, options.verbose);
56
+ } catch (error) {
57
+ console.error(chalk.red('Pattern debug failed:'), (error as Error).message);
58
+ process.exitCode = 1;
59
+ }
60
+ });
61
+ }
62
+
63
+ async function analyzePatterns(
64
+ projectRoot: string,
65
+ includePatterns: string[],
66
+ excludePatterns: string[],
67
+ verbose?: boolean
68
+ ): Promise<DebugPatternsSummary> {
69
+ const includeMatches: PatternMatch[] = [];
70
+ const excludeMatches: PatternMatch[] = [];
71
+
72
+ // Analyze each include pattern individually
73
+ for (const pattern of includePatterns) {
74
+ const files = await fg(pattern, {
75
+ cwd: projectRoot,
76
+ ignore: ['**/node_modules/**'],
77
+ onlyFiles: true,
78
+ absolute: false,
79
+ });
80
+ includeMatches.push({
81
+ pattern,
82
+ type: 'include',
83
+ matchedFiles: files.sort(),
84
+ matchCount: files.length,
85
+ });
86
+ }
87
+
88
+ // Analyze each exclude pattern individually
89
+ for (const pattern of excludePatterns) {
90
+ const files = await fg(pattern, {
91
+ cwd: projectRoot,
92
+ onlyFiles: true,
93
+ absolute: false,
94
+ });
95
+ excludeMatches.push({
96
+ pattern,
97
+ type: 'exclude',
98
+ matchedFiles: files.sort(),
99
+ matchCount: files.length,
100
+ });
101
+ }
102
+
103
+ // Calculate effective files (include - exclude)
104
+ const allIncluded = new Set<string>();
105
+ for (const match of includeMatches) {
106
+ for (const file of match.matchedFiles) {
107
+ allIncluded.add(file);
108
+ }
109
+ }
110
+
111
+ const allExcluded = new Set<string>();
112
+ for (const match of excludeMatches) {
113
+ for (const file of match.matchedFiles) {
114
+ allExcluded.add(file);
115
+ }
116
+ }
117
+
118
+ const effectiveFiles = Array.from(allIncluded)
119
+ .filter(file => !allExcluded.has(file))
120
+ .sort();
121
+
122
+ // Generate suggestions for unmatched patterns
123
+ const suggestions = generateSuggestions(includeMatches, excludeMatches, projectRoot);
124
+
125
+ return {
126
+ projectRoot,
127
+ includePatterns: includeMatches,
128
+ excludePatterns: excludeMatches,
129
+ totalIncluded: allIncluded.size,
130
+ totalExcluded: allExcluded.size,
131
+ effectiveFiles,
132
+ unmatchedSuggestions: suggestions,
133
+ };
134
+ }
135
+
136
+ function generateSuggestions(
137
+ includeMatches: PatternMatch[],
138
+ excludeMatches: PatternMatch[],
139
+ projectRoot: string
140
+ ): string[] {
141
+ const suggestions: string[] = [];
142
+
143
+ // Check for patterns with no matches
144
+ for (const match of includeMatches) {
145
+ if (match.matchCount === 0) {
146
+ const suggestion = suggestPatternFix(match.pattern, projectRoot);
147
+ if (suggestion) {
148
+ suggestions.push(`Include pattern "${match.pattern}" matched 0 files. ${suggestion}`);
149
+ } else {
150
+ suggestions.push(`Include pattern "${match.pattern}" matched 0 files. Check if files exist or adjust the pattern.`);
151
+ }
152
+ }
153
+ }
154
+
155
+ // Check if excludes are too broad
156
+ const totalIncluded = includeMatches.reduce((sum, m) => sum + m.matchCount, 0);
157
+ const totalExcluded = excludeMatches.reduce((sum, m) => sum + m.matchCount, 0);
158
+
159
+ if (totalExcluded > totalIncluded * 0.9 && totalIncluded > 0) {
160
+ suggestions.push('Warning: Exclude patterns are filtering out most files. Consider narrowing exclusions.');
161
+ }
162
+
163
+ // Check for common pattern mistakes
164
+ for (const match of includeMatches) {
165
+ if (match.pattern.startsWith('/')) {
166
+ suggestions.push(`Pattern "${match.pattern}" starts with "/" which may not match relative paths. Try "${match.pattern.slice(1)}".`);
167
+ }
168
+ if (match.pattern.includes('\\')) {
169
+ suggestions.push(`Pattern "${match.pattern}" contains backslashes. Use forward slashes "/" for glob patterns.`);
170
+ }
171
+ }
172
+
173
+ return suggestions;
174
+ }
175
+
176
+ function suggestPatternFix(pattern: string, projectRoot: string): string | null {
177
+ // Common fixes for patterns that don't match
178
+
179
+ // If pattern is like "src/**/*.tsx" but files are in "app/**/*.tsx"
180
+ if (pattern.startsWith('src/')) {
181
+ return 'Try "app/**/*" or check your source directory structure.';
182
+ }
183
+
184
+ // If pattern uses .tsx but project has .jsx
185
+ if (pattern.includes('.tsx')) {
186
+ return 'Try replacing ".tsx" with ".jsx" if using JavaScript.';
187
+ }
188
+
189
+ // If pattern uses double asterisk incorrectly
190
+ if (pattern.includes('/**') && !pattern.includes('/**/')) {
191
+ return 'Use "**/" for recursive matching (e.g., "src/**/*.tsx").';
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ function printPatternAnalysis(summary: DebugPatternsSummary, verbose?: boolean) {
198
+ console.log(chalk.blue('📁 Include Patterns\n'));
199
+
200
+ for (const match of summary.includePatterns) {
201
+ const status = match.matchCount > 0 ? chalk.green('✓') : chalk.red('✗');
202
+ console.log(` ${status} ${chalk.cyan(match.pattern)} → ${match.matchCount} file(s)`);
203
+
204
+ if (verbose && match.matchCount > 0) {
205
+ const preview = match.matchedFiles.slice(0, 10);
206
+ preview.forEach(file => console.log(chalk.gray(` • ${file}`)));
207
+ if (match.matchedFiles.length > 10) {
208
+ console.log(chalk.gray(` ... and ${match.matchedFiles.length - 10} more`));
209
+ }
210
+ }
211
+ }
212
+
213
+ console.log(chalk.blue('\n🚫 Exclude Patterns\n'));
214
+
215
+ for (const match of summary.excludePatterns) {
216
+ const status = match.matchCount > 0 ? chalk.yellow('⚠') : chalk.gray('○');
217
+ console.log(` ${status} ${chalk.cyan(match.pattern)} → ${match.matchCount} file(s)`);
218
+
219
+ if (verbose && match.matchCount > 0) {
220
+ const preview = match.matchedFiles.slice(0, 5);
221
+ preview.forEach(file => console.log(chalk.gray(` • ${file}`)));
222
+ if (match.matchedFiles.length > 5) {
223
+ console.log(chalk.gray(` ... and ${match.matchedFiles.length - 5} more`));
224
+ }
225
+ }
226
+ }
227
+
228
+ console.log(chalk.blue('\n📊 Summary\n'));
229
+ console.log(` Total matched by include patterns: ${summary.totalIncluded}`);
230
+ console.log(` Total matched by exclude patterns: ${summary.totalExcluded}`);
231
+ console.log(chalk.green(` Effective files to scan: ${summary.effectiveFiles.length}`));
232
+
233
+ if (verbose && summary.effectiveFiles.length > 0) {
234
+ console.log(chalk.blue('\n📄 Effective Files (first 20)\n'));
235
+ summary.effectiveFiles.slice(0, 20).forEach(file => {
236
+ console.log(chalk.gray(` • ${file}`));
237
+ });
238
+ if (summary.effectiveFiles.length > 20) {
239
+ console.log(chalk.gray(` ... and ${summary.effectiveFiles.length - 20} more`));
240
+ }
241
+ }
242
+
243
+ if (summary.unmatchedSuggestions.length > 0) {
244
+ console.log(chalk.yellow('\n💡 Suggestions\n'));
245
+ summary.unmatchedSuggestions.forEach(suggestion => {
246
+ console.log(chalk.yellow(` • ${suggestion}`));
247
+ });
248
+ }
249
+
250
+ if (summary.effectiveFiles.length === 0) {
251
+ console.log(chalk.red('\n⚠️ No files will be scanned! Check your patterns.'));
252
+ console.log(chalk.gray(' Common issues:'));
253
+ console.log(chalk.gray(' • Include patterns don\'t match your source directory'));
254
+ console.log(chalk.gray(' • Exclude patterns are too broad'));
255
+ console.log(chalk.gray(' • File extensions don\'t match (.tsx vs .jsx, .ts vs .js)'));
256
+ }
257
+ }
@@ -0,0 +1,136 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import type { Command } from 'commander';
5
+ import { loadConfig, diagnoseWorkspace } from '@i18nsmith/core';
6
+ import type { DiagnosisReport } from '@i18nsmith/core';
7
+ import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
8
+
9
+ interface DiagnoseCommandOptions {
10
+ config?: string;
11
+ json?: boolean;
12
+ report?: string;
13
+ }
14
+
15
+ function printDiagnosisReport(report: DiagnosisReport) {
16
+ console.log(chalk.green(`Locales directory: ${report.localesDir}`));
17
+
18
+ console.log(chalk.blue('\nLocales'));
19
+ if (report.localeFiles.length === 0) {
20
+ console.log(chalk.yellow(' • No locale files detected.'));
21
+ } else {
22
+ for (const entry of report.localeFiles) {
23
+ const relPath = path.relative(process.cwd(), entry.path);
24
+ const status = entry.missing
25
+ ? chalk.red('missing')
26
+ : entry.parseError
27
+ ? chalk.red('invalid JSON')
28
+ : `${entry.keyCount} keys`;
29
+ console.log(` • ${entry.locale} — ${status}${entry.missing ? '' : ` (${relPath})`}`);
30
+ }
31
+ }
32
+
33
+ console.log(chalk.blue('\nRuntime packages'));
34
+ if (report.runtimePackages.length === 0) {
35
+ console.log(chalk.yellow(' • None detected in package.json.'));
36
+ } else {
37
+ for (const pkg of report.runtimePackages) {
38
+ console.log(` • ${pkg.name}@${pkg.version ?? 'latest'} (${pkg.source})`);
39
+ }
40
+ }
41
+
42
+ console.log(chalk.blue('\nProvider candidates'));
43
+ if (report.providerFiles.length === 0) {
44
+ console.log(chalk.gray(' • No provider files discovered.'));
45
+ } else {
46
+ for (const provider of report.providerFiles) {
47
+ const flags: string[] = [];
48
+ if (provider.frameworkHint !== 'unknown') {
49
+ flags.push(provider.frameworkHint);
50
+ }
51
+ if (provider.hasI18nProvider) {
52
+ flags.push('wraps <I18nProvider>');
53
+ }
54
+ if (provider.usesTranslationHook) {
55
+ flags.push('imports translation hook');
56
+ }
57
+ const flagLabel = flags.length ? ` (${flags.join(', ')})` : '';
58
+ console.log(` • ${provider.relativePath}${flagLabel}`);
59
+ }
60
+ }
61
+
62
+ console.log(chalk.blue('\nTranslation usage'));
63
+ console.log(
64
+ ` • Files scanned: ${report.translationUsage.filesExamined} — ` +
65
+ `${report.translationUsage.hookOccurrences} ${report.translationUsage.hookName} hooks, ` +
66
+ `${report.translationUsage.identifierOccurrences} ${report.translationUsage.translationIdentifier}() calls`
67
+ );
68
+
69
+ if (report.translationUsage.hookExampleFiles.length) {
70
+ console.log(
71
+ chalk.gray(` Examples: ${report.translationUsage.hookExampleFiles.concat(report.translationUsage.identifierExampleFiles).slice(0, 5).join(', ')}`)
72
+ );
73
+ }
74
+
75
+ if (report.actionableItems.length) {
76
+ console.log(chalk.blue('\nActionable items'));
77
+ for (const item of report.actionableItems) {
78
+ const label = item.severity === 'error' ? chalk.red('ERROR') : item.severity === 'warn' ? chalk.yellow('WARN') : chalk.cyan('INFO');
79
+ console.log(` • [${label}] ${item.message}`);
80
+ }
81
+ }
82
+
83
+ if (report.recommendations.length) {
84
+ console.log(chalk.blue('\nRecommendations'));
85
+ for (const rec of report.recommendations) {
86
+ console.log(` • ${rec}`);
87
+ }
88
+ }
89
+
90
+ if (report.conflicts.length) {
91
+ console.log(chalk.red('\nConflicts'));
92
+ for (const conflict of report.conflicts) {
93
+ const files = conflict.files?.length ? ` (${conflict.files.join(', ')})` : '';
94
+ console.log(` • ${conflict.message}${files}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ export function registerDiagnose(program: Command) {
100
+ program
101
+ .command('diagnose')
102
+ .description('Detect existing i18n assets and potential merge conflicts')
103
+ .option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
104
+ .option('--json', 'Print raw JSON results', false)
105
+ .option('--report <path>', 'Write JSON report to a file (for CI or editors)')
106
+ .action(async (options: DiagnoseCommandOptions) => {
107
+ console.log(chalk.blue('Running repository diagnostics...'));
108
+ try {
109
+ const config = await loadConfig(options.config);
110
+ const report = await diagnoseWorkspace(config);
111
+
112
+ if (options.report) {
113
+ const outputPath = path.resolve(process.cwd(), options.report);
114
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
115
+ await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
116
+ console.log(chalk.green(`Diagnosis report written to ${outputPath}`));
117
+ }
118
+
119
+ if (options.json) {
120
+ console.log(JSON.stringify(report, null, 2));
121
+ } else {
122
+ printDiagnosisReport(report);
123
+ }
124
+
125
+ const exitSignal = getDiagnosisExitSignal(report);
126
+ if (exitSignal) {
127
+ console.error(chalk.red(`\nBlocking conflicts detected (${report.conflicts.length}).`));
128
+ console.error(chalk.red(`Exit code ${exitSignal.code}: ${exitSignal.reason}`));
129
+ process.exitCode = exitSignal.code;
130
+ }
131
+ } catch (error) {
132
+ console.error(chalk.red('Diagnose failed:'), (error as Error).message);
133
+ process.exitCode = 1;
134
+ }
135
+ });
136
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import { registerInit, parseGlobList } from './init.js';
4
+
5
+ vi.mock('@i18nsmith/core', () => ({
6
+ diagnoseWorkspace: vi.fn().mockResolvedValue({
7
+ localesDir: 'locales',
8
+ localeFiles: [],
9
+ detectedLocales: [],
10
+ runtimePackages: [],
11
+ providerFiles: [],
12
+ adapterFiles: [],
13
+ translationUsage: {
14
+ hookName: 'useTranslation',
15
+ translationIdentifier: 't',
16
+ filesExamined: 0,
17
+ hookOccurrences: 0,
18
+ identifierOccurrences: 0,
19
+ hookExampleFiles: [],
20
+ identifierExampleFiles: [],
21
+ },
22
+ actionableItems: [],
23
+ conflicts: [],
24
+ recommendations: [],
25
+ }),
26
+ }));
27
+
28
+ vi.mock('inquirer', () => ({
29
+ default: {
30
+ prompt: vi.fn().mockResolvedValue({
31
+ sourceLanguage: 'en',
32
+ adapter: 'custom',
33
+ localesDir: 'locales',
34
+ }),
35
+ },
36
+ }));
37
+
38
+ vi.mock('fs/promises', () => ({
39
+ default: {
40
+ writeFile: vi.fn().mockResolvedValue(undefined),
41
+ mkdir: vi.fn().mockResolvedValue(undefined),
42
+ },
43
+ }));
44
+
45
+ describe('init command', () => {
46
+ it('should register the init command', () => {
47
+ const program = new Command();
48
+ registerInit(program);
49
+ const command = program.commands.find((cmd) => cmd.name() === 'init');
50
+ expect(command).toBeDefined();
51
+ });
52
+ });
53
+
54
+ describe('parseGlobList', () => {
55
+ it('treats brace-expanded globs as atomic tokens', () => {
56
+ const input = 'src/**/*.{ts,tsx,js,jsx}, app/**/*.{ts,tsx}';
57
+ expect(parseGlobList(input)).toEqual([
58
+ 'src/**/*.{ts,tsx,js,jsx}',
59
+ 'app/**/*.{ts,tsx}',
60
+ ]);
61
+ });
62
+
63
+ it('handles nested braces', () => {
64
+ const input = 'src/**/*.{ts,tsx,{spec,test}.ts}';
65
+ expect(parseGlobList(input)).toEqual(['src/**/*.{ts,tsx,{spec,test}.ts}']);
66
+ });
67
+
68
+ it('splits simple comma-separated values', () => {
69
+ const input = 'en, fr, es';
70
+ expect(parseGlobList(input)).toEqual(['en', 'fr', 'es']);
71
+ });
72
+
73
+ it('handles empty input', () => {
74
+ expect(parseGlobList('')).toEqual([]);
75
+ expect(parseGlobList(' ')).toEqual([]);
76
+ });
77
+
78
+ it('trims whitespace around entries', () => {
79
+ const input = ' src/**/* , app/**/* ';
80
+ expect(parseGlobList(input)).toEqual(['src/**/*', 'app/**/*']);
81
+ });
82
+ });