@llm-translate/cli 1.0.0-next.1

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 (157) hide show
  1. package/.dockerignore +51 -0
  2. package/.env.example +33 -0
  3. package/.github/workflows/docs-pages.yml +57 -0
  4. package/.github/workflows/release.yml +49 -0
  5. package/.translaterc.json +44 -0
  6. package/CLAUDE.md +243 -0
  7. package/Dockerfile +55 -0
  8. package/README.md +371 -0
  9. package/RFC.md +1595 -0
  10. package/dist/cli/index.d.ts +2 -0
  11. package/dist/cli/index.js +4494 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.d.ts +1152 -0
  14. package/dist/index.js +3841 -0
  15. package/dist/index.js.map +1 -0
  16. package/docker-compose.yml +56 -0
  17. package/docs/.vitepress/config.ts +161 -0
  18. package/docs/api/agent.md +262 -0
  19. package/docs/api/engine.md +274 -0
  20. package/docs/api/index.md +171 -0
  21. package/docs/api/providers.md +304 -0
  22. package/docs/changelog.md +64 -0
  23. package/docs/cli/dir.md +243 -0
  24. package/docs/cli/file.md +213 -0
  25. package/docs/cli/glossary.md +273 -0
  26. package/docs/cli/index.md +129 -0
  27. package/docs/cli/init.md +158 -0
  28. package/docs/cli/serve.md +211 -0
  29. package/docs/glossary.json +235 -0
  30. package/docs/guide/chunking.md +272 -0
  31. package/docs/guide/configuration.md +139 -0
  32. package/docs/guide/cost-optimization.md +237 -0
  33. package/docs/guide/docker.md +371 -0
  34. package/docs/guide/getting-started.md +150 -0
  35. package/docs/guide/glossary.md +241 -0
  36. package/docs/guide/index.md +86 -0
  37. package/docs/guide/ollama.md +515 -0
  38. package/docs/guide/prompt-caching.md +221 -0
  39. package/docs/guide/providers.md +232 -0
  40. package/docs/guide/quality-control.md +206 -0
  41. package/docs/guide/vitepress-integration.md +265 -0
  42. package/docs/index.md +63 -0
  43. package/docs/ja/api/agent.md +262 -0
  44. package/docs/ja/api/engine.md +274 -0
  45. package/docs/ja/api/index.md +171 -0
  46. package/docs/ja/api/providers.md +304 -0
  47. package/docs/ja/changelog.md +64 -0
  48. package/docs/ja/cli/dir.md +243 -0
  49. package/docs/ja/cli/file.md +213 -0
  50. package/docs/ja/cli/glossary.md +273 -0
  51. package/docs/ja/cli/index.md +111 -0
  52. package/docs/ja/cli/init.md +158 -0
  53. package/docs/ja/guide/chunking.md +271 -0
  54. package/docs/ja/guide/configuration.md +139 -0
  55. package/docs/ja/guide/cost-optimization.md +30 -0
  56. package/docs/ja/guide/getting-started.md +150 -0
  57. package/docs/ja/guide/glossary.md +214 -0
  58. package/docs/ja/guide/index.md +32 -0
  59. package/docs/ja/guide/ollama.md +410 -0
  60. package/docs/ja/guide/prompt-caching.md +221 -0
  61. package/docs/ja/guide/providers.md +232 -0
  62. package/docs/ja/guide/quality-control.md +137 -0
  63. package/docs/ja/guide/vitepress-integration.md +265 -0
  64. package/docs/ja/index.md +58 -0
  65. package/docs/ko/api/agent.md +262 -0
  66. package/docs/ko/api/engine.md +274 -0
  67. package/docs/ko/api/index.md +171 -0
  68. package/docs/ko/api/providers.md +304 -0
  69. package/docs/ko/changelog.md +64 -0
  70. package/docs/ko/cli/dir.md +243 -0
  71. package/docs/ko/cli/file.md +213 -0
  72. package/docs/ko/cli/glossary.md +273 -0
  73. package/docs/ko/cli/index.md +111 -0
  74. package/docs/ko/cli/init.md +158 -0
  75. package/docs/ko/guide/chunking.md +271 -0
  76. package/docs/ko/guide/configuration.md +139 -0
  77. package/docs/ko/guide/cost-optimization.md +30 -0
  78. package/docs/ko/guide/getting-started.md +150 -0
  79. package/docs/ko/guide/glossary.md +214 -0
  80. package/docs/ko/guide/index.md +32 -0
  81. package/docs/ko/guide/ollama.md +410 -0
  82. package/docs/ko/guide/prompt-caching.md +221 -0
  83. package/docs/ko/guide/providers.md +232 -0
  84. package/docs/ko/guide/quality-control.md +137 -0
  85. package/docs/ko/guide/vitepress-integration.md +265 -0
  86. package/docs/ko/index.md +58 -0
  87. package/docs/zh/api/agent.md +262 -0
  88. package/docs/zh/api/engine.md +274 -0
  89. package/docs/zh/api/index.md +171 -0
  90. package/docs/zh/api/providers.md +304 -0
  91. package/docs/zh/changelog.md +64 -0
  92. package/docs/zh/cli/dir.md +243 -0
  93. package/docs/zh/cli/file.md +213 -0
  94. package/docs/zh/cli/glossary.md +273 -0
  95. package/docs/zh/cli/index.md +111 -0
  96. package/docs/zh/cli/init.md +158 -0
  97. package/docs/zh/guide/chunking.md +271 -0
  98. package/docs/zh/guide/configuration.md +139 -0
  99. package/docs/zh/guide/cost-optimization.md +30 -0
  100. package/docs/zh/guide/getting-started.md +150 -0
  101. package/docs/zh/guide/glossary.md +214 -0
  102. package/docs/zh/guide/index.md +32 -0
  103. package/docs/zh/guide/ollama.md +410 -0
  104. package/docs/zh/guide/prompt-caching.md +221 -0
  105. package/docs/zh/guide/providers.md +232 -0
  106. package/docs/zh/guide/quality-control.md +137 -0
  107. package/docs/zh/guide/vitepress-integration.md +265 -0
  108. package/docs/zh/index.md +58 -0
  109. package/package.json +91 -0
  110. package/release.config.mjs +15 -0
  111. package/schemas/glossary.schema.json +110 -0
  112. package/src/cli/commands/dir.ts +469 -0
  113. package/src/cli/commands/file.ts +291 -0
  114. package/src/cli/commands/glossary.ts +221 -0
  115. package/src/cli/commands/init.ts +68 -0
  116. package/src/cli/commands/serve.ts +60 -0
  117. package/src/cli/index.ts +64 -0
  118. package/src/cli/options.ts +59 -0
  119. package/src/core/agent.ts +1119 -0
  120. package/src/core/chunker.ts +391 -0
  121. package/src/core/engine.ts +634 -0
  122. package/src/errors.ts +188 -0
  123. package/src/index.ts +147 -0
  124. package/src/integrations/vitepress.ts +549 -0
  125. package/src/parsers/markdown.ts +383 -0
  126. package/src/providers/claude.ts +259 -0
  127. package/src/providers/interface.ts +109 -0
  128. package/src/providers/ollama.ts +379 -0
  129. package/src/providers/openai.ts +308 -0
  130. package/src/providers/registry.ts +153 -0
  131. package/src/server/index.ts +152 -0
  132. package/src/server/middleware/auth.ts +93 -0
  133. package/src/server/middleware/logger.ts +90 -0
  134. package/src/server/routes/health.ts +84 -0
  135. package/src/server/routes/translate.ts +210 -0
  136. package/src/server/types.ts +138 -0
  137. package/src/services/cache.ts +899 -0
  138. package/src/services/config.ts +217 -0
  139. package/src/services/glossary.ts +247 -0
  140. package/src/types/analysis.ts +164 -0
  141. package/src/types/index.ts +265 -0
  142. package/src/types/modes.ts +121 -0
  143. package/src/types/mqm.ts +157 -0
  144. package/src/utils/logger.ts +141 -0
  145. package/src/utils/tokens.ts +116 -0
  146. package/tests/fixtures/glossaries/ml-glossary.json +53 -0
  147. package/tests/fixtures/input/lynq-installation.ko.md +350 -0
  148. package/tests/fixtures/input/lynq-installation.md +350 -0
  149. package/tests/fixtures/input/simple.ko.md +27 -0
  150. package/tests/fixtures/input/simple.md +27 -0
  151. package/tests/unit/chunker.test.ts +229 -0
  152. package/tests/unit/glossary.test.ts +146 -0
  153. package/tests/unit/markdown.test.ts +205 -0
  154. package/tests/unit/tokens.test.ts +81 -0
  155. package/tsconfig.json +28 -0
  156. package/tsup.config.ts +34 -0
  157. package/vitest.config.ts +16 -0
