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,173 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { execSync } from 'child_process';
6
+
7
+ describe('translate command', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18nsmith-translate-test-'));
12
+
13
+ // Create basic project structure
14
+ await fs.mkdir(path.join(tempDir, 'src'), { recursive: true });
15
+ await fs.mkdir(path.join(tempDir, 'locales'), { recursive: true });
16
+
17
+ // Create source file with translation calls
18
+ await fs.writeFile(
19
+ path.join(tempDir, 'src', 'App.tsx'),
20
+ `import { useTranslation } from 'react-i18next';
21
+ export function App() {
22
+ const { t } = useTranslation();
23
+ return <div>{t('greeting')}{t('farewell')}</div>;
24
+ }
25
+ `
26
+ );
27
+
28
+ // Create source locale
29
+ await fs.writeFile(
30
+ path.join(tempDir, 'locales', 'en.json'),
31
+ JSON.stringify({ greeting: 'Hello', farewell: 'Goodbye' }, null, 2)
32
+ );
33
+
34
+ // Create target locale with one missing key
35
+ await fs.writeFile(
36
+ path.join(tempDir, 'locales', 'fr.json'),
37
+ JSON.stringify({ greeting: 'Bonjour' }, null, 2)
38
+ );
39
+
40
+ // Create config
41
+ await fs.writeFile(
42
+ path.join(tempDir, 'i18n.config.json'),
43
+ JSON.stringify({
44
+ version: 1,
45
+ sourceLanguage: 'en',
46
+ targetLanguages: ['fr'],
47
+ localesDir: 'locales',
48
+ include: ['src/**/*.{ts,tsx}'],
49
+ }, null, 2)
50
+ );
51
+ });
52
+
53
+ afterEach(async () => {
54
+ await fs.rm(tempDir, { recursive: true, force: true });
55
+ });
56
+
57
+ const runCli = (args: string): string => {
58
+ const cliPath = path.resolve(__dirname, '..', '..', 'dist', 'index.js');
59
+ try {
60
+ return execSync(`node ${cliPath} ${args}`, {
61
+ cwd: tempDir,
62
+ encoding: 'utf8',
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ });
65
+ } catch (error) {
66
+ const err = error as { stdout?: string; stderr?: string };
67
+ return (err.stdout ?? '') + (err.stderr ?? '');
68
+ }
69
+ };
70
+
71
+ describe('--export', () => {
72
+ it('exports missing translations to CSV', async () => {
73
+ const csvPath = path.join(tempDir, 'missing.csv');
74
+ const output = runCli(`translate --export ${csvPath}`);
75
+
76
+ expect(output).toContain('Exported');
77
+
78
+ const csvContent = await fs.readFile(csvPath, 'utf8');
79
+ expect(csvContent).toContain('key,sourceLocale,sourceValue,targetLocale,translatedValue');
80
+ expect(csvContent).toContain('farewell');
81
+ expect(csvContent).toContain('Goodbye');
82
+ expect(csvContent).toContain('fr');
83
+ });
84
+
85
+ it('handles empty values correctly', async () => {
86
+ // All translations are present
87
+ await fs.writeFile(
88
+ path.join(tempDir, 'locales', 'fr.json'),
89
+ JSON.stringify({ greeting: 'Bonjour', farewell: 'Au revoir' }, null, 2)
90
+ );
91
+
92
+ const output = runCli('translate --export output.csv');
93
+ expect(output).toContain('No missing translations');
94
+ });
95
+ });
96
+
97
+ describe('--import', () => {
98
+ it('imports translations from CSV', async () => {
99
+ const csvPath = path.join(tempDir, 'translations.csv');
100
+ await fs.writeFile(
101
+ csvPath,
102
+ `key,sourceLocale,sourceValue,targetLocale,translatedValue
103
+ farewell,en,Goodbye,fr,Au revoir
104
+ `
105
+ );
106
+
107
+ const output = runCli(`translate --import ${csvPath} --write`);
108
+ expect(output).toContain('1 row');
109
+
110
+ const frContent = await fs.readFile(path.join(tempDir, 'locales', 'fr.json'), 'utf8');
111
+ const frData = JSON.parse(frContent);
112
+ expect(frData.farewell).toBe('Au revoir');
113
+ });
114
+
115
+ it('skips rows with empty translatedValue', async () => {
116
+ const csvPath = path.join(tempDir, 'translations.csv');
117
+ await fs.writeFile(
118
+ csvPath,
119
+ `key,sourceLocale,sourceValue,targetLocale,translatedValue
120
+ farewell,en,Goodbye,fr,
121
+ `
122
+ );
123
+
124
+ const output = runCli(`translate --import ${csvPath}`);
125
+ expect(output).toContain('skipped');
126
+ });
127
+
128
+ it('handles quoted CSV fields with commas', async () => {
129
+ // Add a source key with comma
130
+ await fs.writeFile(
131
+ path.join(tempDir, 'locales', 'en.json'),
132
+ JSON.stringify({
133
+ greeting: 'Hello',
134
+ farewell: 'Goodbye',
135
+ message: 'Hello, world!'
136
+ }, null, 2)
137
+ );
138
+
139
+ const csvPath = path.join(tempDir, 'translations.csv');
140
+ await fs.writeFile(
141
+ csvPath,
142
+ `key,sourceLocale,sourceValue,targetLocale,translatedValue
143
+ message,en,"Hello, world!",fr,"Bonjour, le monde!"
144
+ `
145
+ );
146
+
147
+ const output = runCli(`translate --import ${csvPath} --write`);
148
+ expect(output).toContain('1 row');
149
+
150
+ const frContent = await fs.readFile(path.join(tempDir, 'locales', 'fr.json'), 'utf8');
151
+ const frData = JSON.parse(frContent);
152
+ expect(frData.message).toBe('Bonjour, le monde!');
153
+ });
154
+
155
+ it('dry-run mode does not write files', async () => {
156
+ const csvPath = path.join(tempDir, 'translations.csv');
157
+ await fs.writeFile(
158
+ csvPath,
159
+ `key,sourceLocale,sourceValue,targetLocale,translatedValue
160
+ farewell,en,Goodbye,fr,Au revoir
161
+ `
162
+ );
163
+
164
+ const originalContent = await fs.readFile(path.join(tempDir, 'locales', 'fr.json'), 'utf8');
165
+
166
+ const output = runCli(`translate --import ${csvPath}`);
167
+ expect(output).toContain('DRY RUN');
168
+
169
+ const newContent = await fs.readFile(path.join(tempDir, 'locales', 'fr.json'), 'utf8');
170
+ expect(newContent).toBe(originalContent);
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Translate command - re-exports from translate module
3
+ * @deprecated Import from './translate/index.js' instead
4
+ */
5
+ export { registerTranslate } from './translate/index.js';
6
+ export * from './translate/types.js';
@@ -0,0 +1,479 @@
1
+ /**
2
+ * E2E Tests using fixture projects
3
+ * These tests run against pre-configured fixture projects to test real-world scenarios
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import { spawnSync } from 'child_process';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ // Get paths
17
+ const CLI_PATH = path.resolve(__dirname, '../dist/index.js');
18
+ const FIXTURES_DIR = path.resolve(__dirname, './fixtures');
19
+
20
+ // Helper to run CLI commands
21
+ function runCli(
22
+ args: string[],
23
+ options: { cwd?: string } = {}
24
+ ): { stdout: string; stderr: string; output: string; exitCode: number } {
25
+ const result = spawnSync('node', [CLI_PATH, ...args], {
26
+ cwd: options.cwd ?? process.cwd(),
27
+ encoding: 'utf8',
28
+ timeout: 30000,
29
+ env: {
30
+ ...process.env,
31
+ CI: 'true',
32
+ NO_COLOR: '1',
33
+ FORCE_COLOR: '0',
34
+ },
35
+ });
36
+
37
+ const stdout = result.stdout ?? '';
38
+ const stderr = result.stderr ?? '';
39
+
40
+ return {
41
+ stdout,
42
+ stderr,
43
+ output: stdout + stderr,
44
+ exitCode: result.status ?? 1,
45
+ };
46
+ }
47
+
48
+ // Helper to copy fixture to temp directory
49
+ async function setupFixture(fixtureName: string): Promise<string> {
50
+ const fixtureSource = path.join(FIXTURES_DIR, fixtureName);
51
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `i18nsmith-e2e-${fixtureName}-`));
52
+ await copyDir(fixtureSource, tmpDir);
53
+ return tmpDir;
54
+ }
55
+
56
+ // Helper to recursively copy directory
57
+ async function copyDir(src: string, dest: string): Promise<void> {
58
+ await fs.mkdir(dest, { recursive: true });
59
+ const entries = await fs.readdir(src, { withFileTypes: true });
60
+
61
+ for (const entry of entries) {
62
+ const srcPath = path.join(src, entry.name);
63
+ const destPath = path.join(dest, entry.name);
64
+
65
+ if (entry.isDirectory()) {
66
+ await copyDir(srcPath, destPath);
67
+ } else {
68
+ await fs.copyFile(srcPath, destPath);
69
+ }
70
+ }
71
+ }
72
+
73
+ // Helper to cleanup temp directory
74
+ async function cleanupFixture(fixtureDir: string): Promise<void> {
75
+ await fs.rm(fixtureDir, { recursive: true, force: true });
76
+ }
77
+
78
+ // Helper to extract JSON from output
79
+ function extractJson<T>(output: string): T {
80
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
81
+ if (!jsonMatch) {
82
+ throw new Error(`No JSON found in output: ${output.slice(0, 200)}...`);
83
+ }
84
+ return JSON.parse(jsonMatch[0]);
85
+ }
86
+
87
+ beforeAll(() => {
88
+ const cliRoot = path.resolve(__dirname, '..');
89
+ const result = spawnSync('pnpm', ['build'], {
90
+ cwd: cliRoot,
91
+ stdio: 'inherit',
92
+ env: { ...process.env },
93
+ });
94
+
95
+ if (result.status !== 0) {
96
+ throw new Error('Failed to build CLI before running E2E tests');
97
+ }
98
+ });
99
+
100
+ describe('E2E Fixture Tests', () => {
101
+ describe('basic-react fixture', () => {
102
+ let fixtureDir: string;
103
+
104
+ beforeEach(async () => {
105
+ fixtureDir = await setupFixture('basic-react');
106
+ });
107
+
108
+ afterEach(async () => {
109
+ await cleanupFixture(fixtureDir);
110
+ });
111
+
112
+ it('should run preflight successfully', () => {
113
+ const result = runCli(['preflight'], { cwd: fixtureDir });
114
+
115
+ expect(result.output).toContain('Config File');
116
+ expect(result.output).toContain('pass');
117
+ });
118
+
119
+ it('should scan for translation references', () => {
120
+ const result = runCli(['scan'], { cwd: fixtureDir });
121
+
122
+ expect(result.exitCode).toBe(0);
123
+ expect(result.output).toContain('Scanned');
124
+ expect(result.output).toContain('candidate');
125
+ });
126
+
127
+ it('should check locale files without errors', () => {
128
+ const result = runCli(['check'], { cwd: fixtureDir });
129
+
130
+ expect(result.exitCode).toBe(0);
131
+ expect(result.output).toContain('Locales directory');
132
+ });
133
+
134
+ it('should run sync in dry-run mode by default', () => {
135
+ const result = runCli(['sync'], { cwd: fixtureDir });
136
+
137
+ expect(result.output).toContain('DRY RUN');
138
+ });
139
+
140
+ it('should output scan JSON when requested', () => {
141
+ const result = runCli(['scan', '--json'], { cwd: fixtureDir });
142
+ const parsed = extractJson<{ filesScanned: number; candidates: unknown[] }>(result.stdout);
143
+
144
+ expect(parsed).toHaveProperty('filesScanned');
145
+ expect(parsed).toHaveProperty('candidates');
146
+ expect(typeof parsed.filesScanned).toBe('number');
147
+ });
148
+
149
+ it('should output check JSON when requested', () => {
150
+ const result = runCli(['check', '--json'], { cwd: fixtureDir });
151
+ const parsed = extractJson<{ diagnostics: unknown }>(result.stdout);
152
+
153
+ expect(parsed).toHaveProperty('diagnostics');
154
+ });
155
+ });
156
+
157
+ describe('suspicious-keys fixture', () => {
158
+ let fixtureDir: string;
159
+
160
+ beforeEach(async () => {
161
+ fixtureDir = await setupFixture('suspicious-keys');
162
+ });
163
+
164
+ afterEach(async () => {
165
+ await cleanupFixture(fixtureDir);
166
+ });
167
+
168
+ it('should detect suspicious keys in audit', () => {
169
+ const result = runCli(['audit'], { cwd: fixtureDir });
170
+
171
+ // Should find suspicious patterns
172
+ expect(result.output).toContain('Suspicious');
173
+ });
174
+
175
+ it('should fail with --strict when suspicious keys exist', () => {
176
+ const result = runCli(['sync', '--strict'], { cwd: fixtureDir });
177
+
178
+ // Should fail due to suspicious keys
179
+ expect(result.exitCode).not.toBe(0);
180
+ });
181
+
182
+ it('should pass with --strict when no suspicious patterns', async () => {
183
+ // Remove suspicious keys from en.json, leaving only good ones
184
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
185
+ const cleanLocale = {
186
+ 'proper.namespaced.key': 'This is a properly namespaced key',
187
+ 'buttons.submit': 'Submit',
188
+ 'common.title': 'Application Title'
189
+ };
190
+ await fs.writeFile(localeFile, JSON.stringify(cleanLocale, null, 2));
191
+
192
+ // Also update fr.json
193
+ const frLocaleFile = path.join(fixtureDir, 'locales', 'fr.json');
194
+ const cleanFrLocale = {
195
+ 'proper.namespaced.key': 'Ceci est une clé correctement nommée',
196
+ 'buttons.submit': 'Soumettre',
197
+ 'common.title': 'Titre de l\'application'
198
+ };
199
+ await fs.writeFile(frLocaleFile, JSON.stringify(cleanFrLocale, null, 2));
200
+
201
+ const result = runCli(['sync', '--strict'], { cwd: fixtureDir });
202
+
203
+ // The suspicious keys in source files may still trigger warnings
204
+ // but the locale files themselves should be clean
205
+ expect(result.output).toContain('Scanned');
206
+ });
207
+
208
+ it('should auto-rename suspicious keys when --write is provided', async () => {
209
+ const result = runCli(
210
+ [
211
+ 'sync',
212
+ '--auto-rename-suspicious',
213
+ '--write',
214
+ '--rename-map-file',
215
+ 'auto-rename-map.txt',
216
+ ],
217
+ { cwd: fixtureDir }
218
+ );
219
+
220
+ expect(result.exitCode).toBe(0);
221
+ expect(result.output).toContain('Applying safe rename proposals');
222
+
223
+ const enLocalePath = path.join(fixtureDir, 'locales', 'en.json');
224
+ const frLocalePath = path.join(fixtureDir, 'locales', 'fr.json');
225
+ const enLocale = JSON.parse(await fs.readFile(enLocalePath, 'utf8'));
226
+ const frLocale = JSON.parse(await fs.readFile(frLocalePath, 'utf8'));
227
+
228
+ expect(enLocale).toHaveProperty('common.hello-world');
229
+ expect(enLocale).not.toHaveProperty('Hello World');
230
+ expect(frLocale).toHaveProperty('common.hello-world');
231
+
232
+ const sourceFile = await fs.readFile(path.join(fixtureDir, 'src', 'BadKeys.tsx'), 'utf8');
233
+ expect(sourceFile).toContain("t('common.hello-world')");
234
+ expect(sourceFile).not.toContain("t('Hello World')");
235
+
236
+ const mapPath = path.join(fixtureDir, 'auto-rename-map.txt');
237
+ const mapContents = await fs.readFile(mapPath, 'utf8');
238
+ expect(mapContents).toContain('"Hello World" = "common.hello-world"');
239
+ });
240
+ });
241
+
242
+ describe('nested-locales fixture', () => {
243
+ let fixtureDir: string;
244
+
245
+ beforeEach(async () => {
246
+ fixtureDir = await setupFixture('nested-locales');
247
+ });
248
+
249
+ afterEach(async () => {
250
+ await cleanupFixture(fixtureDir);
251
+ });
252
+
253
+ it('should handle nested JSON structure', () => {
254
+ const result = runCli(['check'], { cwd: fixtureDir });
255
+
256
+ expect(result.exitCode).toBe(0);
257
+ expect(result.output).toContain('Locales directory');
258
+ });
259
+
260
+ it('should scan references with dot-notation keys', () => {
261
+ const result = runCli(['sync'], { cwd: fixtureDir });
262
+
263
+ expect(result.output).toContain('reference');
264
+ });
265
+
266
+ it('should output sync JSON with nested locale data', () => {
267
+ const result = runCli(['sync', '--json'], { cwd: fixtureDir });
268
+ const parsed = extractJson<{ references: unknown[]; filesScanned: number }>(result.stdout);
269
+
270
+ expect(parsed).toHaveProperty('references');
271
+ expect(Array.isArray(parsed.references)).toBe(true);
272
+ });
273
+ });
274
+
275
+ describe('backup and restore workflow', () => {
276
+ let fixtureDir: string;
277
+
278
+ beforeEach(async () => {
279
+ fixtureDir = await setupFixture('basic-react');
280
+ });
281
+
282
+ afterEach(async () => {
283
+ await cleanupFixture(fixtureDir);
284
+ });
285
+
286
+ it('should create backup when writing with prune', async () => {
287
+ // Add an unused key to locales
288
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
289
+ const locale = JSON.parse(await fs.readFile(localeFile, 'utf8'));
290
+ locale['unused.key.for.testing'] = 'Unused value';
291
+ await fs.writeFile(localeFile, JSON.stringify(locale, null, 2));
292
+
293
+ // Run sync with --write --prune --yes (skips confirmation)
294
+ const result = runCli(['sync', '--write', '--prune', '--yes'], { cwd: fixtureDir });
295
+
296
+ // Should mention backup (either text or emoji)
297
+ const mentionsBackup = result.output.includes('backup') || result.output.includes('📦');
298
+ expect(mentionsBackup).toBe(true);
299
+ });
300
+
301
+ it('should list backups after creating one', async () => {
302
+ // Add an unused key to locales
303
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
304
+ const locale = JSON.parse(await fs.readFile(localeFile, 'utf8'));
305
+ locale['unused.key.for.testing'] = 'Unused value';
306
+ await fs.writeFile(localeFile, JSON.stringify(locale, null, 2));
307
+
308
+ // Create a backup
309
+ runCli(['sync', '--write', '--prune', '--yes'], { cwd: fixtureDir });
310
+
311
+ // List backups
312
+ const listResult = runCli(['backup-list'], { cwd: fixtureDir });
313
+
314
+ // Should find the backup or show no backups message
315
+ expect(listResult.output).toMatch(/backup|No backups found/i);
316
+ });
317
+ });
318
+
319
+ describe('dry-run default behavior', () => {
320
+ let fixtureDir: string;
321
+
322
+ beforeEach(async () => {
323
+ fixtureDir = await setupFixture('basic-react');
324
+ });
325
+
326
+ afterEach(async () => {
327
+ await cleanupFixture(fixtureDir);
328
+ });
329
+
330
+ it('sync should not modify files by default', async () => {
331
+ // Get original content
332
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
333
+ const originalContent = await fs.readFile(localeFile, 'utf8');
334
+
335
+ // Run sync without --write
336
+ runCli(['sync'], { cwd: fixtureDir });
337
+
338
+ // Content should be unchanged
339
+ const afterContent = await fs.readFile(localeFile, 'utf8');
340
+ expect(afterContent).toBe(originalContent);
341
+ });
342
+
343
+ it('transform should not modify files by default', async () => {
344
+ // Get original content
345
+ const sourceFile = path.join(fixtureDir, 'src', 'App.tsx');
346
+ const originalContent = await fs.readFile(sourceFile, 'utf8');
347
+
348
+ // Run transform without --write
349
+ runCli(['transform'], { cwd: fixtureDir });
350
+
351
+ // Content should be unchanged
352
+ const afterContent = await fs.readFile(sourceFile, 'utf8');
353
+ expect(afterContent).toBe(originalContent);
354
+ });
355
+ });
356
+
357
+ describe('--assume flag for dynamic keys', () => {
358
+ let fixtureDir: string;
359
+
360
+ beforeEach(async () => {
361
+ fixtureDir = await setupFixture('basic-react');
362
+ });
363
+
364
+ afterEach(async () => {
365
+ await cleanupFixture(fixtureDir);
366
+ });
367
+
368
+ it('should accept assumed keys via --assume flag', async () => {
369
+ // Add keys to locale that exist but might be flagged as unused
370
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
371
+ const locale = JSON.parse(await fs.readFile(localeFile, 'utf8'));
372
+ locale['dynamic.runtime.key'] = 'Dynamic value loaded at runtime';
373
+ await fs.writeFile(localeFile, JSON.stringify(locale, null, 2));
374
+
375
+ // Run sync without --assume - key should show as unused
376
+ const resultWithoutAssume = runCli(['sync', '--json'], { cwd: fixtureDir });
377
+ const parsedWithout = extractJson<{ unusedKeys: { key: string }[] }>(resultWithoutAssume.stdout);
378
+ expect(parsedWithout.unusedKeys.some(k => k.key === 'dynamic.runtime.key')).toBe(true);
379
+
380
+ // Run sync with --assume - key should NOT show as unused
381
+ const resultWithAssume = runCli(['sync', '--json', '--assume', 'dynamic.runtime.key'], { cwd: fixtureDir });
382
+ const parsedWith = extractJson<{ unusedKeys: { key: string }[]; assumedKeys: string[] }>(resultWithAssume.stdout);
383
+ expect(parsedWith.assumedKeys).toContain('dynamic.runtime.key');
384
+ expect(parsedWith.unusedKeys.some(k => k.key === 'dynamic.runtime.key')).toBe(false);
385
+ });
386
+
387
+ it('should accept multiple assumed keys', async () => {
388
+ // Add keys to locale
389
+ const localeFile = path.join(fixtureDir, 'locales', 'en.json');
390
+ const locale = JSON.parse(await fs.readFile(localeFile, 'utf8'));
391
+ locale['dynamic.key.one'] = 'First dynamic key';
392
+ locale['dynamic.key.two'] = 'Second dynamic key';
393
+ await fs.writeFile(localeFile, JSON.stringify(locale, null, 2));
394
+
395
+ // Run sync with multiple --assume values
396
+ const result = runCli(['sync', '--json', '--assume', 'dynamic.key.one,dynamic.key.two'], { cwd: fixtureDir });
397
+ const parsed = extractJson<{ assumedKeys: string[] }>(result.stdout);
398
+
399
+ expect(parsed.assumedKeys).toContain('dynamic.key.one');
400
+ expect(parsed.assumedKeys).toContain('dynamic.key.two');
401
+ });
402
+ });
403
+
404
+ describe('--invalidate-cache flag', () => {
405
+ let fixtureDir: string;
406
+
407
+ beforeEach(async () => {
408
+ fixtureDir = await setupFixture('basic-react');
409
+ });
410
+
411
+ afterEach(async () => {
412
+ await cleanupFixture(fixtureDir);
413
+ });
414
+
415
+ it('should accept --invalidate-cache flag on sync', () => {
416
+ const result = runCli(['sync', '--invalidate-cache'], { cwd: fixtureDir });
417
+ expect(result.exitCode).toBe(0);
418
+ expect(result.output).toContain('Scanned');
419
+ });
420
+
421
+ it('should accept --invalidate-cache flag on check', () => {
422
+ const result = runCli(['check', '--invalidate-cache'], { cwd: fixtureDir });
423
+ expect(result.exitCode).toBe(0);
424
+ expect(result.output).toContain('Locales directory');
425
+ });
426
+ });
427
+
428
+ describe('rename-keys bulk operation', () => {
429
+ let fixtureDir: string;
430
+
431
+ beforeEach(async () => {
432
+ fixtureDir = await setupFixture('basic-react');
433
+ });
434
+
435
+ afterEach(async () => {
436
+ await cleanupFixture(fixtureDir);
437
+ });
438
+
439
+ it('should perform bulk rename with mapping file', async () => {
440
+ // Create a mapping file
441
+ const mappingFile = path.join(fixtureDir, 'rename-map.json');
442
+ const mapping = {
443
+ 'common.welcome': 'home.greeting',
444
+ 'common.logout': 'auth.logout',
445
+ };
446
+ await fs.writeFile(mappingFile, JSON.stringify(mapping, null, 2));
447
+
448
+ // Run rename-keys with the mapping
449
+ const result = runCli(['rename-keys', '--map', 'rename-map.json'], { cwd: fixtureDir });
450
+
451
+ expect(result.output).toContain('DRY RUN');
452
+ // Should process the mappings (even if no actual references exist in fixture)
453
+ expect(result.output).toMatch(/Updated \d+ occurrence/i);
454
+ });
455
+
456
+ it('should accept array format mapping file', async () => {
457
+ // Create a mapping file with array format
458
+ const mappingFile = path.join(fixtureDir, 'rename-map.json');
459
+ const mapping = [
460
+ { from: 'old.key.one', to: 'new.key.one' },
461
+ { from: 'old.key.two', to: 'new.key.two' },
462
+ ];
463
+ await fs.writeFile(mappingFile, JSON.stringify(mapping, null, 2));
464
+
465
+ // Run rename-keys with the mapping
466
+ const result = runCli(['rename-keys', '--map', 'rename-map.json'], { cwd: fixtureDir });
467
+
468
+ expect(result.exitCode).toBe(0);
469
+ expect(result.output).toContain('DRY RUN');
470
+ });
471
+
472
+ it('should error on missing mapping file', () => {
473
+ const result = runCli(['rename-keys', '--map', 'nonexistent.json'], { cwd: fixtureDir });
474
+
475
+ expect(result.exitCode).not.toBe(0);
476
+ expect(result.output).toContain('not found');
477
+ });
478
+ });
479
+ });