@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.
- package/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- 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
|
+
});
|