@@ -0,0 +1,291 @@
1
+ import { Command } from 'commander';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { resolve, dirname, basename, extname } from 'node:path';
4
+ import { mkdir } from 'node:fs/promises';
5
+ import type { FileCommandOptions } from '../options.js';
6
+ import { defaults } from '../options.js';
7
+ import { loadConfig, mergeConfig } from '../../services/config.js';
8
+ import { createTranslationEngine } from '../../core/engine.js';
9
+ import { logger, configureLogger } from '../../utils/logger.js';
10
+ import { TranslationError, getExitCode } from '../../errors.js';
11
+ import type { DocumentFormat } from '../../types/index.js';
12
+
13
+ export const fileCommand = new Command('file')
14
+ .description('Translate a single file')
15
+ .argument('<input>', 'Input file path')
16
+ .argument('[output]', 'Output file path (optional)')
17
+ .option('-s, --source-lang <lang>', 'Source language code')
18
+ .option('-t, --target-lang <lang>', 'Target language code')
19
+ .option('-g, --glossary <path>', 'Path to glossary file')
20
+ .option(
21
+ '-p, --provider <name>',
22
+ 'LLM provider (claude|openai|ollama)',
23
+ defaults.provider
24
+ )
25
+ .option('-m, --model <name>', 'Model name')
26
+ .option(
27
+ '--quality <0-100>',
28
+ 'Quality threshold',
29
+ String(defaults.quality)
30
+ )
31
+ .option(
32
+ '--max-iterations <n>',
33
+ 'Max refinement iterations',
34
+ String(defaults.maxIterations)
35
+ )
36
+ .option('-o, --output <path>', 'Output path')
37
+ .option('-f, --format <fmt>', 'Force output format (md|html|txt)')
38
+ .option('--dry-run', 'Show what would be translated')
39
+ .option('--json', 'Output results as JSON')
40
+ .option(
41
+ '--chunk-size <tokens>',
42
+ 'Max tokens per chunk',
43
+ String(defaults.chunkSize)
44
+ )
45
+ .option('--no-cache', 'Disable translation cache')
46
+ .option('--context <text>', 'Additional context for translation')
47
+ .option('--strict-quality', 'Fail if quality threshold is not met')
48
+ .option('--strict-glossary', 'Fail if glossary terms are not applied')
49
+ .option('-v, --verbose', 'Enable verbose logging')
50
+ .option('-q, --quiet', 'Suppress non-error output')
51
+ .action(async (input: string, output: string | undefined, options: FileCommandOptions) => {
52
+ try {
53
+ // Configure logger
54
+ configureLogger({
55
+ level: options.verbose ? 'debug' : 'info',
56
+ quiet: options.quiet ?? false,
57
+ json: options.json ?? false,
58
+ });
59
+
60
+ // Validate required options
61
+ if (!options.sourceLang) {
62
+ console.error('Error: Source language (-s, --source-lang) is required');
63
+ process.exit(2);
64
+ }
65
+
66
+ if (!options.targetLang) {
67
+ console.error('Error: Target language (-t, --target-lang) is required');
68
+ process.exit(2);
69
+ }
70
+
71
+ // Load configuration
72
+ const baseConfig = await loadConfig({ configPath: options.config });
73
+ const config = mergeConfig(baseConfig, {
74
+ sourceLang: options.sourceLang,
75
+ targetLang: options.targetLang,
76
+ provider: options.provider,
77
+ model: options.model,
78
+ quality: options.quality ? parseInt(options.quality, 10) : undefined,
79
+ maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
80
+ chunkSize: options.chunkSize ? parseInt(options.chunkSize, 10) : undefined,
81
+ glossary: options.glossary,
82
+ noCache: options.cache === false,
83
+ });
84
+
85
+ // Read input file
86
+ const inputPath = resolve(input);
87
+ let content: string;
88
+
89
+ try {
90
+ content = await readFile(inputPath, 'utf-8');
91
+ } catch (error) {
92
+ console.error(`Error: Could not read file '${inputPath}'`);
93
+ process.exit(3);
94
+ }
95
+
96
+ if (!options.quiet) {
97
+ logger.info(`Reading: ${inputPath}`);
98
+ logger.info(`Translating: ${options.sourceLang} → ${options.targetLang}`);
99
+ if (options.glossary) {
100
+ logger.info(`Glossary: ${resolve(options.glossary)}`);
101
+ }
102
+ }
103
+
104
+ // Dry run - just show what would be done
105
+ if (options.dryRun) {
106
+ console.log('Dry run mode - no translation will be performed');
107
+ console.log(`Input: ${inputPath}`);
108
+ console.log(`Output: ${output ?? determineOutputPath(inputPath, options.targetLang)}`);
109
+ console.log(`Source language: ${options.sourceLang}`);
110
+ console.log(`Target language: ${options.targetLang}`);
111
+ console.log(`Content length: ${content.length} characters`);
112
+ return;
113
+ }
114
+
115
+ // Create translation engine
116
+ const engine = createTranslationEngine({
117
+ config,
118
+ verbose: options.verbose,
119
+ noCache: options.cache === false,
120
+ });
121
+
122
+ // Translate content
123
+ const result = await engine.translateContent({
124
+ content,
125
+ sourceLang: options.sourceLang,
126
+ targetLang: options.targetLang,
127
+ format: mapFormat(options.format),
128
+ glossaryPath: options.glossary,
129
+ qualityThreshold: options.quality ? parseInt(options.quality, 10) : undefined,
130
+ maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
131
+ context: options.context,
132
+ strictQuality: options.strictQuality,
133
+ strictGlossary: options.strictGlossary,
134
+ });
135
+
136
+ // Determine output path
137
+ const outputPath = output ?? options.output ?? determineOutputPath(inputPath, options.targetLang);
138
+
139
+ // Ensure output directory exists
140
+ await mkdir(dirname(outputPath), { recursive: true });
141
+
142
+ // Write output
143
+ await writeFile(outputPath, result.content, 'utf-8');
144
+
145
+ // Output results
146
+ if (options.json) {
147
+ console.log(JSON.stringify({
148
+ success: true,
149
+ input: inputPath,
150
+ output: outputPath,
151
+ sourceLang: options.sourceLang,
152
+ targetLang: options.targetLang,
153
+ quality: result.metadata.averageQuality,
154
+ duration: result.metadata.totalDuration,
155
+ chunks: result.chunks.length,
156
+ provider: result.metadata.provider,
157
+ model: result.metadata.model,
158
+ iterations: result.metadata.totalIterations,
159
+ tokensUsed: result.metadata.tokensUsed,
160
+ }, null, 2));
161
+ } else if (!options.quiet) {
162
+ logger.success(`Written to ${outputPath}`);
163
+ console.log('');
164
+ console.log(' Translation Summary:');
165
+ console.log(` - Model: ${result.metadata.provider}/${result.metadata.model}`);
166
+ console.log(` - Quality: ${result.metadata.averageQuality.toFixed(0)}/100`);
167
+ console.log(` - Chunks: ${result.chunks.length}`);
168
+ console.log(` - Iterations: ${result.metadata.totalIterations}`);
169
+ console.log(` - Tokens: ${result.metadata.tokensUsed.input.toLocaleString()} input / ${result.metadata.tokensUsed.output.toLocaleString()} output`);
170
+ console.log(` - Duration: ${(result.metadata.totalDuration / 1000).toFixed(1)}s`);
171
+ }
172
+
173
+ } catch (error) {
174
+ if (error instanceof TranslationError) {
175
+ console.error(`Error: ${error.message}`);
176
+ process.exit(getExitCode(error));
177
+ }
178
+ console.error('Error:', error instanceof Error ? error.message : error);
179
+ process.exit(1);
180
+ }
181
+ });
182
+
183
+ /**
184
+ * Handle stdin/stdout translation mode
185
+ */
186
+ export async function handleStdinTranslation(
187
+ options: FileCommandOptions
188
+ ): Promise<void> {
189
+ try {
190
+ // Configure logger for quiet mode (stdout is for translation output)
191
+ configureLogger({
192
+ level: 'error',
193
+ quiet: true,
194
+ json: false,
195
+ });
196
+
197
+ // Validate required options
198
+ if (!options.sourceLang) {
199
+ console.error('Error: Source language (-s, --source-lang) is required');
200
+ process.exit(2);
201
+ }
202
+
203
+ if (!options.targetLang) {
204
+ console.error('Error: Target language (-t, --target-lang) is required');
205
+ process.exit(2);
206
+ }
207
+
208
+ // Read from stdin
209
+ const chunks: Buffer[] = [];
210
+ for await (const chunk of process.stdin) {
211
+ chunks.push(chunk as Buffer);
212
+ }
213
+ const content = Buffer.concat(chunks).toString('utf-8');
214
+
215
+ if (!content.trim()) {
216
+ console.error('Error: No input provided');
217
+ process.exit(2);
218
+ }
219
+
220
+ // Load configuration
221
+ const baseConfig = await loadConfig({ configPath: options.config });
222
+ const config = mergeConfig(baseConfig, {
223
+ sourceLang: options.sourceLang,
224
+ targetLang: options.targetLang,
225
+ provider: options.provider,
226
+ model: options.model,
227
+ quality: options.quality ? parseInt(options.quality, 10) : undefined,
228
+ maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
229
+ glossary: options.glossary,
230
+ });
231
+
232
+ // Create translation engine
233
+ const engine = createTranslationEngine({
234
+ config,
235
+ verbose: false,
236
+ noCache: options.cache === false,
237
+ });
238
+
239
+ // Translate content
240
+ const result = await engine.translateContent({
241
+ content,
242
+ sourceLang: options.sourceLang,
243
+ targetLang: options.targetLang,
244
+ format: mapFormat(options.format),
245
+ glossaryPath: options.glossary,
246
+ qualityThreshold: options.quality ? parseInt(options.quality, 10) : undefined,
247
+ maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : undefined,
248
+ context: options.context,
249
+ });
250
+
251
+ // Output to stdout
252
+ process.stdout.write(result.content);
253
+
254
+ } catch (error) {
255
+ if (error instanceof TranslationError) {
256
+ console.error(`Error: ${error.message}`);
257
+ process.exit(getExitCode(error));
258
+ }
259
+ console.error('Error:', error instanceof Error ? error.message : error);
260
+ process.exit(1);
261
+ }
262
+ }
263
+
264
+ // ============================================================================
265
+ // Helper Functions
266
+ // ============================================================================
267
+
268
+ function determineOutputPath(inputPath: string, targetLang: string): string {
269
+ const dir = dirname(inputPath);
270
+ const ext = extname(inputPath);
271
+ const base = basename(inputPath, ext);
272
+
273
+ return resolve(dir, `${base}.${targetLang}${ext}`);
274
+ }
275
+
276
+ function mapFormat(format: string | undefined): DocumentFormat | undefined {
277
+ if (!format) return undefined;
278
+
279
+ switch (format.toLowerCase()) {
280
+ case 'md':
281
+ case 'markdown':
282
+ return 'markdown';
283
+ case 'html':
284
+ return 'html';
285
+ case 'txt':
286
+ case 'text':
287
+ return 'text';
288
+ default:
289
+ return undefined;
290
+ }
291
+ }
@@ -0,0 +1,221 @@
1
+ import { Command } from 'commander';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import type { Glossary, GlossaryTerm } from '../../types/index.js';
4
+
5
+ export const glossaryCommand = new Command('glossary')
6
+ .description('Manage glossary (add, remove, list, validate)');
7
+
8
+ // ============================================================================
9
+ // Subcommands
10
+ // ============================================================================
11
+
12
+ glossaryCommand
13
+ .command('list')
14
+ .description('List all terms in a glossary')
15
+ .argument('<path>', 'Path to glossary file')
16
+ .option('--lang <lang>', 'Filter by target language')
17
+ .action(async (path: string, options: { lang?: string }) => {
18
+ try {
19
+ const glossary = await loadGlossary(path);
20
+ console.log(`Glossary: ${glossary.metadata.name}`);
21
+ console.log(`Source: ${glossary.metadata.sourceLang}`);
22
+ console.log(`Targets: ${glossary.metadata.targetLangs.join(', ')}`);
23
+ console.log(`Terms: ${glossary.terms.length}`);
24
+ console.log('---');
25
+
26
+ for (const term of glossary.terms) {
27
+ if (term.doNotTranslate) {
28
+ console.log(` ${term.source} → [do not translate]`);
29
+ } else if (options.lang && term.targets[options.lang]) {
30
+ console.log(` ${term.source} → ${term.targets[options.lang]}`);
31
+ } else if (!options.lang) {
32
+ const translations = Object.entries(term.targets)
33
+ .map(([lang, val]) => `${lang}: ${val}`)
34
+ .join(', ');
35
+ console.log(` ${term.source} → ${translations || '[no translations]'}`);
36
+ }
37
+ }
38
+ } catch (error) {
39
+ console.error('Failed to load glossary:', error);
40
+ process.exit(1);
41
+ }
42
+ });
43
+
44
+ glossaryCommand
45
+ .command('validate')
46
+ .description('Validate a glossary file')
47
+ .argument('<path>', 'Path to glossary file')
48
+ .action(async (path: string) => {
49
+ try {
50
+ const glossary = await loadGlossary(path);
51
+ const errors = validateGlossary(glossary);
52
+
53
+ if (errors.length === 0) {
54
+ console.log('Glossary is valid!');
55
+ console.log(` Name: ${glossary.metadata.name}`);
56
+ console.log(` Terms: ${glossary.terms.length}`);
57
+ console.log(` Source: ${glossary.metadata.sourceLang}`);
58
+ console.log(` Targets: ${glossary.metadata.targetLangs.join(', ')}`);
59
+ } else {
60
+ console.error('Glossary validation failed:');
61
+ for (const error of errors) {
62
+ console.error(` - ${error}`);
63
+ }
64
+ process.exit(6); // GLOSSARY_VALIDATION_FAILED
65
+ }
66
+ } catch (error) {
67
+ console.error('Failed to load glossary:', error);
68
+ process.exit(1);
69
+ }
70
+ });
71
+
72
+ glossaryCommand
73
+ .command('add')
74
+ .description('Add a term to the glossary')
75
+ .argument('<path>', 'Path to glossary file')
76
+ .argument('<source>', 'Source term')
77
+ .option('--target <lang:value...>', 'Target translations (e.g., ko:번역)')
78
+ .option('--context <text>', 'Usage context')
79
+ .option('--do-not-translate', 'Mark as do not translate')
80
+ .action(
81
+ async (
82
+ path: string,
83
+ source: string,
84
+ options: {
85
+ target?: string[];
86
+ context?: string;
87
+ doNotTranslate?: boolean;
88
+ }
89
+ ) => {
90
+ try {
91
+ const glossary = await loadGlossary(path);
92
+
93
+ // Check if term already exists
94
+ const existing = glossary.terms.find(
95
+ (t) => t.source.toLowerCase() === source.toLowerCase()
96
+ );
97
+ if (existing) {
98
+ console.error(`Term "${source}" already exists in glossary.`);
99
+ process.exit(1);
100
+ }
101
+
102
+ // Parse targets
103
+ const targets: Record<string, string> = {};
104
+ if (options.target) {
105
+ for (const t of options.target) {
106
+ const [lang, ...rest] = t.split(':');
107
+ if (lang && rest.length > 0) {
108
+ targets[lang] = rest.join(':');
109
+ }
110
+ }
111
+ }
112
+
113
+ const newTerm: GlossaryTerm = {
114
+ source,
115
+ targets,
116
+ context: options.context,
117
+ doNotTranslate: options.doNotTranslate,
118
+ };
119
+
120
+ glossary.terms.push(newTerm);
121
+ await writeFile(path, JSON.stringify(glossary, null, 2));
122
+ console.log(`Added term: ${source}`);
123
+ } catch (error) {
124
+ console.error('Failed to add term:', error);
125
+ process.exit(1);
126
+ }
127
+ }
128
+ );
129
+
130
+ glossaryCommand
131
+ .command('remove')
132
+ .description('Remove a term from the glossary')
133
+ .argument('<path>', 'Path to glossary file')
134
+ .argument('<source>', 'Source term to remove')
135
+ .action(async (path: string, source: string) => {
136
+ try {
137
+ const glossary = await loadGlossary(path);
138
+ const index = glossary.terms.findIndex(
139
+ (t) => t.source.toLowerCase() === source.toLowerCase()
140
+ );
141
+
142
+ if (index === -1) {
143
+ console.error(`Term "${source}" not found in glossary.`);
144
+ process.exit(1);
145
+ }
146
+
147
+ glossary.terms.splice(index, 1);
148
+ await writeFile(path, JSON.stringify(glossary, null, 2));
149
+ console.log(`Removed term: ${source}`);
150
+ } catch (error) {
151
+ console.error('Failed to remove term:', error);
152
+ process.exit(1);
153
+ }
154
+ });
155
+
156
+ // ============================================================================
157
+ // Helper Functions
158
+ // ============================================================================
159
+
160
+ async function loadGlossary(path: string): Promise<Glossary> {
161
+ const content = await readFile(path, 'utf-8');
162
+ return JSON.parse(content) as Glossary;
163
+ }
164
+
165
+ function validateGlossary(glossary: Glossary): string[] {
166
+ const errors: string[] = [];
167
+
168
+ // Check metadata
169
+ if (!glossary.metadata) {
170
+ errors.push('Missing metadata section');
171
+ } else {
172
+ if (!glossary.metadata.name) {
173
+ errors.push('Missing metadata.name');
174
+ }
175
+ if (!glossary.metadata.sourceLang) {
176
+ errors.push('Missing metadata.sourceLang');
177
+ }
178
+ if (
179
+ !glossary.metadata.targetLangs ||
180
+ glossary.metadata.targetLangs.length === 0
181
+ ) {
182
+ errors.push('Missing or empty metadata.targetLangs');
183
+ }
184
+ }
185
+
186
+ // Check terms
187
+ if (!glossary.terms || !Array.isArray(glossary.terms)) {
188
+ errors.push('Missing or invalid terms array');
189
+ } else {
190
+ const seenSources = new Set<string>();
191
+
192
+ for (let i = 0; i < glossary.terms.length; i++) {
193
+ const term = glossary.terms[i];
194
+ if (!term) continue;
195
+
196
+ if (!term.source) {
197
+ errors.push(`Term at index ${i}: missing source`);
198
+ continue;
199
+ }
200
+
201
+ // Check for duplicates
202
+ const normalizedSource = term.source.toLowerCase();
203
+ if (seenSources.has(normalizedSource)) {
204
+ errors.push(`Duplicate term: "${term.source}"`);
205
+ }
206
+ seenSources.add(normalizedSource);
207
+
208
+ // Check targets if not doNotTranslate
209
+ if (!term.doNotTranslate && Object.keys(term.targets).length === 0) {
210
+ // This is a warning, not an error - term might have doNotTranslateFor
211
+ if (!term.doNotTranslateFor || term.doNotTranslateFor.length === 0) {
212
+ errors.push(
213
+ `Term "${term.source}": no translations and not marked as do-not-translate`
214
+ );
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ return errors;
221
+ }
@@ -0,0 +1,68 @@
1
+ import { Command } from 'commander';
2
+ import { writeFile, access } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import type { TranslateConfig } from '../../types/index.js';
5
+
6
+ const defaultConfig: TranslateConfig = {
7
+ version: '1.0',
8
+ project: {
9
+ name: 'My Project',
10
+ description: 'Project description',
11
+ purpose: 'Technical documentation translation',
12
+ },
13
+ languages: {
14
+ source: 'en',
15
+ targets: ['ko'],
16
+ },
17
+ provider: {
18
+ default: 'claude',
19
+ model: 'claude-sonnet-4-20250514',
20
+ },
21
+ quality: {
22
+ threshold: 85,
23
+ maxIterations: 4,
24
+ evaluationMethod: 'llm',
25
+ },
26
+ chunking: {
27
+ maxTokens: 1024,
28
+ overlapTokens: 150,
29
+ preserveStructure: true,
30
+ },
31
+ paths: {
32
+ output: './docs/{lang}',
33
+ cache: './.translate-cache',
34
+ },
35
+ ignore: ['**/node_modules/**', '**/*.test.md'],
36
+ };
37
+
38
+ export const initCommand = new Command('init')
39
+ .description('Initialize project configuration')
40
+ .option('-f, --force', 'Overwrite existing configuration')
41
+ .action(async (options: { force?: boolean }) => {
42
+ const configPath = join(process.cwd(), '.translaterc.json');
43
+
44
+ // Check if config already exists
45
+ if (!options.force) {
46
+ try {
47
+ await access(configPath);
48
+ console.error(
49
+ 'Configuration file already exists. Use --force to overwrite.'
50
+ );
51
+ process.exit(1);
52
+ } catch {
53
+ // File doesn't exist, continue
54
+ }
55
+ }
56
+
57
+ try {
58
+ await writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
59
+ console.log('Created .translaterc.json');
60
+ console.log('\nNext steps:');
61
+ console.log('1. Edit .translaterc.json to configure your project');
62
+ console.log('2. Set your API key: export ANTHROPIC_API_KEY=your-key');
63
+ console.log('3. Run: llm-translate file <input> -s en -t ko');
64
+ } catch (error) {
65
+ console.error('Failed to create configuration file:', error);
66
+ process.exit(1);
67
+ }
68
+ });
@@ -0,0 +1,60 @@
1
+ import { Command } from 'commander';
2
+ import { startServer } from '../../server/index.js';
3
+
4
+ // ============================================================================
5
+ // Types
6
+ // ============================================================================
7
+
8
+ export interface ServeCommandOptions {
9
+ port?: string;
10
+ host?: string;
11
+ auth?: boolean; // Commander's --no-auth sets this to false
12
+ cors?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ // ============================================================================
17
+ // Serve Command
18
+ // ============================================================================
19
+
20
+ export const serveCommand = new Command('serve')
21
+ .description('Start the translation API server')
22
+ .option(
23
+ '-p, --port <number>',
24
+ 'Server port (env: TRANSLATE_PORT)',
25
+ process.env['TRANSLATE_PORT'] ?? '3000'
26
+ )
27
+ .option('-H, --host <string>', 'Host to bind', '0.0.0.0')
28
+ .option('--no-auth', 'Disable API key authentication')
29
+ .option('--cors', 'Enable CORS for browser clients')
30
+ .option('--json', 'Use JSON logging format (for containers)')
31
+ .action((options: ServeCommandOptions) => {
32
+ const port = parseInt(options.port ?? '3000', 10);
33
+ const host = options.host ?? '0.0.0.0';
34
+
35
+ // Validate port
36
+ if (isNaN(port) || port < 1 || port > 65535) {
37
+ console.error('Error: Invalid port number. Must be between 1 and 65535.');
38
+ process.exit(2);
39
+ }
40
+
41
+ // Check for API key if auth is enabled
42
+ const enableAuth = options.auth !== false;
43
+ if (enableAuth && !process.env['TRANSLATE_API_KEY']) {
44
+ console.warn(
45
+ 'Warning: TRANSLATE_API_KEY not set. API key authentication is disabled.'
46
+ );
47
+ console.warn(
48
+ 'Set TRANSLATE_API_KEY environment variable to enable authentication.\n'
49
+ );
50
+ }
51
+
52
+ startServer({
53
+ port,
54
+ host,
55
+ enableAuth,
56
+ enableCors: options.cors ?? false,
57
+ apiKey: process.env['TRANSLATE_API_KEY'],
58
+ jsonLogging: options.json ?? false,
59
+ });
60
+ });