@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,4494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { access, writeFile, readFile, readdir, mkdir } from 'fs/promises';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import 'unified';
|
|
7
|
+
import 'remark-parse';
|
|
8
|
+
import 'remark-stringify';
|
|
9
|
+
import 'remark-gfm';
|
|
10
|
+
import 'unist-util-visit';
|
|
11
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
12
|
+
import { generateText, streamText } from 'ai';
|
|
13
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
|
|
16
|
+
import { resolve, relative, join, dirname, extname, basename } from 'path';
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import { Hono } from 'hono';
|
|
19
|
+
import { cors } from 'hono/cors';
|
|
20
|
+
import { serve } from '@hono/node-server';
|
|
21
|
+
import { HTTPException } from 'hono/http-exception';
|
|
22
|
+
import { createMiddleware } from 'hono/factory';
|
|
23
|
+
import { zValidator } from '@hono/zod-validator';
|
|
24
|
+
|
|
25
|
+
var __defProp = Object.defineProperty;
|
|
26
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
27
|
+
var __esm = (fn, res) => function __init() {
|
|
28
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
29
|
+
};
|
|
30
|
+
var __export = (target, all) => {
|
|
31
|
+
for (var name in all)
|
|
32
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/cli/options.ts
|
|
36
|
+
var defaults;
|
|
37
|
+
var init_options = __esm({
|
|
38
|
+
"src/cli/options.ts"() {
|
|
39
|
+
defaults = {
|
|
40
|
+
quality: 85,
|
|
41
|
+
maxIterations: 4,
|
|
42
|
+
chunkSize: 1024,
|
|
43
|
+
parallel: 3,
|
|
44
|
+
provider: "claude"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/errors.ts
|
|
50
|
+
function formatErrorMessage(code, details) {
|
|
51
|
+
let message = errorMessages[code] ?? errorMessages["UNKNOWN_ERROR" /* UNKNOWN_ERROR */];
|
|
52
|
+
if (details) {
|
|
53
|
+
for (const [key, value] of Object.entries(details)) {
|
|
54
|
+
message = message.replace(`{${key}}`, String(value));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return message;
|
|
58
|
+
}
|
|
59
|
+
function getExitCode(error) {
|
|
60
|
+
switch (error.code) {
|
|
61
|
+
case "FILE_NOT_FOUND" /* FILE_NOT_FOUND */:
|
|
62
|
+
case "CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */:
|
|
63
|
+
case "GLOSSARY_NOT_FOUND" /* GLOSSARY_NOT_FOUND */:
|
|
64
|
+
return ExitCode.FILE_NOT_FOUND;
|
|
65
|
+
case "CONFIG_INVALID" /* CONFIG_INVALID */:
|
|
66
|
+
case "UNSUPPORTED_FORMAT" /* UNSUPPORTED_FORMAT */:
|
|
67
|
+
return ExitCode.INVALID_ARGUMENTS;
|
|
68
|
+
case "QUALITY_THRESHOLD_NOT_MET" /* QUALITY_THRESHOLD_NOT_MET */:
|
|
69
|
+
return ExitCode.QUALITY_THRESHOLD_NOT_MET;
|
|
70
|
+
case "PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */:
|
|
71
|
+
case "PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */:
|
|
72
|
+
case "PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */:
|
|
73
|
+
case "PROVIDER_ERROR" /* PROVIDER_ERROR */:
|
|
74
|
+
return ExitCode.PROVIDER_ERROR;
|
|
75
|
+
case "GLOSSARY_INVALID" /* GLOSSARY_INVALID */:
|
|
76
|
+
return ExitCode.GLOSSARY_VALIDATION_FAILED;
|
|
77
|
+
default:
|
|
78
|
+
return ExitCode.GENERAL_ERROR;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
var errorMessages, TranslationError, ExitCode;
|
|
82
|
+
var init_errors = __esm({
|
|
83
|
+
"src/errors.ts"() {
|
|
84
|
+
errorMessages = {
|
|
85
|
+
["CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */]: "Configuration file not found. Run `llm-translate init` to create one.",
|
|
86
|
+
["CONFIG_INVALID" /* CONFIG_INVALID */]: "Configuration file is invalid. Please check the format and required fields.",
|
|
87
|
+
["GLOSSARY_NOT_FOUND" /* GLOSSARY_NOT_FOUND */]: "Glossary file not found at the specified path.",
|
|
88
|
+
["GLOSSARY_INVALID" /* GLOSSARY_INVALID */]: "Glossary file is invalid. Please check the JSON format and structure.",
|
|
89
|
+
["PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */]: "The specified LLM provider is not available. Supported providers: claude, openai, ollama.",
|
|
90
|
+
["PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */]: "Authentication failed. Check your API key in environment variables.",
|
|
91
|
+
["PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */]: "Rate limited by the LLM provider. Please wait and try again.",
|
|
92
|
+
["PROVIDER_ERROR" /* PROVIDER_ERROR */]: "An error occurred while communicating with the LLM provider.",
|
|
93
|
+
["QUALITY_THRESHOLD_NOT_MET" /* QUALITY_THRESHOLD_NOT_MET */]: "Translation quality ({score}) did not meet threshold ({threshold}). Use --quality to adjust or --max-iterations to allow more refinement.",
|
|
94
|
+
["GLOSSARY_COMPLIANCE_FAILED" /* GLOSSARY_COMPLIANCE_FAILED */]: "Glossary compliance failed. Missing terms: {missed}. Use --no-strict-glossary to allow partial compliance.",
|
|
95
|
+
["FILE_NOT_FOUND" /* FILE_NOT_FOUND */]: "The specified file was not found.",
|
|
96
|
+
["FILE_READ_ERROR" /* FILE_READ_ERROR */]: "Failed to read the file.",
|
|
97
|
+
["FILE_WRITE_ERROR" /* FILE_WRITE_ERROR */]: "Failed to write to the output file.",
|
|
98
|
+
["UNSUPPORTED_FORMAT" /* UNSUPPORTED_FORMAT */]: "The file format is not supported. Supported formats: markdown, html, text.",
|
|
99
|
+
["CHUNK_TOO_LARGE" /* CHUNK_TOO_LARGE */]: "A chunk exceeds the maximum token limit and cannot be processed.",
|
|
100
|
+
["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: "An unexpected error occurred."
|
|
101
|
+
};
|
|
102
|
+
TranslationError = class _TranslationError extends Error {
|
|
103
|
+
code;
|
|
104
|
+
details;
|
|
105
|
+
constructor(code, details, customMessage) {
|
|
106
|
+
const message = customMessage ?? formatErrorMessage(code, details);
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = "TranslationError";
|
|
109
|
+
this.code = code;
|
|
110
|
+
this.details = details;
|
|
111
|
+
if (Error.captureStackTrace) {
|
|
112
|
+
Error.captureStackTrace(this, _TranslationError);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a JSON representation of the error
|
|
117
|
+
*/
|
|
118
|
+
toJSON() {
|
|
119
|
+
return {
|
|
120
|
+
name: this.name,
|
|
121
|
+
code: this.code,
|
|
122
|
+
message: this.message,
|
|
123
|
+
details: this.details
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
ExitCode = {
|
|
128
|
+
SUCCESS: 0,
|
|
129
|
+
GENERAL_ERROR: 1,
|
|
130
|
+
INVALID_ARGUMENTS: 2,
|
|
131
|
+
FILE_NOT_FOUND: 3,
|
|
132
|
+
QUALITY_THRESHOLD_NOT_MET: 4,
|
|
133
|
+
PROVIDER_ERROR: 5,
|
|
134
|
+
GLOSSARY_VALIDATION_FAILED: 6
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
async function loadConfig(options = {}) {
|
|
139
|
+
const { configPath, cwd = process.cwd() } = options;
|
|
140
|
+
let result;
|
|
141
|
+
try {
|
|
142
|
+
if (configPath) {
|
|
143
|
+
result = await explorer.load(configPath);
|
|
144
|
+
} else {
|
|
145
|
+
result = await explorer.search(cwd);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new TranslationError("CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */, {
|
|
149
|
+
path: configPath ?? cwd,
|
|
150
|
+
error: error instanceof Error ? error.message : String(error)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!result || result.isEmpty) {
|
|
154
|
+
return defaultConfig;
|
|
155
|
+
}
|
|
156
|
+
const parseResult = configSchema.safeParse(result.config);
|
|
157
|
+
if (!parseResult.success) {
|
|
158
|
+
throw new TranslationError("CONFIG_INVALID" /* CONFIG_INVALID */, {
|
|
159
|
+
path: result.filepath,
|
|
160
|
+
errors: parseResult.error.errors.map((e) => ({
|
|
161
|
+
path: e.path.join("."),
|
|
162
|
+
message: e.message
|
|
163
|
+
}))
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return parseResult.data;
|
|
167
|
+
}
|
|
168
|
+
function mergeConfig(config2, overrides) {
|
|
169
|
+
const merged = { ...config2 };
|
|
170
|
+
if (overrides.sourceLang) {
|
|
171
|
+
merged.languages = { ...merged.languages, source: overrides.sourceLang };
|
|
172
|
+
}
|
|
173
|
+
if (overrides.targetLang) {
|
|
174
|
+
merged.languages = {
|
|
175
|
+
...merged.languages,
|
|
176
|
+
targets: [overrides.targetLang]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (overrides.provider) {
|
|
180
|
+
merged.provider = { ...merged.provider, default: overrides.provider };
|
|
181
|
+
}
|
|
182
|
+
if (overrides.model) {
|
|
183
|
+
merged.provider = { ...merged.provider, model: overrides.model };
|
|
184
|
+
}
|
|
185
|
+
if (overrides.quality !== void 0) {
|
|
186
|
+
merged.quality = { ...merged.quality, threshold: overrides.quality };
|
|
187
|
+
}
|
|
188
|
+
if (overrides.maxIterations !== void 0) {
|
|
189
|
+
merged.quality = {
|
|
190
|
+
...merged.quality,
|
|
191
|
+
maxIterations: overrides.maxIterations
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (overrides.chunkSize !== void 0) {
|
|
195
|
+
merged.chunking = { ...merged.chunking, maxTokens: overrides.chunkSize };
|
|
196
|
+
}
|
|
197
|
+
if (overrides.glossary) {
|
|
198
|
+
merged.glossary = {
|
|
199
|
+
path: overrides.glossary,
|
|
200
|
+
strict: merged.glossary?.strict ?? false
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (overrides.output) {
|
|
204
|
+
merged.paths = { ...merged.paths, output: overrides.output };
|
|
205
|
+
}
|
|
206
|
+
if (overrides.noCache) {
|
|
207
|
+
merged.paths = { ...merged.paths, cache: void 0 };
|
|
208
|
+
}
|
|
209
|
+
return merged;
|
|
210
|
+
}
|
|
211
|
+
var providerNameSchema, configSchema, defaultConfig, explorer;
|
|
212
|
+
var init_config = __esm({
|
|
213
|
+
"src/services/config.ts"() {
|
|
214
|
+
init_errors();
|
|
215
|
+
providerNameSchema = z.enum(["claude", "openai", "ollama", "custom"]);
|
|
216
|
+
configSchema = z.object({
|
|
217
|
+
version: z.string(),
|
|
218
|
+
project: z.object({
|
|
219
|
+
name: z.string(),
|
|
220
|
+
description: z.string(),
|
|
221
|
+
purpose: z.string()
|
|
222
|
+
}).optional(),
|
|
223
|
+
languages: z.object({
|
|
224
|
+
source: z.string(),
|
|
225
|
+
targets: z.array(z.string()),
|
|
226
|
+
styles: z.record(z.string(), z.string()).optional()
|
|
227
|
+
}),
|
|
228
|
+
provider: z.object({
|
|
229
|
+
default: providerNameSchema,
|
|
230
|
+
model: z.string().optional(),
|
|
231
|
+
fallback: z.array(providerNameSchema).optional(),
|
|
232
|
+
apiKeys: z.record(providerNameSchema, z.string()).optional()
|
|
233
|
+
}),
|
|
234
|
+
quality: z.object({
|
|
235
|
+
threshold: z.number().min(0).max(100),
|
|
236
|
+
maxIterations: z.number().min(1).max(10),
|
|
237
|
+
evaluationMethod: z.enum(["llm", "embedding", "hybrid"])
|
|
238
|
+
}),
|
|
239
|
+
chunking: z.object({
|
|
240
|
+
maxTokens: z.number().min(100).max(8e3),
|
|
241
|
+
overlapTokens: z.number().min(0),
|
|
242
|
+
preserveStructure: z.boolean()
|
|
243
|
+
}),
|
|
244
|
+
glossary: z.object({
|
|
245
|
+
path: z.string(),
|
|
246
|
+
strict: z.boolean()
|
|
247
|
+
}).optional(),
|
|
248
|
+
paths: z.object({
|
|
249
|
+
output: z.string(),
|
|
250
|
+
cache: z.string().optional()
|
|
251
|
+
}),
|
|
252
|
+
ignore: z.array(z.string()).optional()
|
|
253
|
+
});
|
|
254
|
+
defaultConfig = {
|
|
255
|
+
version: "1.0",
|
|
256
|
+
languages: {
|
|
257
|
+
source: "en",
|
|
258
|
+
targets: []
|
|
259
|
+
},
|
|
260
|
+
provider: {
|
|
261
|
+
default: "claude"
|
|
262
|
+
},
|
|
263
|
+
quality: {
|
|
264
|
+
threshold: 85,
|
|
265
|
+
maxIterations: 4,
|
|
266
|
+
evaluationMethod: "llm"
|
|
267
|
+
},
|
|
268
|
+
chunking: {
|
|
269
|
+
maxTokens: 1024,
|
|
270
|
+
overlapTokens: 150,
|
|
271
|
+
preserveStructure: true
|
|
272
|
+
},
|
|
273
|
+
paths: {
|
|
274
|
+
output: "./{lang}"
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
explorer = cosmiconfig("translate", {
|
|
278
|
+
searchPlaces: [
|
|
279
|
+
".translaterc",
|
|
280
|
+
".translaterc.json",
|
|
281
|
+
".translaterc.yaml",
|
|
282
|
+
".translaterc.yml",
|
|
283
|
+
"translate.config.js",
|
|
284
|
+
"translate.config.mjs"
|
|
285
|
+
]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// src/types/mqm.ts
|
|
291
|
+
function calculateMQMScore(errors) {
|
|
292
|
+
const totalPenalty = errors.reduce(
|
|
293
|
+
(sum, err) => sum + MQM_SEVERITY_WEIGHTS[err.severity],
|
|
294
|
+
0
|
|
295
|
+
);
|
|
296
|
+
return Math.max(0, 100 - totalPenalty);
|
|
297
|
+
}
|
|
298
|
+
function calculateMQMBreakdown(errors) {
|
|
299
|
+
return {
|
|
300
|
+
accuracy: errors.filter((e) => e.type.startsWith("accuracy/")).length,
|
|
301
|
+
fluency: errors.filter((e) => e.type.startsWith("fluency/")).length,
|
|
302
|
+
style: errors.filter((e) => e.type.startsWith("style/")).length
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function parseMQMResponse(response) {
|
|
306
|
+
try {
|
|
307
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
308
|
+
if (!jsonMatch) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
312
|
+
const errors = parsed.errors ?? [];
|
|
313
|
+
const score = parsed.score ?? calculateMQMScore(errors);
|
|
314
|
+
return {
|
|
315
|
+
errors,
|
|
316
|
+
score,
|
|
317
|
+
summary: parsed.summary ?? "",
|
|
318
|
+
breakdown: calculateMQMBreakdown(errors)
|
|
319
|
+
};
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function formatMQMErrorsForPrompt(errors) {
|
|
325
|
+
if (errors.length === 0) {
|
|
326
|
+
return "No errors identified.";
|
|
327
|
+
}
|
|
328
|
+
return errors.map((err, i) => {
|
|
329
|
+
const severity = err.severity.toUpperCase();
|
|
330
|
+
return `${i + 1}. [${severity}] ${err.type}
|
|
331
|
+
Text: "${err.span}"
|
|
332
|
+
Fix: "${err.suggestion}"${err.explanation ? `
|
|
333
|
+
Reason: ${err.explanation}` : ""}`;
|
|
334
|
+
}).join("\n\n");
|
|
335
|
+
}
|
|
336
|
+
var MQM_SEVERITY_WEIGHTS;
|
|
337
|
+
var init_mqm = __esm({
|
|
338
|
+
"src/types/mqm.ts"() {
|
|
339
|
+
MQM_SEVERITY_WEIGHTS = {
|
|
340
|
+
minor: 1,
|
|
341
|
+
// Noticeable but doesn't affect understanding
|
|
342
|
+
major: 5,
|
|
343
|
+
// Affects understanding or usability
|
|
344
|
+
critical: 25
|
|
345
|
+
// Completely wrong or unusable
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/types/analysis.ts
|
|
351
|
+
function parseAnalysisResponse(response) {
|
|
352
|
+
try {
|
|
353
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
354
|
+
if (!jsonMatch) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
358
|
+
return {
|
|
359
|
+
keyTerms: parsed.keyTerms ?? [],
|
|
360
|
+
ambiguousPhrases: parsed.ambiguousPhrases ?? [],
|
|
361
|
+
preserveExact: parsed.preserveExact ?? [],
|
|
362
|
+
challenges: parsed.challenges ?? [],
|
|
363
|
+
domain: parsed.domain ?? "general",
|
|
364
|
+
registerRecommendation: parsed.registerRecommendation ?? "neutral"
|
|
365
|
+
};
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function formatAnalysisForPrompt(analysis) {
|
|
371
|
+
const sections = [];
|
|
372
|
+
if (analysis.keyTerms.length > 0) {
|
|
373
|
+
const terms = analysis.keyTerms.map((t) => {
|
|
374
|
+
const translation = t.suggestedTranslation ? ` \u2192 ${t.suggestedTranslation}` : "";
|
|
375
|
+
const source = t.fromGlossary ? " (glossary)" : "";
|
|
376
|
+
return `- "${t.term}"${translation}${source}: ${t.context}`;
|
|
377
|
+
}).join("\n");
|
|
378
|
+
sections.push(`**Key Terms:**
|
|
379
|
+
${terms}`);
|
|
380
|
+
}
|
|
381
|
+
if (analysis.ambiguousPhrases.length > 0) {
|
|
382
|
+
const phrases = analysis.ambiguousPhrases.map((p) => `- "${p.phrase}": Use interpretation "${p.recommendation}"`).join("\n");
|
|
383
|
+
sections.push(`**Ambiguous Phrases (use these interpretations):**
|
|
384
|
+
${phrases}`);
|
|
385
|
+
}
|
|
386
|
+
if (analysis.preserveExact.length > 0) {
|
|
387
|
+
sections.push(
|
|
388
|
+
`**Do NOT translate (keep exactly as-is):**
|
|
389
|
+
${analysis.preserveExact.map((s) => `- ${s}`).join("\n")}`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
sections.push(
|
|
393
|
+
`**Content Type:** ${analysis.domain}
|
|
394
|
+
**Tone:** ${analysis.registerRecommendation}`
|
|
395
|
+
);
|
|
396
|
+
return sections.join("\n\n");
|
|
397
|
+
}
|
|
398
|
+
function createEmptyAnalysis() {
|
|
399
|
+
return {
|
|
400
|
+
keyTerms: [],
|
|
401
|
+
ambiguousPhrases: [],
|
|
402
|
+
preserveExact: [],
|
|
403
|
+
challenges: [],
|
|
404
|
+
domain: "general",
|
|
405
|
+
registerRecommendation: "neutral"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
var init_analysis = __esm({
|
|
409
|
+
"src/types/analysis.ts"() {
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// src/types/modes.ts
|
|
414
|
+
function getModeConfig(mode, overrides) {
|
|
415
|
+
const preset = MODE_PRESETS[mode];
|
|
416
|
+
{
|
|
417
|
+
return preset;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
var MODE_PRESETS;
|
|
421
|
+
var init_modes = __esm({
|
|
422
|
+
"src/types/modes.ts"() {
|
|
423
|
+
MODE_PRESETS = {
|
|
424
|
+
/**
|
|
425
|
+
* Fast mode: Single pass, no evaluation
|
|
426
|
+
* Best for: Quick drafts, large batches, local models
|
|
427
|
+
* Speed: ~1x (fastest)
|
|
428
|
+
*/
|
|
429
|
+
fast: {
|
|
430
|
+
enableAnalysis: false,
|
|
431
|
+
useMQMEvaluation: false,
|
|
432
|
+
maxIterations: 1,
|
|
433
|
+
qualityThreshold: 0
|
|
434
|
+
// Skip threshold check
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Balanced mode: TEaR with MQM evaluation
|
|
438
|
+
* Best for: General use, good quality with reasonable speed
|
|
439
|
+
* Speed: ~2-3x
|
|
440
|
+
*/
|
|
441
|
+
balanced: {
|
|
442
|
+
enableAnalysis: false,
|
|
443
|
+
useMQMEvaluation: true,
|
|
444
|
+
maxIterations: 2,
|
|
445
|
+
qualityThreshold: 75
|
|
446
|
+
},
|
|
447
|
+
/**
|
|
448
|
+
* Quality mode: Full MAPS + TEaR pipeline
|
|
449
|
+
* Best for: Production content, critical documents
|
|
450
|
+
* Speed: ~4-5x
|
|
451
|
+
*/
|
|
452
|
+
quality: {
|
|
453
|
+
enableAnalysis: true,
|
|
454
|
+
useMQMEvaluation: true,
|
|
455
|
+
maxIterations: 4,
|
|
456
|
+
qualityThreshold: 85
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
async function loadGlossary(path) {
|
|
462
|
+
let content;
|
|
463
|
+
try {
|
|
464
|
+
content = await readFile(path, "utf-8");
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw new TranslationError("GLOSSARY_NOT_FOUND" /* GLOSSARY_NOT_FOUND */, {
|
|
467
|
+
path,
|
|
468
|
+
error: error instanceof Error ? error.message : String(error)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(content);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
throw new TranslationError("GLOSSARY_INVALID" /* GLOSSARY_INVALID */, {
|
|
475
|
+
path,
|
|
476
|
+
error: error instanceof Error ? error.message : String(error)
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function resolveGlossary(glossary, targetLang) {
|
|
481
|
+
return {
|
|
482
|
+
metadata: {
|
|
483
|
+
name: glossary.metadata.name,
|
|
484
|
+
sourceLang: glossary.metadata.sourceLang,
|
|
485
|
+
targetLang,
|
|
486
|
+
version: glossary.metadata.version,
|
|
487
|
+
domain: glossary.metadata.domain
|
|
488
|
+
},
|
|
489
|
+
terms: glossary.terms.map((term) => resolveGlossaryTerm(term, targetLang)).filter((term) => term !== null)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function resolveGlossaryTerm(term, targetLang) {
|
|
493
|
+
const target = resolveTarget(term, targetLang);
|
|
494
|
+
if (target === void 0) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
source: term.source,
|
|
499
|
+
target,
|
|
500
|
+
context: term.context,
|
|
501
|
+
caseSensitive: term.caseSensitive ?? false,
|
|
502
|
+
doNotTranslate: resolveDoNotTranslate(term, targetLang)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function resolveTarget(term, targetLang) {
|
|
506
|
+
if (term.doNotTranslate) {
|
|
507
|
+
return term.source;
|
|
508
|
+
}
|
|
509
|
+
if (term.doNotTranslateFor?.includes(targetLang)) {
|
|
510
|
+
return term.source;
|
|
511
|
+
}
|
|
512
|
+
const translation = term.targets[targetLang];
|
|
513
|
+
if (translation) {
|
|
514
|
+
return translation;
|
|
515
|
+
}
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
function resolveDoNotTranslate(term, targetLang) {
|
|
519
|
+
return term.doNotTranslate === true || term.doNotTranslateFor?.includes(targetLang) === true;
|
|
520
|
+
}
|
|
521
|
+
function createGlossaryLookup(glossary) {
|
|
522
|
+
const termMap = /* @__PURE__ */ new Map();
|
|
523
|
+
const caseSensitiveTerms = [];
|
|
524
|
+
const caseInsensitiveTerms = [];
|
|
525
|
+
for (const term of glossary.terms) {
|
|
526
|
+
if (term.caseSensitive) {
|
|
527
|
+
termMap.set(term.source, term);
|
|
528
|
+
caseSensitiveTerms.push(term);
|
|
529
|
+
} else {
|
|
530
|
+
termMap.set(term.source.toLowerCase(), term);
|
|
531
|
+
caseInsensitiveTerms.push(term);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
find(text) {
|
|
536
|
+
const exact = termMap.get(text);
|
|
537
|
+
if (exact) return exact;
|
|
538
|
+
return termMap.get(text.toLowerCase());
|
|
539
|
+
},
|
|
540
|
+
findAll(text) {
|
|
541
|
+
const matches = [];
|
|
542
|
+
for (const term of caseSensitiveTerms) {
|
|
543
|
+
if (text.includes(term.source)) {
|
|
544
|
+
matches.push(term);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const lowerText = text.toLowerCase();
|
|
548
|
+
for (const term of caseInsensitiveTerms) {
|
|
549
|
+
if (lowerText.includes(term.source.toLowerCase())) {
|
|
550
|
+
matches.push(term);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return matches;
|
|
554
|
+
},
|
|
555
|
+
getTerms() {
|
|
556
|
+
return glossary.terms;
|
|
557
|
+
},
|
|
558
|
+
formatForPrompt() {
|
|
559
|
+
const lines = [];
|
|
560
|
+
for (const term of glossary.terms) {
|
|
561
|
+
const flags = [];
|
|
562
|
+
if (term.caseSensitive) {
|
|
563
|
+
flags.push("case-sensitive");
|
|
564
|
+
} else {
|
|
565
|
+
flags.push("case-insensitive");
|
|
566
|
+
}
|
|
567
|
+
if (term.context) {
|
|
568
|
+
flags.push(`context: ${term.context}`);
|
|
569
|
+
}
|
|
570
|
+
const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
571
|
+
if (term.doNotTranslate) {
|
|
572
|
+
lines.push(`- "${term.source}" \u2192 [DO NOT TRANSLATE, keep as-is]${flagStr}`);
|
|
573
|
+
} else {
|
|
574
|
+
lines.push(`- "${term.source}" \u2192 "${term.target}"${flagStr}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return lines.join("\n");
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
var init_glossary = __esm({
|
|
582
|
+
"src/services/glossary.ts"() {
|
|
583
|
+
init_errors();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
function configureLogger(options) {
|
|
587
|
+
config = { ...config, ...options };
|
|
588
|
+
}
|
|
589
|
+
function shouldLog(level) {
|
|
590
|
+
if (config.quiet && level !== "error") {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[config.level];
|
|
594
|
+
}
|
|
595
|
+
function formatMessage(level, message, data) {
|
|
596
|
+
if (config.json) {
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
level,
|
|
599
|
+
message,
|
|
600
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
601
|
+
...data
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
605
|
+
const prefix = `[${timestamp}]`;
|
|
606
|
+
switch (level) {
|
|
607
|
+
case "debug":
|
|
608
|
+
return chalk.gray(`${prefix} ${message}`);
|
|
609
|
+
case "info":
|
|
610
|
+
return `${prefix} ${message}`;
|
|
611
|
+
case "warn":
|
|
612
|
+
return chalk.yellow(`${prefix} \u26A0 ${message}`);
|
|
613
|
+
case "error":
|
|
614
|
+
return chalk.red(`${prefix} \u2717 ${message}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function createTimer() {
|
|
618
|
+
const start = performance.now();
|
|
619
|
+
return {
|
|
620
|
+
elapsed() {
|
|
621
|
+
return performance.now() - start;
|
|
622
|
+
},
|
|
623
|
+
format() {
|
|
624
|
+
const ms = this.elapsed();
|
|
625
|
+
if (ms < 1e3) {
|
|
626
|
+
return `${ms.toFixed(0)}ms`;
|
|
627
|
+
}
|
|
628
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
var LOG_LEVEL_PRIORITY, config, logger;
|
|
633
|
+
var init_logger = __esm({
|
|
634
|
+
"src/utils/logger.ts"() {
|
|
635
|
+
LOG_LEVEL_PRIORITY = {
|
|
636
|
+
debug: 0,
|
|
637
|
+
info: 1,
|
|
638
|
+
warn: 2,
|
|
639
|
+
error: 3
|
|
640
|
+
};
|
|
641
|
+
config = {
|
|
642
|
+
level: "info",
|
|
643
|
+
quiet: false,
|
|
644
|
+
json: false
|
|
645
|
+
};
|
|
646
|
+
logger = {
|
|
647
|
+
debug(message, data) {
|
|
648
|
+
if (shouldLog("debug")) {
|
|
649
|
+
console.log(formatMessage("debug", message, data));
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
info(message, data) {
|
|
653
|
+
if (shouldLog("info")) {
|
|
654
|
+
console.log(formatMessage("info", message, data));
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
warn(message, data) {
|
|
658
|
+
if (shouldLog("warn")) {
|
|
659
|
+
console.warn(formatMessage("warn", message, data));
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
error(message, data) {
|
|
663
|
+
if (shouldLog("error")) {
|
|
664
|
+
console.error(formatMessage("error", message, data));
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
success(message) {
|
|
668
|
+
if (!config.quiet) {
|
|
669
|
+
console.log(chalk.green(`\u2713 ${message}`));
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
progress(current, total, message) {
|
|
673
|
+
if (!config.quiet && !config.json) {
|
|
674
|
+
const percent = Math.round(current / total * 100);
|
|
675
|
+
const bar = "\u2588".repeat(Math.round(percent / 5)) + "\u2591".repeat(20 - Math.round(percent / 5));
|
|
676
|
+
process.stdout.write(`\r[${bar}] ${percent}% ${message}`);
|
|
677
|
+
if (current === total) {
|
|
678
|
+
console.log();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// src/core/agent.ts
|
|
687
|
+
function buildSystemInstructions(sourceLang, targetLang) {
|
|
688
|
+
return `You are a professional translator specializing in ${sourceLang} to ${targetLang} translation.
|
|
689
|
+
|
|
690
|
+
## Rules:
|
|
691
|
+
1. Apply glossary terms exactly as specified
|
|
692
|
+
2. Preserve all formatting (markdown, HTML tags, code blocks)
|
|
693
|
+
3. Maintain the same tone and style
|
|
694
|
+
4. Do not translate content inside code blocks
|
|
695
|
+
5. Keep URLs, file paths, and technical identifiers unchanged
|
|
696
|
+
6. Keep placeholders like __CODE_BLOCK_0__ unchanged`;
|
|
697
|
+
}
|
|
698
|
+
function buildGlossarySection(glossaryText) {
|
|
699
|
+
return `## Glossary (MUST use these exact translations):
|
|
700
|
+
${glossaryText || "No glossary provided."}`;
|
|
701
|
+
}
|
|
702
|
+
function buildTranslationContent(sourceText, context) {
|
|
703
|
+
const styleSection = context?.styleInstruction ? `Style: ${context.styleInstruction}
|
|
704
|
+
` : "";
|
|
705
|
+
return `## Document Context:
|
|
706
|
+
Purpose: ${context?.documentPurpose ?? "General translation"}
|
|
707
|
+
${styleSection}Previous content: ${context?.previousContext ?? "None"}
|
|
708
|
+
|
|
709
|
+
## Source Text:
|
|
710
|
+
${sourceText}
|
|
711
|
+
|
|
712
|
+
Provide ONLY the translated text below, with no additional commentary or headers:`;
|
|
713
|
+
}
|
|
714
|
+
function buildCacheableTranslationMessage(sourceText, sourceLang, targetLang, glossaryText, context) {
|
|
715
|
+
const systemInstructions = buildSystemInstructions(sourceLang, targetLang);
|
|
716
|
+
const glossarySection = buildGlossarySection(glossaryText);
|
|
717
|
+
const translationContent = buildTranslationContent(sourceText, context);
|
|
718
|
+
const contentParts = [
|
|
719
|
+
{
|
|
720
|
+
type: "text",
|
|
721
|
+
text: systemInstructions,
|
|
722
|
+
cacheControl: { type: "ephemeral" }
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
type: "text",
|
|
726
|
+
text: glossarySection,
|
|
727
|
+
cacheControl: { type: "ephemeral" }
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
type: "text",
|
|
731
|
+
text: translationContent
|
|
732
|
+
// No cache control - this is dynamic per request
|
|
733
|
+
}
|
|
734
|
+
];
|
|
735
|
+
return {
|
|
736
|
+
role: "user",
|
|
737
|
+
content: contentParts
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function buildInitialTranslationPrompt(sourceText, sourceLang, targetLang, glossaryText, context) {
|
|
741
|
+
const styleSection = context?.styleInstruction ? `Style: ${context.styleInstruction}
|
|
742
|
+
` : "";
|
|
743
|
+
return `You are a professional translator. Translate the following ${sourceLang} text to ${targetLang}.
|
|
744
|
+
|
|
745
|
+
## Glossary (MUST use these exact translations):
|
|
746
|
+
${glossaryText || "No glossary provided."}
|
|
747
|
+
|
|
748
|
+
## Document Context:
|
|
749
|
+
Purpose: ${context?.documentPurpose ?? "General translation"}
|
|
750
|
+
${styleSection}Previous content: ${context?.previousContext ?? "None"}
|
|
751
|
+
|
|
752
|
+
## Rules:
|
|
753
|
+
1. Apply glossary terms exactly as specified
|
|
754
|
+
2. Preserve all formatting (markdown, HTML tags, code blocks)
|
|
755
|
+
3. Maintain the same tone and style
|
|
756
|
+
4. Do not translate content inside code blocks
|
|
757
|
+
5. Keep URLs, file paths, and technical identifiers unchanged
|
|
758
|
+
6. Keep placeholders like __CODE_BLOCK_0__ unchanged
|
|
759
|
+
|
|
760
|
+
## Source Text:
|
|
761
|
+
${sourceText}
|
|
762
|
+
|
|
763
|
+
Provide ONLY the translated text below, with no additional commentary or headers:`;
|
|
764
|
+
}
|
|
765
|
+
function buildReflectionPrompt(sourceText, translatedText, sourceLang, targetLang, glossaryText) {
|
|
766
|
+
return `Review this translation and provide specific improvement suggestions.
|
|
767
|
+
|
|
768
|
+
## Source (${sourceLang}):
|
|
769
|
+
${sourceText}
|
|
770
|
+
|
|
771
|
+
## Translation (${targetLang}):
|
|
772
|
+
${translatedText}
|
|
773
|
+
|
|
774
|
+
## Glossary Requirements:
|
|
775
|
+
${glossaryText || "No glossary provided."}
|
|
776
|
+
|
|
777
|
+
## Evaluate and suggest improvements for:
|
|
778
|
+
1. **Accuracy**: Does the translation convey the exact meaning?
|
|
779
|
+
2. **Glossary Compliance**: Are all glossary terms applied correctly?
|
|
780
|
+
3. **Fluency**: Does it read naturally in ${targetLang}?
|
|
781
|
+
4. **Formatting**: Is the structure preserved?
|
|
782
|
+
5. **Consistency**: Are terms translated consistently?
|
|
783
|
+
|
|
784
|
+
Provide a numbered list of specific, actionable suggestions:`;
|
|
785
|
+
}
|
|
786
|
+
function buildImprovementPrompt(sourceText, currentTranslation, suggestions, glossaryText) {
|
|
787
|
+
return `Improve this translation based on the following suggestions.
|
|
788
|
+
|
|
789
|
+
## Source Text:
|
|
790
|
+
${sourceText}
|
|
791
|
+
|
|
792
|
+
## Current Translation:
|
|
793
|
+
${currentTranslation}
|
|
794
|
+
|
|
795
|
+
## Improvement Suggestions:
|
|
796
|
+
${suggestions}
|
|
797
|
+
|
|
798
|
+
## Glossary (MUST apply):
|
|
799
|
+
${glossaryText || "No glossary provided."}
|
|
800
|
+
|
|
801
|
+
Provide ONLY the improved translation below, with no additional commentary or headers:`;
|
|
802
|
+
}
|
|
803
|
+
function buildQualityEvaluationPrompt(sourceText, translatedText, sourceLang, targetLang) {
|
|
804
|
+
return `Rate this translation's quality from 0 to 100.
|
|
805
|
+
|
|
806
|
+
## Source (${sourceLang}):
|
|
807
|
+
${sourceText}
|
|
808
|
+
|
|
809
|
+
## Translation (${targetLang}):
|
|
810
|
+
${translatedText}
|
|
811
|
+
|
|
812
|
+
## Evaluation Criteria:
|
|
813
|
+
- Semantic accuracy (40 points)
|
|
814
|
+
- Fluency and naturalness (25 points)
|
|
815
|
+
- Glossary compliance (20 points)
|
|
816
|
+
- Format preservation (15 points)
|
|
817
|
+
|
|
818
|
+
Respond with only a JSON object:
|
|
819
|
+
{"score": <number>, "breakdown": {"accuracy": <n>, "fluency": <n>, "glossary": <n>, "format": <n>}, "issues": ["issue1", "issue2"]}`;
|
|
820
|
+
}
|
|
821
|
+
function buildMQMEvaluationPrompt(sourceText, translatedText, sourceLang, targetLang, glossaryText) {
|
|
822
|
+
return `Evaluate this translation using MQM (Multidimensional Quality Metrics) framework.
|
|
823
|
+
|
|
824
|
+
## Source (${sourceLang}):
|
|
825
|
+
${sourceText}
|
|
826
|
+
|
|
827
|
+
## Translation (${targetLang}):
|
|
828
|
+
${translatedText}
|
|
829
|
+
|
|
830
|
+
## Glossary Terms (must be applied exactly):
|
|
831
|
+
${glossaryText || "No glossary provided."}
|
|
832
|
+
|
|
833
|
+
## MQM Error Categories:
|
|
834
|
+
- accuracy/mistranslation: Incorrect meaning
|
|
835
|
+
- accuracy/omission: Missing content from source
|
|
836
|
+
- accuracy/addition: Extra content not in source
|
|
837
|
+
- accuracy/untranslated: Source text left unchanged
|
|
838
|
+
- fluency/grammar: Grammatical errors
|
|
839
|
+
- fluency/spelling: Spelling/typos
|
|
840
|
+
- fluency/register: Inappropriate formality
|
|
841
|
+
- fluency/inconsistency: Inconsistent terminology
|
|
842
|
+
- style/awkward: Unnatural phrasing
|
|
843
|
+
- style/unidiomatic: Non-native expressions
|
|
844
|
+
|
|
845
|
+
## Severity Weights:
|
|
846
|
+
- "minor" (1 point): Noticeable but doesn't affect understanding
|
|
847
|
+
- "major" (5 points): Affects understanding or usability
|
|
848
|
+
- "critical" (25 points): Completely wrong or unusable
|
|
849
|
+
|
|
850
|
+
## Instructions:
|
|
851
|
+
1. Identify all translation errors
|
|
852
|
+
2. Classify each by type and severity
|
|
853
|
+
3. Provide the span and suggested fix
|
|
854
|
+
4. Calculate score: 100 - sum(weights)
|
|
855
|
+
|
|
856
|
+
Respond with only a JSON object:
|
|
857
|
+
{
|
|
858
|
+
"errors": [
|
|
859
|
+
{"type": "accuracy/mistranslation", "severity": "major", "span": "affected text", "suggestion": "corrected text", "explanation": "reason"}
|
|
860
|
+
],
|
|
861
|
+
"score": <100 - sum of weights>,
|
|
862
|
+
"summary": "brief overall assessment"
|
|
863
|
+
}`;
|
|
864
|
+
}
|
|
865
|
+
function buildMQMRefinementPrompt(sourceText, currentTranslation, errors, glossaryText) {
|
|
866
|
+
const errorList = formatMQMErrorsForPrompt(errors);
|
|
867
|
+
return `Fix the following translation errors.
|
|
868
|
+
|
|
869
|
+
## Source Text:
|
|
870
|
+
${sourceText}
|
|
871
|
+
|
|
872
|
+
## Current Translation:
|
|
873
|
+
${currentTranslation}
|
|
874
|
+
|
|
875
|
+
## Errors to Fix:
|
|
876
|
+
${errorList}
|
|
877
|
+
|
|
878
|
+
## Glossary (MUST apply):
|
|
879
|
+
${glossaryText || "No glossary provided."}
|
|
880
|
+
|
|
881
|
+
Apply ONLY the fixes listed above. Do not make other changes.
|
|
882
|
+
Provide ONLY the corrected translation, with no additional commentary:`;
|
|
883
|
+
}
|
|
884
|
+
function buildPreAnalysisPrompt(sourceText, sourceLang, targetLang, glossaryText) {
|
|
885
|
+
return `Analyze this ${sourceLang} text before translating to ${targetLang}.
|
|
886
|
+
|
|
887
|
+
## Source Text:
|
|
888
|
+
${sourceText}
|
|
889
|
+
|
|
890
|
+
## Available Glossary Terms:
|
|
891
|
+
${glossaryText || "No glossary provided."}
|
|
892
|
+
|
|
893
|
+
## Analyze and extract:
|
|
894
|
+
1. **Key Terms**: Important domain-specific terms needing careful translation
|
|
895
|
+
2. **Ambiguous Phrases**: Phrases with multiple possible interpretations
|
|
896
|
+
3. **Preserve Exact**: Code, URLs, names that should NOT be translated
|
|
897
|
+
4. **Challenges**: Specific difficulties for ${sourceLang}\u2192${targetLang}
|
|
898
|
+
|
|
899
|
+
Respond with only a JSON object:
|
|
900
|
+
{
|
|
901
|
+
"keyTerms": [{"term": "...", "context": "...", "suggestedTranslation": "...", "fromGlossary": true/false}],
|
|
902
|
+
"ambiguousPhrases": [{"phrase": "...", "interpretations": ["..."], "recommendation": "..."}],
|
|
903
|
+
"preserveExact": ["code snippets", "URLs", "names"],
|
|
904
|
+
"challenges": ["challenge 1", "challenge 2"],
|
|
905
|
+
"domain": "technical|marketing|legal|medical|general",
|
|
906
|
+
"registerRecommendation": "formal|informal|neutral"
|
|
907
|
+
}`;
|
|
908
|
+
}
|
|
909
|
+
function createTranslationAgent(options) {
|
|
910
|
+
return new TranslationAgent(options);
|
|
911
|
+
}
|
|
912
|
+
var TranslationAgent;
|
|
913
|
+
var init_agent = __esm({
|
|
914
|
+
"src/core/agent.ts"() {
|
|
915
|
+
init_mqm();
|
|
916
|
+
init_analysis();
|
|
917
|
+
init_modes();
|
|
918
|
+
init_glossary();
|
|
919
|
+
init_logger();
|
|
920
|
+
init_errors();
|
|
921
|
+
TranslationAgent = class {
|
|
922
|
+
provider;
|
|
923
|
+
qualityThreshold;
|
|
924
|
+
maxIterations;
|
|
925
|
+
verbose;
|
|
926
|
+
strictQuality;
|
|
927
|
+
enableCaching;
|
|
928
|
+
enableAnalysis;
|
|
929
|
+
useMQMEvaluation;
|
|
930
|
+
constructor(options) {
|
|
931
|
+
this.provider = options.provider;
|
|
932
|
+
this.verbose = options.verbose ?? false;
|
|
933
|
+
this.strictQuality = options.strictQuality ?? false;
|
|
934
|
+
const modeConfig = getModeConfig(options.mode ?? "balanced");
|
|
935
|
+
this.qualityThreshold = options.qualityThreshold ?? modeConfig.qualityThreshold;
|
|
936
|
+
this.maxIterations = options.maxIterations ?? modeConfig.maxIterations;
|
|
937
|
+
this.enableAnalysis = options.enableAnalysis ?? modeConfig.enableAnalysis;
|
|
938
|
+
this.useMQMEvaluation = options.useMQMEvaluation ?? modeConfig.useMQMEvaluation;
|
|
939
|
+
this.enableCaching = options.enableCaching ?? options.provider.name === "claude";
|
|
940
|
+
if (this.verbose) {
|
|
941
|
+
logger.info(`Translation mode: ${options.mode ?? "balanced"}`);
|
|
942
|
+
logger.info(` - Analysis: ${this.enableAnalysis ? "enabled" : "disabled"}`);
|
|
943
|
+
logger.info(` - MQM evaluation: ${this.useMQMEvaluation ? "enabled" : "disabled"}`);
|
|
944
|
+
logger.info(` - Quality threshold: ${this.qualityThreshold}`);
|
|
945
|
+
logger.info(` - Max iterations: ${this.maxIterations}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Translate content using Self-Refine loop with optional MAPS analysis and MQM evaluation
|
|
950
|
+
*/
|
|
951
|
+
async translate(request) {
|
|
952
|
+
const timer = createTimer();
|
|
953
|
+
let totalInputTokens = 0;
|
|
954
|
+
let totalOutputTokens = 0;
|
|
955
|
+
let totalCacheReadTokens = 0;
|
|
956
|
+
let totalCacheWriteTokens = 0;
|
|
957
|
+
let iterations = 0;
|
|
958
|
+
const glossaryText = request.glossary ? createGlossaryLookup(
|
|
959
|
+
request.glossary
|
|
960
|
+
).formatForPrompt() : "";
|
|
961
|
+
let analysis = null;
|
|
962
|
+
if (this.enableAnalysis) {
|
|
963
|
+
if (this.verbose) {
|
|
964
|
+
logger.info("Analyzing source text (MAPS)...");
|
|
965
|
+
}
|
|
966
|
+
analysis = await this.analyzeSource(
|
|
967
|
+
request.content,
|
|
968
|
+
request.sourceLang,
|
|
969
|
+
request.targetLang,
|
|
970
|
+
glossaryText
|
|
971
|
+
);
|
|
972
|
+
if (this.verbose && analysis) {
|
|
973
|
+
logger.info(` - Domain: ${analysis.domain}`);
|
|
974
|
+
logger.info(` - Key terms: ${analysis.keyTerms.length}`);
|
|
975
|
+
logger.info(` - Challenges: ${analysis.challenges.length}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (this.verbose) {
|
|
979
|
+
logger.info("Starting initial translation...");
|
|
980
|
+
}
|
|
981
|
+
const initialResult = await this.generateInitialTranslation(
|
|
982
|
+
request.content,
|
|
983
|
+
request.sourceLang,
|
|
984
|
+
request.targetLang,
|
|
985
|
+
glossaryText,
|
|
986
|
+
request.context,
|
|
987
|
+
analysis
|
|
988
|
+
);
|
|
989
|
+
let currentTranslation = initialResult.content;
|
|
990
|
+
iterations++;
|
|
991
|
+
totalInputTokens += initialResult.usage.inputTokens;
|
|
992
|
+
totalOutputTokens += initialResult.usage.outputTokens;
|
|
993
|
+
totalCacheReadTokens += initialResult.usage.cacheReadTokens ?? 0;
|
|
994
|
+
totalCacheWriteTokens += initialResult.usage.cacheWriteTokens ?? 0;
|
|
995
|
+
if (this.maxIterations <= 1 && this.qualityThreshold <= 0) {
|
|
996
|
+
if (this.verbose) {
|
|
997
|
+
logger.info("Fast mode: Skipping evaluation and refinement");
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
content: currentTranslation,
|
|
1001
|
+
metadata: {
|
|
1002
|
+
qualityScore: 0,
|
|
1003
|
+
qualityThreshold: 0,
|
|
1004
|
+
thresholdMet: true,
|
|
1005
|
+
iterations,
|
|
1006
|
+
tokensUsed: {
|
|
1007
|
+
input: totalInputTokens,
|
|
1008
|
+
output: totalOutputTokens,
|
|
1009
|
+
cacheRead: totalCacheReadTokens,
|
|
1010
|
+
cacheWrite: totalCacheWriteTokens
|
|
1011
|
+
},
|
|
1012
|
+
duration: timer.elapsed(),
|
|
1013
|
+
provider: this.provider.name,
|
|
1014
|
+
model: "default"
|
|
1015
|
+
},
|
|
1016
|
+
glossaryCompliance: request.glossary ? this.checkGlossaryCompliance(
|
|
1017
|
+
request.content,
|
|
1018
|
+
currentTranslation,
|
|
1019
|
+
request.glossary
|
|
1020
|
+
) : void 0
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
let qualityScore = 0;
|
|
1024
|
+
let lastEvaluation = null;
|
|
1025
|
+
let lastMQMEvaluation = null;
|
|
1026
|
+
while (iterations < this.maxIterations) {
|
|
1027
|
+
if (this.verbose) {
|
|
1028
|
+
logger.info(
|
|
1029
|
+
`Evaluating translation quality (iteration ${iterations})...`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
if (this.useMQMEvaluation) {
|
|
1033
|
+
lastMQMEvaluation = await this.evaluateQualityMQM(
|
|
1034
|
+
request.content,
|
|
1035
|
+
currentTranslation,
|
|
1036
|
+
request.sourceLang,
|
|
1037
|
+
request.targetLang,
|
|
1038
|
+
glossaryText
|
|
1039
|
+
);
|
|
1040
|
+
qualityScore = lastMQMEvaluation.score;
|
|
1041
|
+
if (this.verbose) {
|
|
1042
|
+
logger.info(`MQM score: ${qualityScore}/${this.qualityThreshold}`);
|
|
1043
|
+
if (lastMQMEvaluation.errors.length > 0) {
|
|
1044
|
+
logger.info(` - Errors: ${lastMQMEvaluation.errors.length} (${lastMQMEvaluation.breakdown.accuracy} accuracy, ${lastMQMEvaluation.breakdown.fluency} fluency, ${lastMQMEvaluation.breakdown.style} style)`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
lastEvaluation = await this.evaluateQuality(
|
|
1049
|
+
request.content,
|
|
1050
|
+
currentTranslation,
|
|
1051
|
+
request.sourceLang,
|
|
1052
|
+
request.targetLang
|
|
1053
|
+
);
|
|
1054
|
+
qualityScore = lastEvaluation.score;
|
|
1055
|
+
if (this.verbose) {
|
|
1056
|
+
logger.info(`Quality score: ${qualityScore}/${this.qualityThreshold}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (qualityScore >= this.qualityThreshold) {
|
|
1060
|
+
if (this.verbose) {
|
|
1061
|
+
logger.success(
|
|
1062
|
+
`Quality threshold met after ${iterations} iterations`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
if (this.verbose) {
|
|
1068
|
+
logger.info("Refining translation...");
|
|
1069
|
+
}
|
|
1070
|
+
let improveResult;
|
|
1071
|
+
if (this.useMQMEvaluation && lastMQMEvaluation && lastMQMEvaluation.errors.length > 0) {
|
|
1072
|
+
improveResult = await this.refineWithMQM(
|
|
1073
|
+
request.content,
|
|
1074
|
+
currentTranslation,
|
|
1075
|
+
lastMQMEvaluation.errors,
|
|
1076
|
+
glossaryText
|
|
1077
|
+
);
|
|
1078
|
+
} else {
|
|
1079
|
+
const suggestions = await this.generateReflection(
|
|
1080
|
+
request.content,
|
|
1081
|
+
currentTranslation,
|
|
1082
|
+
request.sourceLang,
|
|
1083
|
+
request.targetLang,
|
|
1084
|
+
glossaryText
|
|
1085
|
+
);
|
|
1086
|
+
improveResult = await this.improveTranslation(
|
|
1087
|
+
request.content,
|
|
1088
|
+
currentTranslation,
|
|
1089
|
+
suggestions,
|
|
1090
|
+
glossaryText
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
currentTranslation = improveResult.content;
|
|
1094
|
+
iterations++;
|
|
1095
|
+
totalInputTokens += improveResult.usage.inputTokens;
|
|
1096
|
+
totalOutputTokens += improveResult.usage.outputTokens;
|
|
1097
|
+
totalCacheReadTokens += improveResult.usage.cacheReadTokens ?? 0;
|
|
1098
|
+
totalCacheWriteTokens += improveResult.usage.cacheWriteTokens ?? 0;
|
|
1099
|
+
}
|
|
1100
|
+
if (this.useMQMEvaluation) {
|
|
1101
|
+
if (!lastMQMEvaluation || iterations === this.maxIterations) {
|
|
1102
|
+
lastMQMEvaluation = await this.evaluateQualityMQM(
|
|
1103
|
+
request.content,
|
|
1104
|
+
currentTranslation,
|
|
1105
|
+
request.sourceLang,
|
|
1106
|
+
request.targetLang,
|
|
1107
|
+
glossaryText
|
|
1108
|
+
);
|
|
1109
|
+
qualityScore = lastMQMEvaluation.score;
|
|
1110
|
+
}
|
|
1111
|
+
} else {
|
|
1112
|
+
if (!lastEvaluation || iterations === this.maxIterations) {
|
|
1113
|
+
lastEvaluation = await this.evaluateQuality(
|
|
1114
|
+
request.content,
|
|
1115
|
+
currentTranslation,
|
|
1116
|
+
request.sourceLang,
|
|
1117
|
+
request.targetLang
|
|
1118
|
+
);
|
|
1119
|
+
qualityScore = lastEvaluation.score;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const thresholdMet = qualityScore >= this.qualityThreshold;
|
|
1123
|
+
if (!thresholdMet && this.strictQuality) {
|
|
1124
|
+
throw new TranslationError("QUALITY_THRESHOLD_NOT_MET" /* QUALITY_THRESHOLD_NOT_MET */, {
|
|
1125
|
+
score: qualityScore,
|
|
1126
|
+
threshold: this.qualityThreshold,
|
|
1127
|
+
iterations,
|
|
1128
|
+
maxIterations: this.maxIterations,
|
|
1129
|
+
issues: lastEvaluation?.issues ?? lastMQMEvaluation?.errors.map((e) => `${e.type}: ${e.span}`) ?? []
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
if (!thresholdMet && this.verbose) {
|
|
1133
|
+
logger.warn(
|
|
1134
|
+
`Quality threshold not met: ${qualityScore}/${this.qualityThreshold} after ${iterations} iterations`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
if (this.verbose && (totalCacheReadTokens > 0 || totalCacheWriteTokens > 0)) {
|
|
1138
|
+
const cacheHitRate = totalCacheReadTokens > 0 ? (totalCacheReadTokens / (totalCacheReadTokens + totalInputTokens) * 100).toFixed(1) : "0";
|
|
1139
|
+
logger.info(
|
|
1140
|
+
`Cache stats: ${totalCacheReadTokens} read, ${totalCacheWriteTokens} written (${cacheHitRate}% hit rate)`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
content: currentTranslation,
|
|
1145
|
+
metadata: {
|
|
1146
|
+
qualityScore,
|
|
1147
|
+
qualityThreshold: this.qualityThreshold,
|
|
1148
|
+
thresholdMet,
|
|
1149
|
+
iterations,
|
|
1150
|
+
tokensUsed: {
|
|
1151
|
+
input: totalInputTokens,
|
|
1152
|
+
output: totalOutputTokens,
|
|
1153
|
+
cacheRead: totalCacheReadTokens,
|
|
1154
|
+
cacheWrite: totalCacheWriteTokens
|
|
1155
|
+
},
|
|
1156
|
+
duration: timer.elapsed(),
|
|
1157
|
+
provider: this.provider.name,
|
|
1158
|
+
model: "default"
|
|
1159
|
+
},
|
|
1160
|
+
glossaryCompliance: request.glossary ? this.checkGlossaryCompliance(
|
|
1161
|
+
request.content,
|
|
1162
|
+
currentTranslation,
|
|
1163
|
+
request.glossary
|
|
1164
|
+
) : void 0
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
// ============================================================================
|
|
1168
|
+
// Private Methods
|
|
1169
|
+
// ============================================================================
|
|
1170
|
+
async generateInitialTranslation(sourceText, sourceLang, targetLang, glossaryText, context, analysis) {
|
|
1171
|
+
let messages;
|
|
1172
|
+
const analysisContext = analysis ? formatAnalysisForPrompt(analysis) : "";
|
|
1173
|
+
const enrichedContext = {
|
|
1174
|
+
documentPurpose: context?.documentPurpose,
|
|
1175
|
+
styleInstruction: context?.styleInstruction,
|
|
1176
|
+
previousContext: context?.previousChunks?.slice(-2).join("\n")
|
|
1177
|
+
};
|
|
1178
|
+
if (this.enableCaching) {
|
|
1179
|
+
const baseMessage = buildCacheableTranslationMessage(
|
|
1180
|
+
sourceText,
|
|
1181
|
+
sourceLang,
|
|
1182
|
+
targetLang,
|
|
1183
|
+
glossaryText,
|
|
1184
|
+
enrichedContext
|
|
1185
|
+
);
|
|
1186
|
+
if (analysisContext && Array.isArray(baseMessage.content)) {
|
|
1187
|
+
const contentParts = baseMessage.content;
|
|
1188
|
+
contentParts.splice(2, 0, {
|
|
1189
|
+
type: "text",
|
|
1190
|
+
text: `
|
|
1191
|
+
## Pre-Translation Analysis:
|
|
1192
|
+
${analysisContext}
|
|
1193
|
+
`
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
messages = [baseMessage];
|
|
1197
|
+
} else {
|
|
1198
|
+
let prompt = buildInitialTranslationPrompt(
|
|
1199
|
+
sourceText,
|
|
1200
|
+
sourceLang,
|
|
1201
|
+
targetLang,
|
|
1202
|
+
glossaryText,
|
|
1203
|
+
enrichedContext
|
|
1204
|
+
);
|
|
1205
|
+
if (analysisContext) {
|
|
1206
|
+
prompt = prompt.replace(
|
|
1207
|
+
"## Source Text:",
|
|
1208
|
+
`## Pre-Translation Analysis:
|
|
1209
|
+
${analysisContext}
|
|
1210
|
+
|
|
1211
|
+
## Source Text:`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
messages = [{ role: "user", content: prompt }];
|
|
1215
|
+
}
|
|
1216
|
+
const response = await this.provider.chat({ messages });
|
|
1217
|
+
const cleanedContent = this.cleanTranslationOutput(response.content);
|
|
1218
|
+
return {
|
|
1219
|
+
content: this.preserveWhitespace(sourceText, cleanedContent),
|
|
1220
|
+
usage: {
|
|
1221
|
+
inputTokens: response.usage.inputTokens,
|
|
1222
|
+
outputTokens: response.usage.outputTokens,
|
|
1223
|
+
cacheReadTokens: response.usage.cacheReadTokens,
|
|
1224
|
+
cacheWriteTokens: response.usage.cacheWriteTokens
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async generateReflection(sourceText, translatedText, sourceLang, targetLang, glossaryText) {
|
|
1229
|
+
const prompt = buildReflectionPrompt(
|
|
1230
|
+
sourceText,
|
|
1231
|
+
translatedText,
|
|
1232
|
+
sourceLang,
|
|
1233
|
+
targetLang,
|
|
1234
|
+
glossaryText
|
|
1235
|
+
);
|
|
1236
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1237
|
+
const response = await this.provider.chat({ messages });
|
|
1238
|
+
return response.content.trim();
|
|
1239
|
+
}
|
|
1240
|
+
async improveTranslation(sourceText, currentTranslation, suggestions, glossaryText) {
|
|
1241
|
+
let messages;
|
|
1242
|
+
if (this.enableCaching) {
|
|
1243
|
+
const contentParts = [
|
|
1244
|
+
{
|
|
1245
|
+
type: "text",
|
|
1246
|
+
text: `Improve this translation based on the following suggestions.
|
|
1247
|
+
|
|
1248
|
+
## Glossary (MUST apply):
|
|
1249
|
+
${glossaryText || "No glossary provided."}`,
|
|
1250
|
+
cacheControl: { type: "ephemeral" }
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: `## Source Text:
|
|
1255
|
+
${sourceText}
|
|
1256
|
+
|
|
1257
|
+
## Current Translation:
|
|
1258
|
+
${currentTranslation}
|
|
1259
|
+
|
|
1260
|
+
## Improvement Suggestions:
|
|
1261
|
+
${suggestions}
|
|
1262
|
+
|
|
1263
|
+
Provide ONLY the improved translation below, with no additional commentary or headers:`
|
|
1264
|
+
}
|
|
1265
|
+
];
|
|
1266
|
+
messages = [{ role: "user", content: contentParts }];
|
|
1267
|
+
} else {
|
|
1268
|
+
const prompt = buildImprovementPrompt(
|
|
1269
|
+
sourceText,
|
|
1270
|
+
currentTranslation,
|
|
1271
|
+
suggestions,
|
|
1272
|
+
glossaryText
|
|
1273
|
+
);
|
|
1274
|
+
messages = [{ role: "user", content: prompt }];
|
|
1275
|
+
}
|
|
1276
|
+
const response = await this.provider.chat({ messages });
|
|
1277
|
+
const cleanedContent = this.cleanTranslationOutput(response.content);
|
|
1278
|
+
return {
|
|
1279
|
+
content: this.preserveWhitespace(sourceText, cleanedContent),
|
|
1280
|
+
usage: {
|
|
1281
|
+
inputTokens: response.usage.inputTokens,
|
|
1282
|
+
outputTokens: response.usage.outputTokens,
|
|
1283
|
+
cacheReadTokens: response.usage.cacheReadTokens,
|
|
1284
|
+
cacheWriteTokens: response.usage.cacheWriteTokens
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async evaluateQuality(sourceText, translatedText, sourceLang, targetLang) {
|
|
1289
|
+
const prompt = buildQualityEvaluationPrompt(
|
|
1290
|
+
sourceText,
|
|
1291
|
+
translatedText,
|
|
1292
|
+
sourceLang,
|
|
1293
|
+
targetLang
|
|
1294
|
+
);
|
|
1295
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1296
|
+
const response = await this.provider.chat({ messages });
|
|
1297
|
+
try {
|
|
1298
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
1299
|
+
if (!jsonMatch) {
|
|
1300
|
+
throw new Error("No JSON found in response");
|
|
1301
|
+
}
|
|
1302
|
+
const evaluation = JSON.parse(jsonMatch[0]);
|
|
1303
|
+
return {
|
|
1304
|
+
score: evaluation.score,
|
|
1305
|
+
breakdown: evaluation.breakdown,
|
|
1306
|
+
issues: evaluation.issues
|
|
1307
|
+
};
|
|
1308
|
+
} catch {
|
|
1309
|
+
return {
|
|
1310
|
+
score: 75,
|
|
1311
|
+
// Default score
|
|
1312
|
+
breakdown: {
|
|
1313
|
+
accuracy: 30,
|
|
1314
|
+
fluency: 20,
|
|
1315
|
+
glossary: 15,
|
|
1316
|
+
format: 10
|
|
1317
|
+
},
|
|
1318
|
+
issues: ["Failed to parse quality evaluation response"]
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Pre-translation analysis using MAPS-style approach
|
|
1324
|
+
* Identifies key terms, ambiguous phrases, and translation challenges
|
|
1325
|
+
*/
|
|
1326
|
+
async analyzeSource(sourceText, sourceLang, targetLang, glossaryText) {
|
|
1327
|
+
const prompt = buildPreAnalysisPrompt(
|
|
1328
|
+
sourceText,
|
|
1329
|
+
sourceLang,
|
|
1330
|
+
targetLang,
|
|
1331
|
+
glossaryText
|
|
1332
|
+
);
|
|
1333
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1334
|
+
try {
|
|
1335
|
+
const response = await this.provider.chat({ messages });
|
|
1336
|
+
return parseAnalysisResponse(response.content);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
if (this.verbose) {
|
|
1339
|
+
logger.warn(`Pre-analysis failed: ${error}`);
|
|
1340
|
+
}
|
|
1341
|
+
return createEmptyAnalysis();
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Evaluate translation quality using MQM framework
|
|
1346
|
+
* Returns structured error annotations for targeted refinement
|
|
1347
|
+
*/
|
|
1348
|
+
async evaluateQualityMQM(sourceText, translatedText, sourceLang, targetLang, glossaryText) {
|
|
1349
|
+
const prompt = buildMQMEvaluationPrompt(
|
|
1350
|
+
sourceText,
|
|
1351
|
+
translatedText,
|
|
1352
|
+
sourceLang,
|
|
1353
|
+
targetLang,
|
|
1354
|
+
glossaryText
|
|
1355
|
+
);
|
|
1356
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1357
|
+
try {
|
|
1358
|
+
const response = await this.provider.chat({ messages });
|
|
1359
|
+
const evaluation = parseMQMResponse(response.content);
|
|
1360
|
+
if (evaluation) {
|
|
1361
|
+
return evaluation;
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
errors: [],
|
|
1365
|
+
score: 75,
|
|
1366
|
+
summary: "Failed to parse MQM evaluation",
|
|
1367
|
+
breakdown: { accuracy: 0, fluency: 0, style: 0 }
|
|
1368
|
+
};
|
|
1369
|
+
} catch {
|
|
1370
|
+
return {
|
|
1371
|
+
errors: [],
|
|
1372
|
+
score: 75,
|
|
1373
|
+
summary: "MQM evaluation failed",
|
|
1374
|
+
breakdown: { accuracy: 0, fluency: 0, style: 0 }
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Refine translation based on MQM error annotations
|
|
1380
|
+
* Applies targeted fixes for identified errors
|
|
1381
|
+
*/
|
|
1382
|
+
async refineWithMQM(sourceText, currentTranslation, errors, glossaryText) {
|
|
1383
|
+
const prompt = buildMQMRefinementPrompt(
|
|
1384
|
+
sourceText,
|
|
1385
|
+
currentTranslation,
|
|
1386
|
+
errors,
|
|
1387
|
+
glossaryText
|
|
1388
|
+
);
|
|
1389
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1390
|
+
const response = await this.provider.chat({ messages });
|
|
1391
|
+
const cleanedContent = this.cleanTranslationOutput(response.content);
|
|
1392
|
+
return {
|
|
1393
|
+
content: this.preserveWhitespace(sourceText, cleanedContent),
|
|
1394
|
+
usage: {
|
|
1395
|
+
inputTokens: response.usage.inputTokens,
|
|
1396
|
+
outputTokens: response.usage.outputTokens,
|
|
1397
|
+
cacheReadTokens: response.usage.cacheReadTokens,
|
|
1398
|
+
cacheWriteTokens: response.usage.cacheWriteTokens
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Clean up translation output by removing prompt artifacts
|
|
1404
|
+
* Uses guardrails to detect and remove any trailing prompt-like content
|
|
1405
|
+
*/
|
|
1406
|
+
cleanTranslationOutput(text) {
|
|
1407
|
+
let cleaned = text.trim();
|
|
1408
|
+
const trailingHeaderPattern = /\n+##\s+[A-Z][^:\n]*:\s*$/;
|
|
1409
|
+
cleaned = cleaned.replace(trailingHeaderPattern, "");
|
|
1410
|
+
const incompletePromptPattern = /:\s*$/;
|
|
1411
|
+
if (incompletePromptPattern.test(cleaned)) {
|
|
1412
|
+
const lines = cleaned.split("\n");
|
|
1413
|
+
while (lines.length > 0 && incompletePromptPattern.test(lines[lines.length - 1]?.trim() ?? "")) {
|
|
1414
|
+
lines.pop();
|
|
1415
|
+
}
|
|
1416
|
+
cleaned = lines.join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
const evaluationListPattern = /\n+\d+\.\s*\*\*[^*]+\*\*[\s\S]*$/;
|
|
1419
|
+
if (evaluationListPattern.test(cleaned)) {
|
|
1420
|
+
cleaned = cleaned.replace(evaluationListPattern, "");
|
|
1421
|
+
}
|
|
1422
|
+
return cleaned.trim();
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Preserve leading/trailing whitespace from source text in translated text
|
|
1426
|
+
* This ensures document structure (line breaks between sections) is maintained
|
|
1427
|
+
*/
|
|
1428
|
+
preserveWhitespace(sourceText, translatedText) {
|
|
1429
|
+
const leadingMatch = sourceText.match(/^(\s*)/);
|
|
1430
|
+
const leadingWhitespace = leadingMatch ? leadingMatch[1] : "";
|
|
1431
|
+
const trailingMatch = sourceText.match(/(\s*)$/);
|
|
1432
|
+
const trailingWhitespace = trailingMatch ? trailingMatch[1] : "";
|
|
1433
|
+
return leadingWhitespace + translatedText + trailingWhitespace;
|
|
1434
|
+
}
|
|
1435
|
+
checkGlossaryCompliance(sourceText, translatedText, glossary) {
|
|
1436
|
+
const lookup = createGlossaryLookup(glossary);
|
|
1437
|
+
const sourceTerms = lookup.findAll(sourceText);
|
|
1438
|
+
const applied = [];
|
|
1439
|
+
const missed = [];
|
|
1440
|
+
for (const term of sourceTerms) {
|
|
1441
|
+
const targetInTranslation = term.caseSensitive ? translatedText.includes(term.target) : translatedText.toLowerCase().includes(term.target.toLowerCase());
|
|
1442
|
+
if (targetInTranslation) {
|
|
1443
|
+
applied.push(term.source);
|
|
1444
|
+
} else {
|
|
1445
|
+
missed.push(term.source);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return { applied, missed };
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// src/utils/tokens.ts
|
|
1455
|
+
function estimateTokens(text) {
|
|
1456
|
+
if (!text) return 0;
|
|
1457
|
+
let latinChars = 0;
|
|
1458
|
+
let cjkChars = 0;
|
|
1459
|
+
let otherChars = 0;
|
|
1460
|
+
for (const char of text) {
|
|
1461
|
+
const code = char.charCodeAt(0);
|
|
1462
|
+
if (code >= 19968 && code <= 40959 || // CJK Unified Ideographs
|
|
1463
|
+
code >= 13312 && code <= 19903 || // CJK Extension A
|
|
1464
|
+
code >= 44032 && code <= 55215 || // Hangul Syllables
|
|
1465
|
+
code >= 12352 && code <= 12447 || // Hiragana
|
|
1466
|
+
code >= 12448 && code <= 12543) {
|
|
1467
|
+
cjkChars++;
|
|
1468
|
+
} else if (code >= 65 && code <= 90 || // A-Z
|
|
1469
|
+
code >= 97 && code <= 122 || // a-z
|
|
1470
|
+
code >= 48 && code <= 57) {
|
|
1471
|
+
latinChars++;
|
|
1472
|
+
} else {
|
|
1473
|
+
otherChars++;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const latinTokens = latinChars / 4;
|
|
1477
|
+
const cjkTokens = cjkChars / 1.5;
|
|
1478
|
+
const otherTokens = otherChars / 3;
|
|
1479
|
+
return Math.ceil(latinTokens + cjkTokens + otherTokens);
|
|
1480
|
+
}
|
|
1481
|
+
var init_tokens = __esm({
|
|
1482
|
+
"src/utils/tokens.ts"() {
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// src/core/chunker.ts
|
|
1487
|
+
function chunkContent(content, options = {}) {
|
|
1488
|
+
if (!content.trim()) {
|
|
1489
|
+
return [];
|
|
1490
|
+
}
|
|
1491
|
+
const config2 = {
|
|
1492
|
+
...DEFAULT_CONFIG,
|
|
1493
|
+
maxTokens: options.maxTokens ?? DEFAULT_CONFIG.maxTokens,
|
|
1494
|
+
overlapTokens: options.overlapTokens ?? DEFAULT_CONFIG.overlapTokens
|
|
1495
|
+
};
|
|
1496
|
+
const headerHierarchy = extractHeaderHierarchy(content);
|
|
1497
|
+
const { segments } = extractPreservedSections(content);
|
|
1498
|
+
const chunks = [];
|
|
1499
|
+
let previousChunkContent;
|
|
1500
|
+
for (const segment of segments) {
|
|
1501
|
+
const segmentHeaders = getHeadersForPosition(
|
|
1502
|
+
headerHierarchy,
|
|
1503
|
+
segment.startOffset
|
|
1504
|
+
);
|
|
1505
|
+
if (segment.type === "preserve") {
|
|
1506
|
+
chunks.push({
|
|
1507
|
+
id: `chunk-${chunks.length}`,
|
|
1508
|
+
content: segment.content,
|
|
1509
|
+
type: "preserve",
|
|
1510
|
+
startOffset: segment.startOffset,
|
|
1511
|
+
endOffset: segment.endOffset,
|
|
1512
|
+
metadata: {
|
|
1513
|
+
headerHierarchy: segmentHeaders
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
} else {
|
|
1517
|
+
const textChunks = splitIntoChunks(
|
|
1518
|
+
segment.content,
|
|
1519
|
+
config2,
|
|
1520
|
+
segment.startOffset
|
|
1521
|
+
);
|
|
1522
|
+
for (let idx = 0; idx < textChunks.length; idx++) {
|
|
1523
|
+
const chunk = textChunks[idx];
|
|
1524
|
+
if (!chunk) continue;
|
|
1525
|
+
const chunkHeaders = getHeadersForPosition(
|
|
1526
|
+
headerHierarchy,
|
|
1527
|
+
chunk.startOffset
|
|
1528
|
+
);
|
|
1529
|
+
chunks.push({
|
|
1530
|
+
...chunk,
|
|
1531
|
+
id: `chunk-${chunks.length}`,
|
|
1532
|
+
metadata: {
|
|
1533
|
+
headerHierarchy: chunkHeaders.length > 0 ? chunkHeaders : segmentHeaders,
|
|
1534
|
+
previousContext: previousChunkContent
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
previousChunkContent = truncateForContext(chunk.content, 200);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return chunks;
|
|
1542
|
+
}
|
|
1543
|
+
function extractHeaderHierarchy(content) {
|
|
1544
|
+
const headers = [];
|
|
1545
|
+
const headerRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
1546
|
+
let match;
|
|
1547
|
+
while ((match = headerRegex.exec(content)) !== null) {
|
|
1548
|
+
const hashMarks = match[1];
|
|
1549
|
+
if (hashMarks) {
|
|
1550
|
+
headers.push({
|
|
1551
|
+
level: hashMarks.length,
|
|
1552
|
+
text: match[0],
|
|
1553
|
+
offset: match.index
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return headers;
|
|
1558
|
+
}
|
|
1559
|
+
function getHeadersForPosition(headers, position) {
|
|
1560
|
+
const relevantHeaders = [];
|
|
1561
|
+
const currentLevels = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (const header of headers) {
|
|
1563
|
+
if (header.offset > position) break;
|
|
1564
|
+
for (const [level] of currentLevels) {
|
|
1565
|
+
if (level >= header.level) {
|
|
1566
|
+
currentLevels.delete(level);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
currentLevels.set(header.level, header.text);
|
|
1570
|
+
}
|
|
1571
|
+
for (let level = 1; level <= 6; level++) {
|
|
1572
|
+
const headerText = currentLevels.get(level);
|
|
1573
|
+
if (headerText) {
|
|
1574
|
+
relevantHeaders.push(headerText);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return relevantHeaders;
|
|
1578
|
+
}
|
|
1579
|
+
function truncateForContext(content, maxChars) {
|
|
1580
|
+
if (content.length <= maxChars) return content;
|
|
1581
|
+
const truncated = content.slice(-maxChars);
|
|
1582
|
+
const firstSpace = truncated.indexOf(" ");
|
|
1583
|
+
if (firstSpace > 0 && firstSpace < 50) {
|
|
1584
|
+
return "..." + truncated.slice(firstSpace + 1);
|
|
1585
|
+
}
|
|
1586
|
+
return "..." + truncated;
|
|
1587
|
+
}
|
|
1588
|
+
function extractPreservedSections(content) {
|
|
1589
|
+
const preservedRanges = [];
|
|
1590
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
1591
|
+
let match;
|
|
1592
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
1593
|
+
preservedRanges.push({
|
|
1594
|
+
start: match.index,
|
|
1595
|
+
end: match.index + match[0].length,
|
|
1596
|
+
content: match[0]
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
preservedRanges.sort((a, b) => a.start - b.start);
|
|
1600
|
+
const segments = [];
|
|
1601
|
+
let lastEnd = 0;
|
|
1602
|
+
for (const range of preservedRanges) {
|
|
1603
|
+
if (range.start > lastEnd) {
|
|
1604
|
+
const translatableContent = content.slice(lastEnd, range.start);
|
|
1605
|
+
if (translatableContent.length > 0) {
|
|
1606
|
+
segments.push({
|
|
1607
|
+
content: translatableContent,
|
|
1608
|
+
type: translatableContent.trim() ? "translatable" : "preserve",
|
|
1609
|
+
startOffset: lastEnd,
|
|
1610
|
+
endOffset: range.start
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
segments.push({
|
|
1615
|
+
content: range.content,
|
|
1616
|
+
type: "preserve",
|
|
1617
|
+
startOffset: range.start,
|
|
1618
|
+
endOffset: range.end
|
|
1619
|
+
});
|
|
1620
|
+
lastEnd = range.end;
|
|
1621
|
+
}
|
|
1622
|
+
if (lastEnd < content.length) {
|
|
1623
|
+
const remainingContent = content.slice(lastEnd);
|
|
1624
|
+
if (remainingContent.length > 0) {
|
|
1625
|
+
segments.push({
|
|
1626
|
+
content: remainingContent,
|
|
1627
|
+
type: remainingContent.trim() ? "translatable" : "preserve",
|
|
1628
|
+
startOffset: lastEnd,
|
|
1629
|
+
endOffset: content.length
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (segments.length === 0) {
|
|
1634
|
+
segments.push({
|
|
1635
|
+
content,
|
|
1636
|
+
type: "translatable",
|
|
1637
|
+
startOffset: 0,
|
|
1638
|
+
endOffset: content.length
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return { segments };
|
|
1642
|
+
}
|
|
1643
|
+
function splitIntoChunks(text, config2, baseOffset) {
|
|
1644
|
+
const chunks = [];
|
|
1645
|
+
const tokenCount = estimateTokens(text);
|
|
1646
|
+
if (tokenCount <= config2.maxTokens) {
|
|
1647
|
+
return [
|
|
1648
|
+
{
|
|
1649
|
+
id: "",
|
|
1650
|
+
content: text,
|
|
1651
|
+
type: "translatable",
|
|
1652
|
+
startOffset: baseOffset,
|
|
1653
|
+
endOffset: baseOffset + text.length
|
|
1654
|
+
}
|
|
1655
|
+
];
|
|
1656
|
+
}
|
|
1657
|
+
const parts = text.split(/(\n\n+)/);
|
|
1658
|
+
let currentChunk = "";
|
|
1659
|
+
let chunkStartOffset = baseOffset;
|
|
1660
|
+
let textOffset = baseOffset;
|
|
1661
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1662
|
+
const part = parts[i];
|
|
1663
|
+
if (part === void 0) continue;
|
|
1664
|
+
const potentialChunk = currentChunk + part;
|
|
1665
|
+
const potentialTokens = estimateTokens(potentialChunk);
|
|
1666
|
+
if (potentialTokens > config2.maxTokens && currentChunk) {
|
|
1667
|
+
chunks.push({
|
|
1668
|
+
id: "",
|
|
1669
|
+
content: currentChunk,
|
|
1670
|
+
type: "translatable",
|
|
1671
|
+
startOffset: chunkStartOffset,
|
|
1672
|
+
endOffset: textOffset
|
|
1673
|
+
});
|
|
1674
|
+
currentChunk = part;
|
|
1675
|
+
chunkStartOffset = textOffset;
|
|
1676
|
+
} else {
|
|
1677
|
+
currentChunk = potentialChunk;
|
|
1678
|
+
}
|
|
1679
|
+
textOffset += part.length;
|
|
1680
|
+
}
|
|
1681
|
+
if (currentChunk.length > 0) {
|
|
1682
|
+
chunks.push({
|
|
1683
|
+
id: "",
|
|
1684
|
+
content: currentChunk,
|
|
1685
|
+
type: "translatable",
|
|
1686
|
+
startOffset: chunkStartOffset,
|
|
1687
|
+
endOffset: baseOffset + text.length
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return chunks;
|
|
1691
|
+
}
|
|
1692
|
+
function getChunkStats(chunks) {
|
|
1693
|
+
const translatableChunks = chunks.filter((c) => c.type === "translatable");
|
|
1694
|
+
const preservedChunks = chunks.filter((c) => c.type === "preserve");
|
|
1695
|
+
const totalTokens = chunks.reduce(
|
|
1696
|
+
(sum, chunk) => sum + estimateTokens(chunk.content),
|
|
1697
|
+
0
|
|
1698
|
+
);
|
|
1699
|
+
return {
|
|
1700
|
+
totalChunks: chunks.length,
|
|
1701
|
+
translatableChunks: translatableChunks.length,
|
|
1702
|
+
preservedChunks: preservedChunks.length,
|
|
1703
|
+
totalTokens,
|
|
1704
|
+
averageTokens: chunks.length > 0 ? Math.round(totalTokens / chunks.length) : 0
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
var DEFAULT_CONFIG;
|
|
1708
|
+
var init_chunker = __esm({
|
|
1709
|
+
"src/core/chunker.ts"() {
|
|
1710
|
+
init_tokens();
|
|
1711
|
+
DEFAULT_CONFIG = {
|
|
1712
|
+
maxTokens: 1024,
|
|
1713
|
+
overlapTokens: 150,
|
|
1714
|
+
separators: ["\n\n", "\n", ". ", " "],
|
|
1715
|
+
preservePatterns: [
|
|
1716
|
+
/```[\s\S]*?```/g,
|
|
1717
|
+
// Code blocks
|
|
1718
|
+
/`[^`]+`/g,
|
|
1719
|
+
// Inline code
|
|
1720
|
+
/\[.*?\]\(.*?\)/g
|
|
1721
|
+
// Links
|
|
1722
|
+
]
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
function extractTextForTranslation(content) {
|
|
1727
|
+
const preservedSections = /* @__PURE__ */ new Map();
|
|
1728
|
+
let placeholderIndex = 0;
|
|
1729
|
+
let text = content.replace(/^[ \t]*```[^\n]*\n[\s\S]*?^[ \t]*```[ \t]*$/gm, (match) => {
|
|
1730
|
+
const placeholder = `__CODE_BLOCK_${placeholderIndex++}__`;
|
|
1731
|
+
preservedSections.set(placeholder, match);
|
|
1732
|
+
return placeholder;
|
|
1733
|
+
});
|
|
1734
|
+
text = text.replace(/(`{2,})(?:[^`\n]|`(?!\1))*?\1/g, (match) => {
|
|
1735
|
+
const placeholder = `__INLINE_CODE_${placeholderIndex++}__`;
|
|
1736
|
+
preservedSections.set(placeholder, match);
|
|
1737
|
+
return placeholder;
|
|
1738
|
+
});
|
|
1739
|
+
text = text.replace(/`[^`\n]+`/g, (match) => {
|
|
1740
|
+
const placeholder = `__INLINE_CODE_${placeholderIndex++}__`;
|
|
1741
|
+
preservedSections.set(placeholder, match);
|
|
1742
|
+
return placeholder;
|
|
1743
|
+
});
|
|
1744
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
|
|
1745
|
+
const placeholder = `__LINK_URL_${placeholderIndex++}__`;
|
|
1746
|
+
preservedSections.set(placeholder, url);
|
|
1747
|
+
return `[${linkText}](${placeholder})`;
|
|
1748
|
+
});
|
|
1749
|
+
return { text, preservedSections };
|
|
1750
|
+
}
|
|
1751
|
+
function restorePreservedSections(translatedText, preservedSections) {
|
|
1752
|
+
let result = translatedText;
|
|
1753
|
+
const sortedEntries = [...preservedSections.entries()].sort(
|
|
1754
|
+
(a, b) => b[0].length - a[0].length
|
|
1755
|
+
);
|
|
1756
|
+
for (const [placeholder, original] of sortedEntries) {
|
|
1757
|
+
const match = placeholder.match(/^__(.+)__$/);
|
|
1758
|
+
if (match && match[1]) {
|
|
1759
|
+
const identifier = match[1];
|
|
1760
|
+
const escapedId = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1761
|
+
const flexiblePattern = new RegExp(
|
|
1762
|
+
`[ \\t]*_*_*[ \\t]*${escapedId}(?!\\d)[ \\t]*_*_*[ \\t]*`,
|
|
1763
|
+
"gi"
|
|
1764
|
+
);
|
|
1765
|
+
result = result.replace(flexiblePattern, () => original);
|
|
1766
|
+
} else {
|
|
1767
|
+
result = result.split(placeholder).join(original);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
result = ensureInlineCodeSpacing(result);
|
|
1771
|
+
return result;
|
|
1772
|
+
}
|
|
1773
|
+
function ensureInlineCodeSpacing(text) {
|
|
1774
|
+
let result = text.replace(
|
|
1775
|
+
/([\w\u3000-\u9fff\uac00-\ud7af])(`+[^`\n]+`+)/g,
|
|
1776
|
+
"$1 $2"
|
|
1777
|
+
);
|
|
1778
|
+
result = result.replace(
|
|
1779
|
+
/(\d+\.)(`+[^`\n]+`+)/g,
|
|
1780
|
+
"$1 $2"
|
|
1781
|
+
);
|
|
1782
|
+
result = result.replace(
|
|
1783
|
+
/(`+[^`\n]+`+)([\w\u3000-\u9fff\uac00-\ud7af])/g,
|
|
1784
|
+
"$1 $2"
|
|
1785
|
+
);
|
|
1786
|
+
return result;
|
|
1787
|
+
}
|
|
1788
|
+
var init_markdown = __esm({
|
|
1789
|
+
"src/parsers/markdown.ts"() {
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
function mapFinishReason(reason) {
|
|
1793
|
+
switch (reason) {
|
|
1794
|
+
case "stop":
|
|
1795
|
+
case "end_turn":
|
|
1796
|
+
return "stop";
|
|
1797
|
+
case "length":
|
|
1798
|
+
case "max_tokens":
|
|
1799
|
+
return "length";
|
|
1800
|
+
default:
|
|
1801
|
+
return "error";
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function createClaudeProvider(config2 = {}) {
|
|
1805
|
+
return new ClaudeProvider(config2);
|
|
1806
|
+
}
|
|
1807
|
+
var MODEL_INFO, DEFAULT_MODEL, ClaudeProvider;
|
|
1808
|
+
var init_claude = __esm({
|
|
1809
|
+
"src/providers/claude.ts"() {
|
|
1810
|
+
init_errors();
|
|
1811
|
+
init_tokens();
|
|
1812
|
+
MODEL_INFO = {
|
|
1813
|
+
// Latest Claude 4.5 models
|
|
1814
|
+
"claude-sonnet-4-5-20250929": {
|
|
1815
|
+
maxContextTokens: 2e5,
|
|
1816
|
+
supportsStreaming: true,
|
|
1817
|
+
costPer1kInput: 3e-3,
|
|
1818
|
+
costPer1kOutput: 0.015
|
|
1819
|
+
},
|
|
1820
|
+
"claude-opus-4-5-20251101": {
|
|
1821
|
+
maxContextTokens: 2e5,
|
|
1822
|
+
supportsStreaming: true,
|
|
1823
|
+
costPer1kInput: 0.015,
|
|
1824
|
+
costPer1kOutput: 0.075
|
|
1825
|
+
},
|
|
1826
|
+
"claude-haiku-4-5-20251001": {
|
|
1827
|
+
maxContextTokens: 2e5,
|
|
1828
|
+
supportsStreaming: true,
|
|
1829
|
+
costPer1kInput: 1e-3,
|
|
1830
|
+
costPer1kOutput: 5e-3
|
|
1831
|
+
},
|
|
1832
|
+
// Claude 4 models (previous generation)
|
|
1833
|
+
"claude-sonnet-4-20250514": {
|
|
1834
|
+
maxContextTokens: 2e5,
|
|
1835
|
+
supportsStreaming: true,
|
|
1836
|
+
costPer1kInput: 3e-3,
|
|
1837
|
+
costPer1kOutput: 0.015
|
|
1838
|
+
},
|
|
1839
|
+
"claude-opus-4-20250514": {
|
|
1840
|
+
maxContextTokens: 2e5,
|
|
1841
|
+
supportsStreaming: true,
|
|
1842
|
+
costPer1kInput: 0.015,
|
|
1843
|
+
costPer1kOutput: 0.075
|
|
1844
|
+
},
|
|
1845
|
+
// Claude 3.5 models
|
|
1846
|
+
"claude-3-5-haiku-20241022": {
|
|
1847
|
+
maxContextTokens: 2e5,
|
|
1848
|
+
supportsStreaming: true,
|
|
1849
|
+
costPer1kInput: 1e-3,
|
|
1850
|
+
costPer1kOutput: 5e-3
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
1854
|
+
ClaudeProvider = class {
|
|
1855
|
+
name = "claude";
|
|
1856
|
+
defaultModel;
|
|
1857
|
+
client;
|
|
1858
|
+
constructor(config2 = {}) {
|
|
1859
|
+
const apiKey = config2.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
1860
|
+
if (!apiKey) {
|
|
1861
|
+
throw new TranslationError("PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */, {
|
|
1862
|
+
provider: "claude",
|
|
1863
|
+
message: "ANTHROPIC_API_KEY environment variable is not set"
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
this.client = createAnthropic({
|
|
1867
|
+
apiKey,
|
|
1868
|
+
baseURL: config2.baseUrl
|
|
1869
|
+
});
|
|
1870
|
+
this.defaultModel = config2.defaultModel ?? DEFAULT_MODEL;
|
|
1871
|
+
}
|
|
1872
|
+
async chat(request) {
|
|
1873
|
+
const model = request.model ?? this.defaultModel;
|
|
1874
|
+
try {
|
|
1875
|
+
const messages = this.convertMessages(request.messages);
|
|
1876
|
+
const result = await generateText({
|
|
1877
|
+
model: this.client(model),
|
|
1878
|
+
messages,
|
|
1879
|
+
temperature: request.temperature ?? 0,
|
|
1880
|
+
maxTokens: request.maxTokens ?? 4096
|
|
1881
|
+
});
|
|
1882
|
+
const anthropicMeta = result.providerMetadata?.anthropic;
|
|
1883
|
+
return {
|
|
1884
|
+
content: result.text,
|
|
1885
|
+
usage: {
|
|
1886
|
+
inputTokens: result.usage?.promptTokens ?? 0,
|
|
1887
|
+
outputTokens: result.usage?.completionTokens ?? 0,
|
|
1888
|
+
cacheReadTokens: anthropicMeta?.cacheReadInputTokens,
|
|
1889
|
+
cacheWriteTokens: anthropicMeta?.cacheCreationInputTokens
|
|
1890
|
+
},
|
|
1891
|
+
model,
|
|
1892
|
+
finishReason: mapFinishReason(result.finishReason)
|
|
1893
|
+
};
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
throw this.handleError(error);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Convert messages to Vercel AI SDK format with cache control support
|
|
1900
|
+
*/
|
|
1901
|
+
convertMessages(messages) {
|
|
1902
|
+
return messages.map((msg) => {
|
|
1903
|
+
if (typeof msg.content === "string") {
|
|
1904
|
+
return { role: msg.role, content: msg.content };
|
|
1905
|
+
}
|
|
1906
|
+
const parts = msg.content.map((part) => ({
|
|
1907
|
+
type: "text",
|
|
1908
|
+
text: part.text,
|
|
1909
|
+
...part.cacheControl && {
|
|
1910
|
+
providerOptions: {
|
|
1911
|
+
anthropic: { cacheControl: part.cacheControl }
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}));
|
|
1915
|
+
return { role: msg.role, content: parts };
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
async *stream(request) {
|
|
1919
|
+
const model = request.model ?? this.defaultModel;
|
|
1920
|
+
try {
|
|
1921
|
+
const messages = this.convertMessages(request.messages);
|
|
1922
|
+
const result = streamText({
|
|
1923
|
+
model: this.client(model),
|
|
1924
|
+
messages,
|
|
1925
|
+
temperature: request.temperature ?? 0,
|
|
1926
|
+
maxTokens: request.maxTokens ?? 4096
|
|
1927
|
+
});
|
|
1928
|
+
for await (const chunk of result.textStream) {
|
|
1929
|
+
yield chunk;
|
|
1930
|
+
}
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
throw this.handleError(error);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
countTokens(text) {
|
|
1936
|
+
return estimateTokens(text);
|
|
1937
|
+
}
|
|
1938
|
+
getModelInfo(model) {
|
|
1939
|
+
const modelName = model ?? this.defaultModel;
|
|
1940
|
+
return MODEL_INFO[modelName] ?? {
|
|
1941
|
+
maxContextTokens: 2e5,
|
|
1942
|
+
supportsStreaming: true
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
handleError(error) {
|
|
1946
|
+
if (error instanceof TranslationError) {
|
|
1947
|
+
return error;
|
|
1948
|
+
}
|
|
1949
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1950
|
+
if (errorMessage.includes("rate_limit") || errorMessage.includes("429")) {
|
|
1951
|
+
return new TranslationError("PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */, {
|
|
1952
|
+
provider: "claude",
|
|
1953
|
+
message: errorMessage
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
if (errorMessage.includes("authentication") || errorMessage.includes("401") || errorMessage.includes("invalid_api_key")) {
|
|
1957
|
+
return new TranslationError("PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */, {
|
|
1958
|
+
provider: "claude",
|
|
1959
|
+
message: errorMessage
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
1963
|
+
provider: "claude",
|
|
1964
|
+
message: errorMessage
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
function mapFinishReason2(reason) {
|
|
1971
|
+
switch (reason) {
|
|
1972
|
+
case "stop":
|
|
1973
|
+
return "stop";
|
|
1974
|
+
case "length":
|
|
1975
|
+
case "max_tokens":
|
|
1976
|
+
return "length";
|
|
1977
|
+
default:
|
|
1978
|
+
return "error";
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function createOpenAIProvider(config2 = {}) {
|
|
1982
|
+
return new OpenAIProvider(config2);
|
|
1983
|
+
}
|
|
1984
|
+
var MODEL_INFO2, DEFAULT_MODEL2, OpenAIProvider;
|
|
1985
|
+
var init_openai = __esm({
|
|
1986
|
+
"src/providers/openai.ts"() {
|
|
1987
|
+
init_errors();
|
|
1988
|
+
init_tokens();
|
|
1989
|
+
MODEL_INFO2 = {
|
|
1990
|
+
// GPT-4o models (latest)
|
|
1991
|
+
"gpt-4o": {
|
|
1992
|
+
maxContextTokens: 128e3,
|
|
1993
|
+
supportsStreaming: true,
|
|
1994
|
+
costPer1kInput: 25e-4,
|
|
1995
|
+
costPer1kOutput: 0.01
|
|
1996
|
+
},
|
|
1997
|
+
"gpt-4o-2024-11-20": {
|
|
1998
|
+
maxContextTokens: 128e3,
|
|
1999
|
+
supportsStreaming: true,
|
|
2000
|
+
costPer1kInput: 25e-4,
|
|
2001
|
+
costPer1kOutput: 0.01
|
|
2002
|
+
},
|
|
2003
|
+
"gpt-4o-2024-08-06": {
|
|
2004
|
+
maxContextTokens: 128e3,
|
|
2005
|
+
supportsStreaming: true,
|
|
2006
|
+
costPer1kInput: 25e-4,
|
|
2007
|
+
costPer1kOutput: 0.01
|
|
2008
|
+
},
|
|
2009
|
+
// GPT-4o mini (cost-effective)
|
|
2010
|
+
"gpt-4o-mini": {
|
|
2011
|
+
maxContextTokens: 128e3,
|
|
2012
|
+
supportsStreaming: true,
|
|
2013
|
+
costPer1kInput: 15e-5,
|
|
2014
|
+
costPer1kOutput: 6e-4
|
|
2015
|
+
},
|
|
2016
|
+
"gpt-4o-mini-2024-07-18": {
|
|
2017
|
+
maxContextTokens: 128e3,
|
|
2018
|
+
supportsStreaming: true,
|
|
2019
|
+
costPer1kInput: 15e-5,
|
|
2020
|
+
costPer1kOutput: 6e-4
|
|
2021
|
+
},
|
|
2022
|
+
// GPT-4 Turbo
|
|
2023
|
+
"gpt-4-turbo": {
|
|
2024
|
+
maxContextTokens: 128e3,
|
|
2025
|
+
supportsStreaming: true,
|
|
2026
|
+
costPer1kInput: 0.01,
|
|
2027
|
+
costPer1kOutput: 0.03
|
|
2028
|
+
},
|
|
2029
|
+
"gpt-4-turbo-2024-04-09": {
|
|
2030
|
+
maxContextTokens: 128e3,
|
|
2031
|
+
supportsStreaming: true,
|
|
2032
|
+
costPer1kInput: 0.01,
|
|
2033
|
+
costPer1kOutput: 0.03
|
|
2034
|
+
},
|
|
2035
|
+
// GPT-4 (original)
|
|
2036
|
+
"gpt-4": {
|
|
2037
|
+
maxContextTokens: 8192,
|
|
2038
|
+
supportsStreaming: true,
|
|
2039
|
+
costPer1kInput: 0.03,
|
|
2040
|
+
costPer1kOutput: 0.06
|
|
2041
|
+
},
|
|
2042
|
+
// GPT-3.5 Turbo
|
|
2043
|
+
"gpt-3.5-turbo": {
|
|
2044
|
+
maxContextTokens: 16385,
|
|
2045
|
+
supportsStreaming: true,
|
|
2046
|
+
costPer1kInput: 5e-4,
|
|
2047
|
+
costPer1kOutput: 15e-4
|
|
2048
|
+
},
|
|
2049
|
+
// o1 models (reasoning)
|
|
2050
|
+
"o1": {
|
|
2051
|
+
maxContextTokens: 2e5,
|
|
2052
|
+
supportsStreaming: false,
|
|
2053
|
+
costPer1kInput: 0.015,
|
|
2054
|
+
costPer1kOutput: 0.06
|
|
2055
|
+
},
|
|
2056
|
+
"o1-preview": {
|
|
2057
|
+
maxContextTokens: 128e3,
|
|
2058
|
+
supportsStreaming: false,
|
|
2059
|
+
costPer1kInput: 0.015,
|
|
2060
|
+
costPer1kOutput: 0.06
|
|
2061
|
+
},
|
|
2062
|
+
"o1-mini": {
|
|
2063
|
+
maxContextTokens: 128e3,
|
|
2064
|
+
supportsStreaming: false,
|
|
2065
|
+
costPer1kInput: 3e-3,
|
|
2066
|
+
costPer1kOutput: 0.012
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
DEFAULT_MODEL2 = "gpt-4o-mini";
|
|
2070
|
+
OpenAIProvider = class {
|
|
2071
|
+
name = "openai";
|
|
2072
|
+
defaultModel;
|
|
2073
|
+
client;
|
|
2074
|
+
constructor(config2 = {}) {
|
|
2075
|
+
const apiKey = config2.apiKey ?? process.env["OPENAI_API_KEY"];
|
|
2076
|
+
if (!apiKey) {
|
|
2077
|
+
throw new TranslationError("PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */, {
|
|
2078
|
+
provider: "openai",
|
|
2079
|
+
message: "OPENAI_API_KEY environment variable is not set"
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
this.client = createOpenAI({
|
|
2083
|
+
apiKey,
|
|
2084
|
+
baseURL: config2.baseUrl
|
|
2085
|
+
});
|
|
2086
|
+
this.defaultModel = config2.defaultModel ?? DEFAULT_MODEL2;
|
|
2087
|
+
}
|
|
2088
|
+
async chat(request) {
|
|
2089
|
+
const model = request.model ?? this.defaultModel;
|
|
2090
|
+
try {
|
|
2091
|
+
const messages = this.convertMessages(request.messages);
|
|
2092
|
+
const result = await generateText({
|
|
2093
|
+
model: this.client(model),
|
|
2094
|
+
messages,
|
|
2095
|
+
temperature: request.temperature ?? 0,
|
|
2096
|
+
maxTokens: request.maxTokens ?? 4096
|
|
2097
|
+
});
|
|
2098
|
+
return {
|
|
2099
|
+
content: result.text,
|
|
2100
|
+
usage: {
|
|
2101
|
+
inputTokens: result.usage?.promptTokens ?? 0,
|
|
2102
|
+
outputTokens: result.usage?.completionTokens ?? 0
|
|
2103
|
+
},
|
|
2104
|
+
model,
|
|
2105
|
+
finishReason: mapFinishReason2(result.finishReason)
|
|
2106
|
+
};
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
throw this.handleError(error);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Convert messages to Vercel AI SDK format
|
|
2113
|
+
* OpenAI doesn't support cache control like Claude, so we simplify content
|
|
2114
|
+
*/
|
|
2115
|
+
convertMessages(messages) {
|
|
2116
|
+
return messages.map((msg) => {
|
|
2117
|
+
if (Array.isArray(msg.content)) {
|
|
2118
|
+
return {
|
|
2119
|
+
role: msg.role,
|
|
2120
|
+
content: msg.content.map((part) => part.text).join("")
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
return { role: msg.role, content: msg.content };
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
async *stream(request) {
|
|
2127
|
+
const model = request.model ?? this.defaultModel;
|
|
2128
|
+
const modelInfo = this.getModelInfo(model);
|
|
2129
|
+
if (!modelInfo.supportsStreaming) {
|
|
2130
|
+
const response = await this.chat(request);
|
|
2131
|
+
yield response.content;
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
const messages = this.convertMessages(request.messages);
|
|
2136
|
+
const result = streamText({
|
|
2137
|
+
model: this.client(model),
|
|
2138
|
+
messages,
|
|
2139
|
+
temperature: request.temperature ?? 0,
|
|
2140
|
+
maxTokens: request.maxTokens ?? 4096
|
|
2141
|
+
});
|
|
2142
|
+
for await (const chunk of result.textStream) {
|
|
2143
|
+
yield chunk;
|
|
2144
|
+
}
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
throw this.handleError(error);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
countTokens(text) {
|
|
2150
|
+
return estimateTokens(text);
|
|
2151
|
+
}
|
|
2152
|
+
getModelInfo(model) {
|
|
2153
|
+
const modelName = model ?? this.defaultModel;
|
|
2154
|
+
return MODEL_INFO2[modelName] ?? {
|
|
2155
|
+
maxContextTokens: 128e3,
|
|
2156
|
+
supportsStreaming: true
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
handleError(error) {
|
|
2160
|
+
if (error instanceof TranslationError) {
|
|
2161
|
+
return error;
|
|
2162
|
+
}
|
|
2163
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2164
|
+
if (errorMessage.includes("rate_limit") || errorMessage.includes("429") || errorMessage.includes("Rate limit")) {
|
|
2165
|
+
return new TranslationError("PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */, {
|
|
2166
|
+
provider: "openai",
|
|
2167
|
+
message: errorMessage
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
if (errorMessage.includes("authentication") || errorMessage.includes("401") || errorMessage.includes("invalid_api_key") || errorMessage.includes("Incorrect API key")) {
|
|
2171
|
+
return new TranslationError("PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */, {
|
|
2172
|
+
provider: "openai",
|
|
2173
|
+
message: errorMessage
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
if (errorMessage.includes("quota") || errorMessage.includes("insufficient_quota")) {
|
|
2177
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2178
|
+
provider: "openai",
|
|
2179
|
+
message: "API quota exceeded. Please check your billing settings."
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
if (errorMessage.includes("context_length_exceeded") || errorMessage.includes("maximum context length")) {
|
|
2183
|
+
return new TranslationError("CHUNK_TOO_LARGE" /* CHUNK_TOO_LARGE */, {
|
|
2184
|
+
provider: "openai",
|
|
2185
|
+
message: errorMessage
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2189
|
+
provider: "openai",
|
|
2190
|
+
message: errorMessage
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
function mapFinishReason3(reason) {
|
|
2197
|
+
switch (reason) {
|
|
2198
|
+
case "stop":
|
|
2199
|
+
return "stop";
|
|
2200
|
+
case "length":
|
|
2201
|
+
case "max_tokens":
|
|
2202
|
+
return "length";
|
|
2203
|
+
default:
|
|
2204
|
+
return "error";
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
function createOllamaProvider(config2 = {}) {
|
|
2208
|
+
return new OllamaProvider(config2);
|
|
2209
|
+
}
|
|
2210
|
+
var MODEL_INFO3, DEFAULT_MODEL3, DEFAULT_BASE_URL, OllamaProvider;
|
|
2211
|
+
var init_ollama = __esm({
|
|
2212
|
+
"src/providers/ollama.ts"() {
|
|
2213
|
+
init_errors();
|
|
2214
|
+
init_tokens();
|
|
2215
|
+
MODEL_INFO3 = {
|
|
2216
|
+
// Llama 3.x models
|
|
2217
|
+
"llama3.3": {
|
|
2218
|
+
maxContextTokens: 128e3,
|
|
2219
|
+
supportsStreaming: true
|
|
2220
|
+
},
|
|
2221
|
+
"llama3.2": {
|
|
2222
|
+
maxContextTokens: 128e3,
|
|
2223
|
+
supportsStreaming: true
|
|
2224
|
+
},
|
|
2225
|
+
"llama3.1": {
|
|
2226
|
+
maxContextTokens: 128e3,
|
|
2227
|
+
supportsStreaming: true
|
|
2228
|
+
},
|
|
2229
|
+
"llama3": {
|
|
2230
|
+
maxContextTokens: 8192,
|
|
2231
|
+
supportsStreaming: true
|
|
2232
|
+
},
|
|
2233
|
+
// Llama 2 models
|
|
2234
|
+
llama2: {
|
|
2235
|
+
maxContextTokens: 4096,
|
|
2236
|
+
supportsStreaming: true
|
|
2237
|
+
},
|
|
2238
|
+
"llama2:13b": {
|
|
2239
|
+
maxContextTokens: 4096,
|
|
2240
|
+
supportsStreaming: true
|
|
2241
|
+
},
|
|
2242
|
+
"llama2:70b": {
|
|
2243
|
+
maxContextTokens: 4096,
|
|
2244
|
+
supportsStreaming: true
|
|
2245
|
+
},
|
|
2246
|
+
// Mistral models
|
|
2247
|
+
mistral: {
|
|
2248
|
+
maxContextTokens: 32768,
|
|
2249
|
+
supportsStreaming: true
|
|
2250
|
+
},
|
|
2251
|
+
"mistral-nemo": {
|
|
2252
|
+
maxContextTokens: 128e3,
|
|
2253
|
+
supportsStreaming: true
|
|
2254
|
+
},
|
|
2255
|
+
mixtral: {
|
|
2256
|
+
maxContextTokens: 32768,
|
|
2257
|
+
supportsStreaming: true
|
|
2258
|
+
},
|
|
2259
|
+
// Qwen models
|
|
2260
|
+
qwen2: {
|
|
2261
|
+
maxContextTokens: 32768,
|
|
2262
|
+
supportsStreaming: true
|
|
2263
|
+
},
|
|
2264
|
+
"qwen2.5": {
|
|
2265
|
+
maxContextTokens: 128e3,
|
|
2266
|
+
supportsStreaming: true
|
|
2267
|
+
},
|
|
2268
|
+
"qwen2.5-coder": {
|
|
2269
|
+
maxContextTokens: 128e3,
|
|
2270
|
+
supportsStreaming: true
|
|
2271
|
+
},
|
|
2272
|
+
// Gemma models
|
|
2273
|
+
gemma2: {
|
|
2274
|
+
maxContextTokens: 8192,
|
|
2275
|
+
supportsStreaming: true
|
|
2276
|
+
},
|
|
2277
|
+
gemma: {
|
|
2278
|
+
maxContextTokens: 8192,
|
|
2279
|
+
supportsStreaming: true
|
|
2280
|
+
},
|
|
2281
|
+
// Phi models
|
|
2282
|
+
phi3: {
|
|
2283
|
+
maxContextTokens: 128e3,
|
|
2284
|
+
supportsStreaming: true
|
|
2285
|
+
},
|
|
2286
|
+
"phi3:mini": {
|
|
2287
|
+
maxContextTokens: 128e3,
|
|
2288
|
+
supportsStreaming: true
|
|
2289
|
+
},
|
|
2290
|
+
// Code models
|
|
2291
|
+
codellama: {
|
|
2292
|
+
maxContextTokens: 16384,
|
|
2293
|
+
supportsStreaming: true
|
|
2294
|
+
},
|
|
2295
|
+
"deepseek-coder": {
|
|
2296
|
+
maxContextTokens: 16384,
|
|
2297
|
+
supportsStreaming: true
|
|
2298
|
+
},
|
|
2299
|
+
// Other popular models
|
|
2300
|
+
"neural-chat": {
|
|
2301
|
+
maxContextTokens: 8192,
|
|
2302
|
+
supportsStreaming: true
|
|
2303
|
+
},
|
|
2304
|
+
vicuna: {
|
|
2305
|
+
maxContextTokens: 2048,
|
|
2306
|
+
supportsStreaming: true
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
DEFAULT_MODEL3 = "llama3.2";
|
|
2310
|
+
DEFAULT_BASE_URL = "http://localhost:11434";
|
|
2311
|
+
OllamaProvider = class {
|
|
2312
|
+
name = "ollama";
|
|
2313
|
+
defaultModel;
|
|
2314
|
+
client;
|
|
2315
|
+
baseUrl;
|
|
2316
|
+
constructor(config2 = {}) {
|
|
2317
|
+
this.baseUrl = config2.baseUrl ?? process.env["OLLAMA_BASE_URL"] ?? DEFAULT_BASE_URL;
|
|
2318
|
+
this.client = createOpenAI({
|
|
2319
|
+
apiKey: "ollama",
|
|
2320
|
+
// Ollama doesn't require an API key
|
|
2321
|
+
baseURL: `${this.baseUrl}/v1`
|
|
2322
|
+
});
|
|
2323
|
+
this.defaultModel = config2.defaultModel ?? DEFAULT_MODEL3;
|
|
2324
|
+
}
|
|
2325
|
+
async chat(request) {
|
|
2326
|
+
const model = request.model ?? this.defaultModel;
|
|
2327
|
+
try {
|
|
2328
|
+
await this.ensureModelAvailable(model);
|
|
2329
|
+
const messages = this.convertMessages(request.messages);
|
|
2330
|
+
const result = await generateText({
|
|
2331
|
+
model: this.client(model),
|
|
2332
|
+
messages,
|
|
2333
|
+
temperature: request.temperature ?? 0,
|
|
2334
|
+
maxTokens: request.maxTokens ?? 4096
|
|
2335
|
+
});
|
|
2336
|
+
return {
|
|
2337
|
+
content: result.text,
|
|
2338
|
+
usage: {
|
|
2339
|
+
inputTokens: result.usage?.promptTokens ?? 0,
|
|
2340
|
+
outputTokens: result.usage?.completionTokens ?? 0
|
|
2341
|
+
},
|
|
2342
|
+
model,
|
|
2343
|
+
finishReason: mapFinishReason3(result.finishReason)
|
|
2344
|
+
};
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
throw this.handleError(error, model);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Convert messages to Vercel AI SDK format
|
|
2351
|
+
* Ollama doesn't support cache control, so we simplify content
|
|
2352
|
+
*/
|
|
2353
|
+
convertMessages(messages) {
|
|
2354
|
+
return messages.map((msg) => {
|
|
2355
|
+
if (Array.isArray(msg.content)) {
|
|
2356
|
+
return {
|
|
2357
|
+
role: msg.role,
|
|
2358
|
+
content: msg.content.map((part) => part.text).join("")
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
return { role: msg.role, content: msg.content };
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
async *stream(request) {
|
|
2365
|
+
const model = request.model ?? this.defaultModel;
|
|
2366
|
+
try {
|
|
2367
|
+
await this.ensureModelAvailable(model);
|
|
2368
|
+
const messages = this.convertMessages(request.messages);
|
|
2369
|
+
const result = streamText({
|
|
2370
|
+
model: this.client(model),
|
|
2371
|
+
messages,
|
|
2372
|
+
temperature: request.temperature ?? 0,
|
|
2373
|
+
maxTokens: request.maxTokens ?? 4096
|
|
2374
|
+
});
|
|
2375
|
+
for await (const chunk of result.textStream) {
|
|
2376
|
+
yield chunk;
|
|
2377
|
+
}
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
throw this.handleError(error, model);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
countTokens(text) {
|
|
2383
|
+
return estimateTokens(text);
|
|
2384
|
+
}
|
|
2385
|
+
getModelInfo(model) {
|
|
2386
|
+
const modelName = model ?? this.defaultModel;
|
|
2387
|
+
if (MODEL_INFO3[modelName]) {
|
|
2388
|
+
return MODEL_INFO3[modelName];
|
|
2389
|
+
}
|
|
2390
|
+
const baseModel = modelName.split(":")[0] ?? modelName;
|
|
2391
|
+
if (baseModel && MODEL_INFO3[baseModel]) {
|
|
2392
|
+
return MODEL_INFO3[baseModel];
|
|
2393
|
+
}
|
|
2394
|
+
return {
|
|
2395
|
+
maxContextTokens: 4096,
|
|
2396
|
+
supportsStreaming: true
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Check if the Ollama server is running and the model is available
|
|
2401
|
+
*/
|
|
2402
|
+
async ensureModelAvailable(model) {
|
|
2403
|
+
try {
|
|
2404
|
+
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
2405
|
+
if (!response.ok) {
|
|
2406
|
+
throw new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2407
|
+
provider: "ollama",
|
|
2408
|
+
message: `Ollama server not responding at ${this.baseUrl}`
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
const data = await response.json();
|
|
2412
|
+
const models = data.models ?? [];
|
|
2413
|
+
const modelNames = models.map((m) => m.name);
|
|
2414
|
+
const modelExists = modelNames.some(
|
|
2415
|
+
(name) => name === model || name.startsWith(`${model}:`)
|
|
2416
|
+
);
|
|
2417
|
+
if (!modelExists) {
|
|
2418
|
+
throw new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2419
|
+
provider: "ollama",
|
|
2420
|
+
model,
|
|
2421
|
+
availableModels: modelNames.slice(0, 10),
|
|
2422
|
+
// Show first 10
|
|
2423
|
+
message: `Model "${model}" not found. Pull it with: ollama pull ${model}`
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
if (error instanceof TranslationError) {
|
|
2428
|
+
throw error;
|
|
2429
|
+
}
|
|
2430
|
+
throw new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2431
|
+
provider: "ollama",
|
|
2432
|
+
baseUrl: this.baseUrl,
|
|
2433
|
+
message: `Cannot connect to Ollama server at ${this.baseUrl}. Is Ollama running?`
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
handleError(error, model) {
|
|
2438
|
+
if (error instanceof TranslationError) {
|
|
2439
|
+
return error;
|
|
2440
|
+
}
|
|
2441
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2442
|
+
if (errorMessage.includes("ECONNREFUSED") || errorMessage.includes("fetch failed") || errorMessage.includes("network")) {
|
|
2443
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2444
|
+
provider: "ollama",
|
|
2445
|
+
baseUrl: this.baseUrl,
|
|
2446
|
+
message: `Cannot connect to Ollama server at ${this.baseUrl}. Is Ollama running?`
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
if (errorMessage.includes("model") && errorMessage.includes("not found")) {
|
|
2450
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2451
|
+
provider: "ollama",
|
|
2452
|
+
model,
|
|
2453
|
+
message: `Model "${model}" not found. Pull it with: ollama pull ${model}`
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
if (errorMessage.includes("context") || errorMessage.includes("too long")) {
|
|
2457
|
+
return new TranslationError("CHUNK_TOO_LARGE" /* CHUNK_TOO_LARGE */, {
|
|
2458
|
+
provider: "ollama",
|
|
2459
|
+
model,
|
|
2460
|
+
message: errorMessage
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
if (errorMessage.includes("out of memory") || errorMessage.includes("OOM")) {
|
|
2464
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2465
|
+
provider: "ollama",
|
|
2466
|
+
model,
|
|
2467
|
+
message: "Out of memory. Try a smaller model or reduce chunk size."
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
return new TranslationError("PROVIDER_ERROR" /* PROVIDER_ERROR */, {
|
|
2471
|
+
provider: "ollama",
|
|
2472
|
+
message: errorMessage
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
// src/providers/registry.ts
|
|
2480
|
+
function registerProvider(name, factory) {
|
|
2481
|
+
providers.set(name, factory);
|
|
2482
|
+
}
|
|
2483
|
+
function getProvider(name, config2 = {}) {
|
|
2484
|
+
const factory = providers.get(name);
|
|
2485
|
+
if (!factory) {
|
|
2486
|
+
throw new TranslationError("PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */, {
|
|
2487
|
+
provider: name,
|
|
2488
|
+
available: Array.from(providers.keys())
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
return factory(config2);
|
|
2492
|
+
}
|
|
2493
|
+
function getAvailableProviders() {
|
|
2494
|
+
return Array.from(providers.keys());
|
|
2495
|
+
}
|
|
2496
|
+
function getProviderConfigFromEnv(name) {
|
|
2497
|
+
switch (name) {
|
|
2498
|
+
case "claude":
|
|
2499
|
+
return {
|
|
2500
|
+
apiKey: process.env["ANTHROPIC_API_KEY"]
|
|
2501
|
+
// defaultModel is handled by the provider itself
|
|
2502
|
+
};
|
|
2503
|
+
case "openai":
|
|
2504
|
+
return {
|
|
2505
|
+
apiKey: process.env["OPENAI_API_KEY"],
|
|
2506
|
+
defaultModel: "gpt-4o"
|
|
2507
|
+
};
|
|
2508
|
+
case "ollama":
|
|
2509
|
+
return {
|
|
2510
|
+
baseUrl: process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434",
|
|
2511
|
+
defaultModel: "llama3.2"
|
|
2512
|
+
// Better multilingual support than llama2
|
|
2513
|
+
};
|
|
2514
|
+
case "custom":
|
|
2515
|
+
return {
|
|
2516
|
+
apiKey: process.env["LLM_API_KEY"],
|
|
2517
|
+
baseUrl: process.env["LLM_BASE_URL"]
|
|
2518
|
+
};
|
|
2519
|
+
default:
|
|
2520
|
+
return {};
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
var providers;
|
|
2524
|
+
var init_registry = __esm({
|
|
2525
|
+
"src/providers/registry.ts"() {
|
|
2526
|
+
init_errors();
|
|
2527
|
+
init_claude();
|
|
2528
|
+
init_openai();
|
|
2529
|
+
init_ollama();
|
|
2530
|
+
providers = /* @__PURE__ */ new Map();
|
|
2531
|
+
registerProvider("claude", createClaudeProvider);
|
|
2532
|
+
registerProvider("openai", createOpenAIProvider);
|
|
2533
|
+
registerProvider("ollama", createOllamaProvider);
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
function hashContent(content) {
|
|
2537
|
+
return createHash("sha256").update(content, "utf8").digest("hex").slice(0, 16);
|
|
2538
|
+
}
|
|
2539
|
+
function generateCacheKey(key) {
|
|
2540
|
+
const contentHash = hashContent(key.content);
|
|
2541
|
+
const glossaryHash = key.glossary ? hashContent(key.glossary) : "none";
|
|
2542
|
+
return `${contentHash}_${key.sourceLang}_${key.targetLang}_${glossaryHash}_${key.provider}_${key.model}`;
|
|
2543
|
+
}
|
|
2544
|
+
function createCacheManager(options) {
|
|
2545
|
+
return new CacheManager(options);
|
|
2546
|
+
}
|
|
2547
|
+
function createNullCacheManager() {
|
|
2548
|
+
const nullManager = {
|
|
2549
|
+
isNull: true,
|
|
2550
|
+
get: () => ({ hit: false }),
|
|
2551
|
+
set: () => {
|
|
2552
|
+
},
|
|
2553
|
+
has: () => false,
|
|
2554
|
+
delete: () => false,
|
|
2555
|
+
clear: () => {
|
|
2556
|
+
},
|
|
2557
|
+
getStats: () => ({ entries: 0, sizeBytes: 0, version: CACHE_VERSION }),
|
|
2558
|
+
getAllEntries: () => ({}),
|
|
2559
|
+
updateMetadata: () => {
|
|
2560
|
+
},
|
|
2561
|
+
getMetadata: () => ({}),
|
|
2562
|
+
applyPolicies: () => 0,
|
|
2563
|
+
invalidateMatching: () => 0,
|
|
2564
|
+
addPolicy: () => {
|
|
2565
|
+
},
|
|
2566
|
+
removePolicy: () => false,
|
|
2567
|
+
getPolicies: () => []
|
|
2568
|
+
};
|
|
2569
|
+
return nullManager;
|
|
2570
|
+
}
|
|
2571
|
+
var CACHE_VERSION, INDEX_FILE, ENTRIES_DIR, CacheManager;
|
|
2572
|
+
var init_cache = __esm({
|
|
2573
|
+
"src/services/cache.ts"() {
|
|
2574
|
+
init_logger();
|
|
2575
|
+
CACHE_VERSION = "1.0";
|
|
2576
|
+
INDEX_FILE = "index.json";
|
|
2577
|
+
ENTRIES_DIR = "entries";
|
|
2578
|
+
CacheManager = class {
|
|
2579
|
+
cacheDir;
|
|
2580
|
+
indexPath;
|
|
2581
|
+
entriesDir;
|
|
2582
|
+
metadataPath;
|
|
2583
|
+
verbose;
|
|
2584
|
+
index = null;
|
|
2585
|
+
policies;
|
|
2586
|
+
metadata = null;
|
|
2587
|
+
constructor(options) {
|
|
2588
|
+
this.cacheDir = options.cacheDir;
|
|
2589
|
+
this.indexPath = join(this.cacheDir, INDEX_FILE);
|
|
2590
|
+
this.entriesDir = join(this.cacheDir, ENTRIES_DIR);
|
|
2591
|
+
this.metadataPath = join(this.cacheDir, "metadata.json");
|
|
2592
|
+
this.verbose = options.verbose ?? false;
|
|
2593
|
+
this.policies = options.invalidationPolicies ?? [];
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Initialize cache directory and load index
|
|
2597
|
+
*/
|
|
2598
|
+
ensureInitialized() {
|
|
2599
|
+
if (this.index !== null) return;
|
|
2600
|
+
if (!existsSync(this.cacheDir)) {
|
|
2601
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
2602
|
+
}
|
|
2603
|
+
if (!existsSync(this.entriesDir)) {
|
|
2604
|
+
mkdirSync(this.entriesDir, { recursive: true });
|
|
2605
|
+
}
|
|
2606
|
+
if (existsSync(this.indexPath)) {
|
|
2607
|
+
try {
|
|
2608
|
+
const data = readFileSync(this.indexPath, "utf-8");
|
|
2609
|
+
this.index = JSON.parse(data);
|
|
2610
|
+
if (this.index.version !== CACHE_VERSION) {
|
|
2611
|
+
if (this.verbose) {
|
|
2612
|
+
logger.warn(`Cache version mismatch (${this.index.version} vs ${CACHE_VERSION}), clearing cache`);
|
|
2613
|
+
}
|
|
2614
|
+
this.clearSync();
|
|
2615
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
2616
|
+
}
|
|
2617
|
+
} catch {
|
|
2618
|
+
if (this.verbose) {
|
|
2619
|
+
logger.warn("Failed to load cache index, creating new one");
|
|
2620
|
+
}
|
|
2621
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
2622
|
+
}
|
|
2623
|
+
} else {
|
|
2624
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
2625
|
+
}
|
|
2626
|
+
this.loadMetadata();
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Load cache metadata from disk
|
|
2630
|
+
*/
|
|
2631
|
+
loadMetadata() {
|
|
2632
|
+
if (existsSync(this.metadataPath)) {
|
|
2633
|
+
try {
|
|
2634
|
+
const data = readFileSync(this.metadataPath, "utf-8");
|
|
2635
|
+
this.metadata = JSON.parse(data);
|
|
2636
|
+
} catch {
|
|
2637
|
+
this.metadata = {};
|
|
2638
|
+
}
|
|
2639
|
+
} else {
|
|
2640
|
+
this.metadata = {};
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Save cache metadata to disk
|
|
2645
|
+
*/
|
|
2646
|
+
saveMetadata() {
|
|
2647
|
+
if (!this.metadata) return;
|
|
2648
|
+
try {
|
|
2649
|
+
writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2), "utf-8");
|
|
2650
|
+
} catch (error) {
|
|
2651
|
+
if (this.verbose) {
|
|
2652
|
+
logger.error(`Failed to save cache metadata: ${error}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Update cache metadata
|
|
2658
|
+
*/
|
|
2659
|
+
updateMetadata(updates) {
|
|
2660
|
+
this.ensureInitialized();
|
|
2661
|
+
this.metadata = { ...this.metadata, ...updates };
|
|
2662
|
+
this.saveMetadata();
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Get current cache metadata
|
|
2666
|
+
*/
|
|
2667
|
+
getMetadata() {
|
|
2668
|
+
this.ensureInitialized();
|
|
2669
|
+
return { ...this.metadata };
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Apply all configured invalidation policies
|
|
2673
|
+
* @returns Number of entries invalidated
|
|
2674
|
+
*/
|
|
2675
|
+
applyPolicies(context) {
|
|
2676
|
+
this.ensureInitialized();
|
|
2677
|
+
if (this.policies.length === 0) {
|
|
2678
|
+
return 0;
|
|
2679
|
+
}
|
|
2680
|
+
let totalInvalidated = 0;
|
|
2681
|
+
for (const policy of this.policies) {
|
|
2682
|
+
const result = policy.check({
|
|
2683
|
+
...context,
|
|
2684
|
+
previousGlossaryHash: this.metadata?.glossaryHash,
|
|
2685
|
+
currentTime: context.currentTime ?? /* @__PURE__ */ new Date()
|
|
2686
|
+
});
|
|
2687
|
+
if (!result.shouldInvalidate) {
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
if (this.verbose) {
|
|
2691
|
+
logger.info(`Applying ${policy.name}: ${result.reason}`);
|
|
2692
|
+
}
|
|
2693
|
+
if (result.scope === "all") {
|
|
2694
|
+
const count = Object.keys(this.index.entries).length;
|
|
2695
|
+
this.clear();
|
|
2696
|
+
totalInvalidated += count;
|
|
2697
|
+
break;
|
|
2698
|
+
}
|
|
2699
|
+
if (result.scope === "matching" && result.filter) {
|
|
2700
|
+
const invalidated = this.invalidateMatching(result.filter);
|
|
2701
|
+
totalInvalidated += invalidated;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
if (context.glossaryHash) {
|
|
2705
|
+
this.metadata.glossaryHash = context.glossaryHash;
|
|
2706
|
+
}
|
|
2707
|
+
if (context.provider) {
|
|
2708
|
+
this.metadata.provider = context.provider;
|
|
2709
|
+
}
|
|
2710
|
+
if (context.model) {
|
|
2711
|
+
this.metadata.model = context.model;
|
|
2712
|
+
}
|
|
2713
|
+
if (totalInvalidated > 0) {
|
|
2714
|
+
this.metadata.lastInvalidation = (/* @__PURE__ */ new Date()).toISOString();
|
|
2715
|
+
}
|
|
2716
|
+
this.saveMetadata();
|
|
2717
|
+
return totalInvalidated;
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Invalidate entries matching a filter function
|
|
2721
|
+
* @returns Number of entries invalidated
|
|
2722
|
+
*/
|
|
2723
|
+
invalidateMatching(filter) {
|
|
2724
|
+
this.ensureInitialized();
|
|
2725
|
+
const keysToDelete = [];
|
|
2726
|
+
for (const [key, entry] of Object.entries(this.index.entries)) {
|
|
2727
|
+
if (filter(entry, key)) {
|
|
2728
|
+
keysToDelete.push(key);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
for (const key of keysToDelete) {
|
|
2732
|
+
const entryPath = join(this.entriesDir, `${key}.json`);
|
|
2733
|
+
try {
|
|
2734
|
+
if (existsSync(entryPath)) {
|
|
2735
|
+
rmSync(entryPath);
|
|
2736
|
+
}
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
delete this.index.entries[key];
|
|
2740
|
+
}
|
|
2741
|
+
if (keysToDelete.length > 0) {
|
|
2742
|
+
this.saveIndex();
|
|
2743
|
+
if (this.verbose) {
|
|
2744
|
+
logger.info(`Invalidated ${keysToDelete.length} cache entries`);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return keysToDelete.length;
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Add an invalidation policy at runtime
|
|
2751
|
+
*/
|
|
2752
|
+
addPolicy(policy) {
|
|
2753
|
+
this.policies.push(policy);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Remove an invalidation policy by name
|
|
2757
|
+
*/
|
|
2758
|
+
removePolicy(name) {
|
|
2759
|
+
const index = this.policies.findIndex((p) => p.name === name);
|
|
2760
|
+
if (index !== -1) {
|
|
2761
|
+
this.policies.splice(index, 1);
|
|
2762
|
+
return true;
|
|
2763
|
+
}
|
|
2764
|
+
return false;
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Get all configured policies
|
|
2768
|
+
*/
|
|
2769
|
+
getPolicies() {
|
|
2770
|
+
return [...this.policies];
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Save index to disk
|
|
2774
|
+
*/
|
|
2775
|
+
saveIndex() {
|
|
2776
|
+
if (!this.index) return;
|
|
2777
|
+
try {
|
|
2778
|
+
const dir = dirname(this.indexPath);
|
|
2779
|
+
if (!existsSync(dir)) {
|
|
2780
|
+
mkdirSync(dir, { recursive: true });
|
|
2781
|
+
}
|
|
2782
|
+
writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), "utf-8");
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
if (this.verbose) {
|
|
2785
|
+
logger.error(`Failed to save cache index: ${error}`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Get cached translation if available
|
|
2791
|
+
*/
|
|
2792
|
+
get(key) {
|
|
2793
|
+
this.ensureInitialized();
|
|
2794
|
+
const cacheKey = generateCacheKey(key);
|
|
2795
|
+
const entry = this.index.entries[cacheKey];
|
|
2796
|
+
if (entry) {
|
|
2797
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
2798
|
+
if (existsSync(entryPath)) {
|
|
2799
|
+
if (this.verbose) {
|
|
2800
|
+
logger.info(`Cache hit: ${cacheKey.slice(0, 20)}...`);
|
|
2801
|
+
}
|
|
2802
|
+
return { hit: true, entry };
|
|
2803
|
+
} else {
|
|
2804
|
+
delete this.index.entries[cacheKey];
|
|
2805
|
+
this.saveIndex();
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
if (this.verbose) {
|
|
2809
|
+
logger.debug(`Cache miss: ${cacheKey.slice(0, 20)}...`);
|
|
2810
|
+
}
|
|
2811
|
+
return { hit: false };
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Store translation in cache
|
|
2815
|
+
*/
|
|
2816
|
+
set(key, translation, qualityScore) {
|
|
2817
|
+
this.ensureInitialized();
|
|
2818
|
+
const cacheKey = generateCacheKey(key);
|
|
2819
|
+
const contentHash = hashContent(key.content);
|
|
2820
|
+
const glossaryHash = key.glossary ? hashContent(key.glossary) : "";
|
|
2821
|
+
const entry = {
|
|
2822
|
+
sourceHash: contentHash,
|
|
2823
|
+
sourceLang: key.sourceLang,
|
|
2824
|
+
targetLang: key.targetLang,
|
|
2825
|
+
glossaryHash,
|
|
2826
|
+
translation,
|
|
2827
|
+
qualityScore,
|
|
2828
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2829
|
+
provider: key.provider,
|
|
2830
|
+
model: key.model
|
|
2831
|
+
};
|
|
2832
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
2833
|
+
try {
|
|
2834
|
+
writeFileSync(entryPath, JSON.stringify(entry, null, 2), "utf-8");
|
|
2835
|
+
this.index.entries[cacheKey] = entry;
|
|
2836
|
+
this.saveIndex();
|
|
2837
|
+
if (this.verbose) {
|
|
2838
|
+
logger.info(`Cached: ${cacheKey.slice(0, 20)}...`);
|
|
2839
|
+
}
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
if (this.verbose) {
|
|
2842
|
+
logger.error(`Failed to cache entry: ${error}`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Check if entry exists in cache
|
|
2848
|
+
*/
|
|
2849
|
+
has(key) {
|
|
2850
|
+
return this.get(key).hit;
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Remove entry from cache
|
|
2854
|
+
*/
|
|
2855
|
+
delete(key) {
|
|
2856
|
+
this.ensureInitialized();
|
|
2857
|
+
const cacheKey = generateCacheKey(key);
|
|
2858
|
+
if (this.index.entries[cacheKey]) {
|
|
2859
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
2860
|
+
try {
|
|
2861
|
+
if (existsSync(entryPath)) {
|
|
2862
|
+
rmSync(entryPath);
|
|
2863
|
+
}
|
|
2864
|
+
} catch {
|
|
2865
|
+
}
|
|
2866
|
+
delete this.index.entries[cacheKey];
|
|
2867
|
+
this.saveIndex();
|
|
2868
|
+
return true;
|
|
2869
|
+
}
|
|
2870
|
+
return false;
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Clear entire cache (synchronous)
|
|
2874
|
+
*/
|
|
2875
|
+
clearSync() {
|
|
2876
|
+
try {
|
|
2877
|
+
if (existsSync(this.entriesDir)) {
|
|
2878
|
+
rmSync(this.entriesDir, { recursive: true, force: true });
|
|
2879
|
+
}
|
|
2880
|
+
if (existsSync(this.indexPath)) {
|
|
2881
|
+
rmSync(this.indexPath);
|
|
2882
|
+
}
|
|
2883
|
+
mkdirSync(this.entriesDir, { recursive: true });
|
|
2884
|
+
} catch {
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Clear entire cache
|
|
2889
|
+
*/
|
|
2890
|
+
clear() {
|
|
2891
|
+
this.clearSync();
|
|
2892
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
2893
|
+
this.saveIndex();
|
|
2894
|
+
if (this.verbose) {
|
|
2895
|
+
logger.info("Cache cleared");
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Get cache statistics
|
|
2900
|
+
*/
|
|
2901
|
+
getStats() {
|
|
2902
|
+
this.ensureInitialized();
|
|
2903
|
+
let sizeBytes = 0;
|
|
2904
|
+
if (existsSync(this.entriesDir)) {
|
|
2905
|
+
try {
|
|
2906
|
+
const files = readdirSync(this.entriesDir);
|
|
2907
|
+
for (const file of files) {
|
|
2908
|
+
const filePath = join(this.entriesDir, file);
|
|
2909
|
+
try {
|
|
2910
|
+
const stat = statSync(filePath);
|
|
2911
|
+
sizeBytes += stat.size;
|
|
2912
|
+
} catch {
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
} catch {
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (existsSync(this.indexPath)) {
|
|
2919
|
+
try {
|
|
2920
|
+
const stat = statSync(this.indexPath);
|
|
2921
|
+
sizeBytes += stat.size;
|
|
2922
|
+
} catch {
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
entries: Object.keys(this.index?.entries ?? {}).length,
|
|
2927
|
+
sizeBytes,
|
|
2928
|
+
version: CACHE_VERSION
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Get all cached entries (for debugging)
|
|
2933
|
+
*/
|
|
2934
|
+
getAllEntries() {
|
|
2935
|
+
this.ensureInitialized();
|
|
2936
|
+
return { ...this.index.entries };
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
// src/core/engine.ts
|
|
2943
|
+
function createTranslationEngine(options) {
|
|
2944
|
+
return new TranslationEngine(options);
|
|
2945
|
+
}
|
|
2946
|
+
var TranslationEngine;
|
|
2947
|
+
var init_engine = __esm({
|
|
2948
|
+
"src/core/engine.ts"() {
|
|
2949
|
+
init_agent();
|
|
2950
|
+
init_chunker();
|
|
2951
|
+
init_markdown();
|
|
2952
|
+
init_glossary();
|
|
2953
|
+
init_registry();
|
|
2954
|
+
init_logger();
|
|
2955
|
+
init_errors();
|
|
2956
|
+
init_cache();
|
|
2957
|
+
TranslationEngine = class {
|
|
2958
|
+
config;
|
|
2959
|
+
provider;
|
|
2960
|
+
verbose;
|
|
2961
|
+
cache;
|
|
2962
|
+
cacheHits = 0;
|
|
2963
|
+
cacheMisses = 0;
|
|
2964
|
+
constructor(options) {
|
|
2965
|
+
this.config = options.config;
|
|
2966
|
+
this.verbose = options.verbose ?? false;
|
|
2967
|
+
if (options.provider) {
|
|
2968
|
+
this.provider = options.provider;
|
|
2969
|
+
} else {
|
|
2970
|
+
const providerConfig = getProviderConfigFromEnv(this.config.provider.default);
|
|
2971
|
+
if (this.config.provider.model) {
|
|
2972
|
+
providerConfig.defaultModel = this.config.provider.model;
|
|
2973
|
+
}
|
|
2974
|
+
this.provider = getProvider(this.config.provider.default, providerConfig);
|
|
2975
|
+
}
|
|
2976
|
+
const cacheDisabled = options.noCache || !this.config.paths?.cache;
|
|
2977
|
+
if (cacheDisabled) {
|
|
2978
|
+
this.cache = createNullCacheManager();
|
|
2979
|
+
if (this.verbose && options.noCache) {
|
|
2980
|
+
logger.info("Cache disabled (--no-cache)");
|
|
2981
|
+
}
|
|
2982
|
+
} else {
|
|
2983
|
+
this.cache = createCacheManager({
|
|
2984
|
+
cacheDir: this.config.paths.cache,
|
|
2985
|
+
verbose: this.verbose
|
|
2986
|
+
});
|
|
2987
|
+
if (this.verbose) {
|
|
2988
|
+
const stats = this.cache.getStats();
|
|
2989
|
+
logger.info(`Cache initialized: ${stats.entries} entries`);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Translate a single file/content
|
|
2995
|
+
*/
|
|
2996
|
+
async translateContent(options) {
|
|
2997
|
+
const timer = createTimer();
|
|
2998
|
+
const format = options.format ?? this.detectFormat(options.content);
|
|
2999
|
+
if (this.verbose) {
|
|
3000
|
+
logger.info(`Translating content (${format} format)`);
|
|
3001
|
+
logger.info(`Source: ${options.sourceLang} \u2192 Target: ${options.targetLang}`);
|
|
3002
|
+
}
|
|
3003
|
+
let glossary;
|
|
3004
|
+
if (options.glossaryPath) {
|
|
3005
|
+
try {
|
|
3006
|
+
const rawGlossary = await loadGlossary(options.glossaryPath);
|
|
3007
|
+
glossary = resolveGlossary(rawGlossary, options.targetLang);
|
|
3008
|
+
if (this.verbose) {
|
|
3009
|
+
logger.info(`Loaded glossary: ${glossary.terms.length} terms`);
|
|
3010
|
+
}
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
if (this.verbose) {
|
|
3013
|
+
logger.warn(`Failed to load glossary: ${error}`);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
} else if (this.config.glossary?.path) {
|
|
3017
|
+
try {
|
|
3018
|
+
const rawGlossary = await loadGlossary(this.config.glossary.path);
|
|
3019
|
+
glossary = resolveGlossary(rawGlossary, options.targetLang);
|
|
3020
|
+
if (this.verbose) {
|
|
3021
|
+
logger.info(`Loaded glossary from config: ${glossary.terms.length} terms`);
|
|
3022
|
+
}
|
|
3023
|
+
} catch {
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
let result;
|
|
3027
|
+
switch (format) {
|
|
3028
|
+
case "markdown":
|
|
3029
|
+
result = await this.translateMarkdown(options, glossary);
|
|
3030
|
+
break;
|
|
3031
|
+
case "html":
|
|
3032
|
+
result = await this.translatePlainText(options, glossary);
|
|
3033
|
+
break;
|
|
3034
|
+
case "text":
|
|
3035
|
+
default:
|
|
3036
|
+
result = await this.translatePlainText(options, glossary);
|
|
3037
|
+
break;
|
|
3038
|
+
}
|
|
3039
|
+
result.metadata.totalDuration = timer.elapsed();
|
|
3040
|
+
if (glossary && glossary.terms.length > 0) {
|
|
3041
|
+
const compliance = this.checkDocumentGlossaryCompliance(
|
|
3042
|
+
options.content,
|
|
3043
|
+
result.content,
|
|
3044
|
+
glossary
|
|
3045
|
+
);
|
|
3046
|
+
result.glossaryCompliance = compliance;
|
|
3047
|
+
if (this.verbose) {
|
|
3048
|
+
logger.info(`Glossary compliance: ${compliance.applied.length}/${compliance.applied.length + compliance.missed.length} terms applied`);
|
|
3049
|
+
if (compliance.missed.length > 0) {
|
|
3050
|
+
logger.warn(`Missed glossary terms: ${compliance.missed.join(", ")}`);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
if (options.strictGlossary && !compliance.compliant) {
|
|
3054
|
+
throw new TranslationError("GLOSSARY_COMPLIANCE_FAILED" /* GLOSSARY_COMPLIANCE_FAILED */, {
|
|
3055
|
+
missed: compliance.missed.join(", "),
|
|
3056
|
+
applied: compliance.applied,
|
|
3057
|
+
total: glossary.terms.length
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (this.verbose) {
|
|
3062
|
+
logger.success(`Translation complete in ${timer.format()}`);
|
|
3063
|
+
logger.info(`Average quality: ${result.metadata.averageQuality.toFixed(1)}/100`);
|
|
3064
|
+
}
|
|
3065
|
+
return result;
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Check glossary compliance for the entire document
|
|
3069
|
+
*/
|
|
3070
|
+
checkDocumentGlossaryCompliance(sourceContent, translatedContent, glossary) {
|
|
3071
|
+
const applied = [];
|
|
3072
|
+
const missed = [];
|
|
3073
|
+
const sourceLower = sourceContent.toLowerCase();
|
|
3074
|
+
const translatedLower = translatedContent.toLowerCase();
|
|
3075
|
+
for (const term of glossary.terms) {
|
|
3076
|
+
const sourceInContent = term.caseSensitive ? sourceContent.includes(term.source) : sourceLower.includes(term.source.toLowerCase());
|
|
3077
|
+
if (!sourceInContent) {
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
const targetInTranslation = term.caseSensitive ? translatedContent.includes(term.target) : translatedLower.includes(term.target.toLowerCase());
|
|
3081
|
+
if (targetInTranslation) {
|
|
3082
|
+
applied.push(term.source);
|
|
3083
|
+
} else {
|
|
3084
|
+
missed.push(term.source);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return {
|
|
3088
|
+
applied,
|
|
3089
|
+
missed,
|
|
3090
|
+
compliant: missed.length === 0
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
// ============================================================================
|
|
3094
|
+
// Format-Specific Translation
|
|
3095
|
+
// ============================================================================
|
|
3096
|
+
async translateMarkdown(options, glossary) {
|
|
3097
|
+
const { text, preservedSections } = extractTextForTranslation(options.content);
|
|
3098
|
+
const chunks = chunkContent(text, {
|
|
3099
|
+
maxTokens: this.config.chunking.maxTokens,
|
|
3100
|
+
overlapTokens: this.config.chunking.overlapTokens
|
|
3101
|
+
});
|
|
3102
|
+
if (this.verbose) {
|
|
3103
|
+
const stats = getChunkStats(chunks);
|
|
3104
|
+
logger.info(`Chunked into ${stats.translatableChunks} translatable sections`);
|
|
3105
|
+
}
|
|
3106
|
+
const agent = createTranslationAgent({
|
|
3107
|
+
provider: this.provider,
|
|
3108
|
+
qualityThreshold: options.qualityThreshold ?? this.config.quality.threshold,
|
|
3109
|
+
maxIterations: options.maxIterations ?? this.config.quality.maxIterations,
|
|
3110
|
+
verbose: this.verbose,
|
|
3111
|
+
strictQuality: options.strictQuality
|
|
3112
|
+
});
|
|
3113
|
+
const chunkResults = [];
|
|
3114
|
+
let totalInputTokens = 0;
|
|
3115
|
+
let totalOutputTokens = 0;
|
|
3116
|
+
let totalIterations = 0;
|
|
3117
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
3118
|
+
const chunk = chunks[i];
|
|
3119
|
+
if (!chunk) continue;
|
|
3120
|
+
if (chunk.type === "preserve") {
|
|
3121
|
+
chunkResults.push({
|
|
3122
|
+
original: chunk.content,
|
|
3123
|
+
translated: chunk.content,
|
|
3124
|
+
startOffset: chunk.startOffset,
|
|
3125
|
+
endOffset: chunk.endOffset,
|
|
3126
|
+
qualityScore: 100
|
|
3127
|
+
});
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
if (this.verbose) {
|
|
3131
|
+
logger.info(`Translating chunk ${i + 1}/${chunks.length}...`);
|
|
3132
|
+
}
|
|
3133
|
+
const result = await this.translateChunk(chunk, options, glossary, agent);
|
|
3134
|
+
chunkResults.push(result);
|
|
3135
|
+
if (result.tokensUsed) {
|
|
3136
|
+
totalInputTokens += result.tokensUsed.input;
|
|
3137
|
+
totalOutputTokens += result.tokensUsed.output;
|
|
3138
|
+
}
|
|
3139
|
+
if (result.iterations) {
|
|
3140
|
+
totalIterations += result.iterations;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
const translatedText = chunkResults.map((r) => r.translated).join("");
|
|
3144
|
+
const finalContent = restorePreservedSections(translatedText, preservedSections);
|
|
3145
|
+
const qualityScores = chunkResults.filter((r) => r.qualityScore > 0).map((r) => r.qualityScore);
|
|
3146
|
+
const averageQuality = qualityScores.length > 0 ? qualityScores.reduce((a, b) => a + b, 0) / qualityScores.length : 0;
|
|
3147
|
+
const cacheHits = chunkResults.filter((r) => r.cached).length;
|
|
3148
|
+
const cacheMisses = chunkResults.filter((r) => !r.cached && r.qualityScore > 0).length;
|
|
3149
|
+
return {
|
|
3150
|
+
content: finalContent,
|
|
3151
|
+
chunks: chunkResults,
|
|
3152
|
+
metadata: {
|
|
3153
|
+
totalTokensUsed: totalInputTokens + totalOutputTokens,
|
|
3154
|
+
totalDuration: 0,
|
|
3155
|
+
// Will be set by caller
|
|
3156
|
+
averageQuality,
|
|
3157
|
+
provider: this.provider.name,
|
|
3158
|
+
model: this.config.provider.model ?? this.provider.defaultModel,
|
|
3159
|
+
totalIterations,
|
|
3160
|
+
tokensUsed: {
|
|
3161
|
+
input: totalInputTokens,
|
|
3162
|
+
output: totalOutputTokens
|
|
3163
|
+
},
|
|
3164
|
+
cache: {
|
|
3165
|
+
hits: cacheHits,
|
|
3166
|
+
misses: cacheMisses
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
async translatePlainText(options, glossary) {
|
|
3172
|
+
const chunks = chunkContent(options.content, {
|
|
3173
|
+
maxTokens: this.config.chunking.maxTokens,
|
|
3174
|
+
overlapTokens: this.config.chunking.overlapTokens
|
|
3175
|
+
});
|
|
3176
|
+
const agent = createTranslationAgent({
|
|
3177
|
+
provider: this.provider,
|
|
3178
|
+
qualityThreshold: options.qualityThreshold ?? this.config.quality.threshold,
|
|
3179
|
+
maxIterations: options.maxIterations ?? this.config.quality.maxIterations,
|
|
3180
|
+
verbose: this.verbose,
|
|
3181
|
+
strictQuality: options.strictQuality
|
|
3182
|
+
});
|
|
3183
|
+
const chunkResults = [];
|
|
3184
|
+
let totalInputTokens = 0;
|
|
3185
|
+
let totalOutputTokens = 0;
|
|
3186
|
+
let totalIterations = 0;
|
|
3187
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
3188
|
+
const chunk = chunks[i];
|
|
3189
|
+
if (!chunk) continue;
|
|
3190
|
+
if (chunk.type === "preserve") {
|
|
3191
|
+
chunkResults.push({
|
|
3192
|
+
original: chunk.content,
|
|
3193
|
+
translated: chunk.content,
|
|
3194
|
+
startOffset: chunk.startOffset,
|
|
3195
|
+
endOffset: chunk.endOffset,
|
|
3196
|
+
qualityScore: 100
|
|
3197
|
+
});
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
if (this.verbose) {
|
|
3201
|
+
logger.info(`Translating chunk ${i + 1}/${chunks.length}...`);
|
|
3202
|
+
}
|
|
3203
|
+
const result = await this.translateChunk(chunk, options, glossary, agent);
|
|
3204
|
+
chunkResults.push(result);
|
|
3205
|
+
if (result.tokensUsed) {
|
|
3206
|
+
totalInputTokens += result.tokensUsed.input;
|
|
3207
|
+
totalOutputTokens += result.tokensUsed.output;
|
|
3208
|
+
}
|
|
3209
|
+
if (result.iterations) {
|
|
3210
|
+
totalIterations += result.iterations;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
const translatedContent = chunkResults.map((r) => r.translated).join("");
|
|
3214
|
+
const qualityScores = chunkResults.filter((r) => r.qualityScore > 0).map((r) => r.qualityScore);
|
|
3215
|
+
const averageQuality = qualityScores.length > 0 ? qualityScores.reduce((a, b) => a + b, 0) / qualityScores.length : 0;
|
|
3216
|
+
const cacheHits = chunkResults.filter((r) => r.cached).length;
|
|
3217
|
+
const cacheMisses = chunkResults.filter((r) => !r.cached && r.qualityScore > 0).length;
|
|
3218
|
+
return {
|
|
3219
|
+
content: translatedContent,
|
|
3220
|
+
chunks: chunkResults,
|
|
3221
|
+
metadata: {
|
|
3222
|
+
totalTokensUsed: totalInputTokens + totalOutputTokens,
|
|
3223
|
+
totalDuration: 0,
|
|
3224
|
+
averageQuality,
|
|
3225
|
+
provider: this.provider.name,
|
|
3226
|
+
model: this.config.provider.model ?? this.provider.defaultModel,
|
|
3227
|
+
totalIterations,
|
|
3228
|
+
tokensUsed: {
|
|
3229
|
+
input: totalInputTokens,
|
|
3230
|
+
output: totalOutputTokens
|
|
3231
|
+
},
|
|
3232
|
+
cache: {
|
|
3233
|
+
hits: cacheHits,
|
|
3234
|
+
misses: cacheMisses
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
async translateChunk(chunk, options, glossary, agent) {
|
|
3240
|
+
const glossaryString = glossary ? JSON.stringify(glossary.terms.map((t) => ({ s: t.source, t: t.target }))) : void 0;
|
|
3241
|
+
const cacheKey = {
|
|
3242
|
+
content: chunk.content,
|
|
3243
|
+
sourceLang: options.sourceLang,
|
|
3244
|
+
targetLang: options.targetLang,
|
|
3245
|
+
glossary: glossaryString,
|
|
3246
|
+
provider: this.provider.name,
|
|
3247
|
+
model: this.config.provider.model ?? this.provider.defaultModel
|
|
3248
|
+
};
|
|
3249
|
+
const cacheResult = this.cache.get(cacheKey);
|
|
3250
|
+
if (cacheResult.hit && cacheResult.entry) {
|
|
3251
|
+
this.cacheHits++;
|
|
3252
|
+
if (this.verbose) {
|
|
3253
|
+
logger.info(` \u21B3 Cache hit (quality: ${cacheResult.entry.qualityScore})`);
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
original: chunk.content,
|
|
3257
|
+
translated: cacheResult.entry.translation,
|
|
3258
|
+
startOffset: chunk.startOffset,
|
|
3259
|
+
endOffset: chunk.endOffset,
|
|
3260
|
+
qualityScore: cacheResult.entry.qualityScore,
|
|
3261
|
+
iterations: 0,
|
|
3262
|
+
tokensUsed: { input: 0, output: 0, cacheRead: 1 },
|
|
3263
|
+
cached: true
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
this.cacheMisses++;
|
|
3267
|
+
const resolvedStyleInstruction = options.styleInstruction ?? this.config.languages.styles?.[options.targetLang];
|
|
3268
|
+
const context = {
|
|
3269
|
+
documentPurpose: options.context,
|
|
3270
|
+
styleInstruction: resolvedStyleInstruction
|
|
3271
|
+
};
|
|
3272
|
+
if (chunk.metadata?.headerHierarchy && chunk.metadata.headerHierarchy.length > 0) {
|
|
3273
|
+
context.documentSummary = `Current section: ${chunk.metadata.headerHierarchy.join(" > ")}`;
|
|
3274
|
+
}
|
|
3275
|
+
if (chunk.metadata?.previousContext) {
|
|
3276
|
+
context.previousChunks = [chunk.metadata.previousContext];
|
|
3277
|
+
}
|
|
3278
|
+
const request = {
|
|
3279
|
+
content: chunk.content,
|
|
3280
|
+
sourceLang: options.sourceLang,
|
|
3281
|
+
targetLang: options.targetLang,
|
|
3282
|
+
format: options.format ?? "text",
|
|
3283
|
+
glossary,
|
|
3284
|
+
context
|
|
3285
|
+
};
|
|
3286
|
+
try {
|
|
3287
|
+
const result = await agent.translate(request);
|
|
3288
|
+
this.cache.set(cacheKey, result.content, result.metadata.qualityScore);
|
|
3289
|
+
return {
|
|
3290
|
+
original: chunk.content,
|
|
3291
|
+
translated: result.content,
|
|
3292
|
+
startOffset: chunk.startOffset,
|
|
3293
|
+
endOffset: chunk.endOffset,
|
|
3294
|
+
qualityScore: result.metadata.qualityScore,
|
|
3295
|
+
iterations: result.metadata.iterations,
|
|
3296
|
+
tokensUsed: result.metadata.tokensUsed
|
|
3297
|
+
};
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
logger.error(`Failed to translate chunk: ${error}`);
|
|
3300
|
+
return {
|
|
3301
|
+
original: chunk.content,
|
|
3302
|
+
translated: chunk.content,
|
|
3303
|
+
// Fallback to original
|
|
3304
|
+
startOffset: chunk.startOffset,
|
|
3305
|
+
endOffset: chunk.endOffset,
|
|
3306
|
+
qualityScore: 0,
|
|
3307
|
+
iterations: 0,
|
|
3308
|
+
tokensUsed: { input: 0, output: 0 }
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
// ============================================================================
|
|
3313
|
+
// Utility Methods
|
|
3314
|
+
// ============================================================================
|
|
3315
|
+
detectFormat(content) {
|
|
3316
|
+
if (content.includes("# ") || content.includes("## ") || content.includes("```") || content.includes("- ") || content.match(/\[.+\]\(.+\)/)) {
|
|
3317
|
+
return "markdown";
|
|
3318
|
+
}
|
|
3319
|
+
if (content.includes("<html") || content.includes("<body") || content.includes("<div") || content.includes("<p>")) {
|
|
3320
|
+
return "html";
|
|
3321
|
+
}
|
|
3322
|
+
return "text";
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
|
|
3328
|
+
// src/cli/commands/file.ts
|
|
3329
|
+
var file_exports = {};
|
|
3330
|
+
__export(file_exports, {
|
|
3331
|
+
fileCommand: () => fileCommand,
|
|
3332
|
+
handleStdinTranslation: () => handleStdinTranslation
|
|
3333
|
+
});
|
|
3334
|
+
async function handleStdinTranslation(options) {
|
|
3335
|
+
try {
|
|
3336
|
+
configureLogger({
|
|
3337
|
+
level: "error",
|
|
3338
|
+
quiet: true,
|
|
3339
|
+
json: false
|
|
3340
|
+
});
|
|
3341
|
+
if (!options.sourceLang) {
|
|
3342
|
+
console.error("Error: Source language (-s, --source-lang) is required");
|
|
3343
|
+
process.exit(2);
|
|
3344
|
+
}
|
|
3345
|
+
if (!options.targetLang) {
|
|
3346
|
+
console.error("Error: Target language (-t, --target-lang) is required");
|
|
3347
|
+
process.exit(2);
|
|
3348
|
+
}
|
|
3349
|
+
const chunks = [];
|
|
3350
|
+
for await (const chunk of process.stdin) {
|
|
3351
|
+
chunks.push(chunk);
|
|
3352
|
+
}
|
|
3353
|
+
const content = Buffer.concat(chunks).toString("utf-8");
|
|
3354
|
+
if (!content.trim()) {
|
|
3355
|
+
console.error("Error: No input provided");
|
|
3356
|
+
process.exit(2);
|
|
3357
|
+
}
|
|
3358
|
+
const baseConfig = await loadConfig({ configPath: options.config });
|
|
3359
|
+
const config2 = mergeConfig(baseConfig, {
|
|
3360
|
+
sourceLang: options.sourceLang,
|
|
3361
|
+
targetLang: options.targetLang,
|
|
3362
|
+
provider: options.provider,
|
|
3363
|
+
model: options.model,
|
|
3364
|
+
quality: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3365
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3366
|
+
glossary: options.glossary
|
|
3367
|
+
});
|
|
3368
|
+
const engine = createTranslationEngine({
|
|
3369
|
+
config: config2,
|
|
3370
|
+
verbose: false,
|
|
3371
|
+
noCache: options.cache === false
|
|
3372
|
+
});
|
|
3373
|
+
const result = await engine.translateContent({
|
|
3374
|
+
content,
|
|
3375
|
+
sourceLang: options.sourceLang,
|
|
3376
|
+
targetLang: options.targetLang,
|
|
3377
|
+
format: mapFormat(options.format),
|
|
3378
|
+
glossaryPath: options.glossary,
|
|
3379
|
+
qualityThreshold: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3380
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3381
|
+
context: options.context
|
|
3382
|
+
});
|
|
3383
|
+
process.stdout.write(result.content);
|
|
3384
|
+
} catch (error) {
|
|
3385
|
+
if (error instanceof TranslationError) {
|
|
3386
|
+
console.error(`Error: ${error.message}`);
|
|
3387
|
+
process.exit(getExitCode(error));
|
|
3388
|
+
}
|
|
3389
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
3390
|
+
process.exit(1);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
function determineOutputPath(inputPath, targetLang) {
|
|
3394
|
+
const dir = dirname(inputPath);
|
|
3395
|
+
const ext = extname(inputPath);
|
|
3396
|
+
const base = basename(inputPath, ext);
|
|
3397
|
+
return resolve(dir, `${base}.${targetLang}${ext}`);
|
|
3398
|
+
}
|
|
3399
|
+
function mapFormat(format) {
|
|
3400
|
+
if (!format) return void 0;
|
|
3401
|
+
switch (format.toLowerCase()) {
|
|
3402
|
+
case "md":
|
|
3403
|
+
case "markdown":
|
|
3404
|
+
return "markdown";
|
|
3405
|
+
case "html":
|
|
3406
|
+
return "html";
|
|
3407
|
+
case "txt":
|
|
3408
|
+
case "text":
|
|
3409
|
+
return "text";
|
|
3410
|
+
default:
|
|
3411
|
+
return void 0;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
var fileCommand;
|
|
3415
|
+
var init_file = __esm({
|
|
3416
|
+
"src/cli/commands/file.ts"() {
|
|
3417
|
+
init_options();
|
|
3418
|
+
init_config();
|
|
3419
|
+
init_engine();
|
|
3420
|
+
init_logger();
|
|
3421
|
+
init_errors();
|
|
3422
|
+
fileCommand = new Command("file").description("Translate a single file").argument("<input>", "Input file path").argument("[output]", "Output file path (optional)").option("-s, --source-lang <lang>", "Source language code").option("-t, --target-lang <lang>", "Target language code").option("-g, --glossary <path>", "Path to glossary file").option(
|
|
3423
|
+
"-p, --provider <name>",
|
|
3424
|
+
"LLM provider (claude|openai|ollama)",
|
|
3425
|
+
defaults.provider
|
|
3426
|
+
).option("-m, --model <name>", "Model name").option(
|
|
3427
|
+
"--quality <0-100>",
|
|
3428
|
+
"Quality threshold",
|
|
3429
|
+
String(defaults.quality)
|
|
3430
|
+
).option(
|
|
3431
|
+
"--max-iterations <n>",
|
|
3432
|
+
"Max refinement iterations",
|
|
3433
|
+
String(defaults.maxIterations)
|
|
3434
|
+
).option("-o, --output <path>", "Output path").option("-f, --format <fmt>", "Force output format (md|html|txt)").option("--dry-run", "Show what would be translated").option("--json", "Output results as JSON").option(
|
|
3435
|
+
"--chunk-size <tokens>",
|
|
3436
|
+
"Max tokens per chunk",
|
|
3437
|
+
String(defaults.chunkSize)
|
|
3438
|
+
).option("--no-cache", "Disable translation cache").option("--context <text>", "Additional context for translation").option("--strict-quality", "Fail if quality threshold is not met").option("--strict-glossary", "Fail if glossary terms are not applied").option("-v, --verbose", "Enable verbose logging").option("-q, --quiet", "Suppress non-error output").action(async (input, output, options) => {
|
|
3439
|
+
try {
|
|
3440
|
+
configureLogger({
|
|
3441
|
+
level: options.verbose ? "debug" : "info",
|
|
3442
|
+
quiet: options.quiet ?? false,
|
|
3443
|
+
json: options.json ?? false
|
|
3444
|
+
});
|
|
3445
|
+
if (!options.sourceLang) {
|
|
3446
|
+
console.error("Error: Source language (-s, --source-lang) is required");
|
|
3447
|
+
process.exit(2);
|
|
3448
|
+
}
|
|
3449
|
+
if (!options.targetLang) {
|
|
3450
|
+
console.error("Error: Target language (-t, --target-lang) is required");
|
|
3451
|
+
process.exit(2);
|
|
3452
|
+
}
|
|
3453
|
+
const baseConfig = await loadConfig({ configPath: options.config });
|
|
3454
|
+
const config2 = mergeConfig(baseConfig, {
|
|
3455
|
+
sourceLang: options.sourceLang,
|
|
3456
|
+
targetLang: options.targetLang,
|
|
3457
|
+
provider: options.provider,
|
|
3458
|
+
model: options.model,
|
|
3459
|
+
quality: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3460
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3461
|
+
chunkSize: options.chunkSize ? parseInt(options.chunkSize, 10) : void 0,
|
|
3462
|
+
glossary: options.glossary,
|
|
3463
|
+
noCache: options.cache === false
|
|
3464
|
+
});
|
|
3465
|
+
const inputPath = resolve(input);
|
|
3466
|
+
let content;
|
|
3467
|
+
try {
|
|
3468
|
+
content = await readFile(inputPath, "utf-8");
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
console.error(`Error: Could not read file '${inputPath}'`);
|
|
3471
|
+
process.exit(3);
|
|
3472
|
+
}
|
|
3473
|
+
if (!options.quiet) {
|
|
3474
|
+
logger.info(`Reading: ${inputPath}`);
|
|
3475
|
+
logger.info(`Translating: ${options.sourceLang} \u2192 ${options.targetLang}`);
|
|
3476
|
+
if (options.glossary) {
|
|
3477
|
+
logger.info(`Glossary: ${resolve(options.glossary)}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
if (options.dryRun) {
|
|
3481
|
+
console.log("Dry run mode - no translation will be performed");
|
|
3482
|
+
console.log(`Input: ${inputPath}`);
|
|
3483
|
+
console.log(`Output: ${output ?? determineOutputPath(inputPath, options.targetLang)}`);
|
|
3484
|
+
console.log(`Source language: ${options.sourceLang}`);
|
|
3485
|
+
console.log(`Target language: ${options.targetLang}`);
|
|
3486
|
+
console.log(`Content length: ${content.length} characters`);
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
const engine = createTranslationEngine({
|
|
3490
|
+
config: config2,
|
|
3491
|
+
verbose: options.verbose,
|
|
3492
|
+
noCache: options.cache === false
|
|
3493
|
+
});
|
|
3494
|
+
const result = await engine.translateContent({
|
|
3495
|
+
content,
|
|
3496
|
+
sourceLang: options.sourceLang,
|
|
3497
|
+
targetLang: options.targetLang,
|
|
3498
|
+
format: mapFormat(options.format),
|
|
3499
|
+
glossaryPath: options.glossary,
|
|
3500
|
+
qualityThreshold: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3501
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3502
|
+
context: options.context,
|
|
3503
|
+
strictQuality: options.strictQuality,
|
|
3504
|
+
strictGlossary: options.strictGlossary
|
|
3505
|
+
});
|
|
3506
|
+
const outputPath = output ?? options.output ?? determineOutputPath(inputPath, options.targetLang);
|
|
3507
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
3508
|
+
await writeFile(outputPath, result.content, "utf-8");
|
|
3509
|
+
if (options.json) {
|
|
3510
|
+
console.log(JSON.stringify({
|
|
3511
|
+
success: true,
|
|
3512
|
+
input: inputPath,
|
|
3513
|
+
output: outputPath,
|
|
3514
|
+
sourceLang: options.sourceLang,
|
|
3515
|
+
targetLang: options.targetLang,
|
|
3516
|
+
quality: result.metadata.averageQuality,
|
|
3517
|
+
duration: result.metadata.totalDuration,
|
|
3518
|
+
chunks: result.chunks.length,
|
|
3519
|
+
provider: result.metadata.provider,
|
|
3520
|
+
model: result.metadata.model,
|
|
3521
|
+
iterations: result.metadata.totalIterations,
|
|
3522
|
+
tokensUsed: result.metadata.tokensUsed
|
|
3523
|
+
}, null, 2));
|
|
3524
|
+
} else if (!options.quiet) {
|
|
3525
|
+
logger.success(`Written to ${outputPath}`);
|
|
3526
|
+
console.log("");
|
|
3527
|
+
console.log(" Translation Summary:");
|
|
3528
|
+
console.log(` - Model: ${result.metadata.provider}/${result.metadata.model}`);
|
|
3529
|
+
console.log(` - Quality: ${result.metadata.averageQuality.toFixed(0)}/100`);
|
|
3530
|
+
console.log(` - Chunks: ${result.chunks.length}`);
|
|
3531
|
+
console.log(` - Iterations: ${result.metadata.totalIterations}`);
|
|
3532
|
+
console.log(` - Tokens: ${result.metadata.tokensUsed.input.toLocaleString()} input / ${result.metadata.tokensUsed.output.toLocaleString()} output`);
|
|
3533
|
+
console.log(` - Duration: ${(result.metadata.totalDuration / 1e3).toFixed(1)}s`);
|
|
3534
|
+
}
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
if (error instanceof TranslationError) {
|
|
3537
|
+
console.error(`Error: ${error.message}`);
|
|
3538
|
+
process.exit(getExitCode(error));
|
|
3539
|
+
}
|
|
3540
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
3541
|
+
process.exit(1);
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
|
|
3547
|
+
// src/cli/index.ts
|
|
3548
|
+
init_file();
|
|
3549
|
+
|
|
3550
|
+
// src/cli/commands/dir.ts
|
|
3551
|
+
init_options();
|
|
3552
|
+
init_config();
|
|
3553
|
+
init_engine();
|
|
3554
|
+
init_logger();
|
|
3555
|
+
init_errors();
|
|
3556
|
+
var dirCommand = new Command("dir").description("Translate all files in a directory").argument("<input>", "Input directory path").argument("<output>", "Output directory path").option("-s, --source-lang <lang>", "Source language code").option("-t, --target-lang <lang>", "Target language code").option("-g, --glossary <path>", "Path to glossary file").option(
|
|
3557
|
+
"-p, --provider <name>",
|
|
3558
|
+
"LLM provider (claude|openai|ollama)",
|
|
3559
|
+
defaults.provider
|
|
3560
|
+
).option("-m, --model <name>", "Model name").option(
|
|
3561
|
+
"--quality <0-100>",
|
|
3562
|
+
"Quality threshold",
|
|
3563
|
+
String(defaults.quality)
|
|
3564
|
+
).option(
|
|
3565
|
+
"--max-iterations <n>",
|
|
3566
|
+
"Max refinement iterations",
|
|
3567
|
+
String(defaults.maxIterations)
|
|
3568
|
+
).option("-f, --format <fmt>", "Force output format (md|html|txt)").option("--dry-run", "Show what would be translated").option("--json", "Output results as JSON").option(
|
|
3569
|
+
"--chunk-size <tokens>",
|
|
3570
|
+
"Max tokens per chunk",
|
|
3571
|
+
String(defaults.chunkSize)
|
|
3572
|
+
).option(
|
|
3573
|
+
"--parallel <n>",
|
|
3574
|
+
"Parallel file processing",
|
|
3575
|
+
String(defaults.parallel)
|
|
3576
|
+
).option("--no-cache", "Disable translation cache").option("--context <text>", "Additional context for translation").option("--include <patterns>", "File patterns to include (comma-separated)", "*.md,*.markdown").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-v, --verbose", "Enable verbose logging").option("-q, --quiet", "Suppress non-error output").action(async (input, output, options) => {
|
|
3577
|
+
try {
|
|
3578
|
+
configureLogger({
|
|
3579
|
+
level: options.verbose ? "debug" : "info",
|
|
3580
|
+
quiet: options.quiet ?? false,
|
|
3581
|
+
json: options.json ?? false
|
|
3582
|
+
});
|
|
3583
|
+
if (!options.targetLang) {
|
|
3584
|
+
console.error("Error: Target language (-t, --target-lang) is required");
|
|
3585
|
+
process.exit(2);
|
|
3586
|
+
}
|
|
3587
|
+
const inputDir = resolve(input);
|
|
3588
|
+
const outputDir = resolve(output);
|
|
3589
|
+
const includePatterns = options.include?.split(",").map((p) => p.trim()) ?? ["*.md", "*.markdown"];
|
|
3590
|
+
const excludePatterns = options.exclude?.split(",").map((p) => p.trim()) ?? [];
|
|
3591
|
+
const files = await findFiles(inputDir, includePatterns, excludePatterns, outputDir);
|
|
3592
|
+
if (files.length === 0) {
|
|
3593
|
+
console.log("No files found matching the specified patterns");
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
if (!options.quiet) {
|
|
3597
|
+
logger.info(`Found ${files.length} file(s) to translate`);
|
|
3598
|
+
logger.info(`Input: ${inputDir}`);
|
|
3599
|
+
logger.info(`Output: ${outputDir}`);
|
|
3600
|
+
logger.info(`Target language: ${options.targetLang}`);
|
|
3601
|
+
if (options.glossary) {
|
|
3602
|
+
logger.info(`Glossary: ${resolve(options.glossary)}`);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
if (options.dryRun) {
|
|
3606
|
+
console.log("\nDry run mode - no translation will be performed\n");
|
|
3607
|
+
console.log("Files to translate:");
|
|
3608
|
+
for (const file of files) {
|
|
3609
|
+
const relativePath = relative(inputDir, file);
|
|
3610
|
+
const outputPath = join(outputDir, relativePath);
|
|
3611
|
+
console.log(` ${relativePath} \u2192 ${relative(process.cwd(), outputPath)}`);
|
|
3612
|
+
}
|
|
3613
|
+
console.log(`
|
|
3614
|
+
Total: ${files.length} file(s)`);
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
const baseConfig = await loadConfig({ configPath: options.config });
|
|
3618
|
+
const config2 = mergeConfig(baseConfig, {
|
|
3619
|
+
sourceLang: options.sourceLang,
|
|
3620
|
+
targetLang: options.targetLang,
|
|
3621
|
+
provider: options.provider,
|
|
3622
|
+
model: options.model,
|
|
3623
|
+
quality: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3624
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3625
|
+
chunkSize: options.chunkSize ? parseInt(options.chunkSize, 10) : void 0,
|
|
3626
|
+
glossary: options.glossary,
|
|
3627
|
+
noCache: options.cache === false
|
|
3628
|
+
});
|
|
3629
|
+
const engine = createTranslationEngine({
|
|
3630
|
+
config: config2,
|
|
3631
|
+
verbose: options.verbose,
|
|
3632
|
+
noCache: options.cache === false
|
|
3633
|
+
});
|
|
3634
|
+
const parallelCount = typeof options.parallel === "string" ? parseInt(options.parallel, 10) : options.parallel ?? defaults.parallel;
|
|
3635
|
+
if (!options.quiet) {
|
|
3636
|
+
logger.info(`Parallel processing: ${parallelCount} file(s) at a time`);
|
|
3637
|
+
}
|
|
3638
|
+
const results = await processFiles(
|
|
3639
|
+
files,
|
|
3640
|
+
inputDir,
|
|
3641
|
+
outputDir,
|
|
3642
|
+
engine,
|
|
3643
|
+
options,
|
|
3644
|
+
parallelCount
|
|
3645
|
+
);
|
|
3646
|
+
outputResults(results, options);
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
if (error instanceof TranslationError) {
|
|
3649
|
+
console.error(`Error: ${error.message}`);
|
|
3650
|
+
process.exit(getExitCode(error));
|
|
3651
|
+
}
|
|
3652
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
3653
|
+
process.exit(1);
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3656
|
+
async function findFiles(dir, includePatterns, excludePatterns, outputDir) {
|
|
3657
|
+
const files = [];
|
|
3658
|
+
const outputRelative = outputDir ? relative(dir, outputDir) : null;
|
|
3659
|
+
const isOutputInsideInput = outputRelative && !outputRelative.startsWith("..");
|
|
3660
|
+
async function scan(currentDir) {
|
|
3661
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
3662
|
+
for (const entry of entries) {
|
|
3663
|
+
const fullPath = join(currentDir, entry.name);
|
|
3664
|
+
const relativePath = relative(dir, fullPath);
|
|
3665
|
+
if (entry.name.startsWith(".")) {
|
|
3666
|
+
continue;
|
|
3667
|
+
}
|
|
3668
|
+
if (entry.isDirectory()) {
|
|
3669
|
+
if (isOutputInsideInput && relativePath === outputRelative) {
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
if (/^[a-z]{2}(-[A-Z]{2})?$/.test(entry.name)) {
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3675
|
+
if (!matchesPatterns(relativePath + "/", excludePatterns)) {
|
|
3676
|
+
await scan(fullPath);
|
|
3677
|
+
}
|
|
3678
|
+
} else if (entry.isFile()) {
|
|
3679
|
+
if (matchesPatterns(entry.name, includePatterns) && !matchesPatterns(relativePath, excludePatterns)) {
|
|
3680
|
+
files.push(fullPath);
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
await scan(dir);
|
|
3686
|
+
return files.sort();
|
|
3687
|
+
}
|
|
3688
|
+
function matchesPatterns(path, patterns) {
|
|
3689
|
+
for (const pattern of patterns) {
|
|
3690
|
+
if (matchGlob(path, pattern)) {
|
|
3691
|
+
return true;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
function matchGlob(path, pattern) {
|
|
3697
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{DOUBLESTAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLESTAR}}/g, ".*").replace(/\?/g, ".");
|
|
3698
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
3699
|
+
return regex.test(path);
|
|
3700
|
+
}
|
|
3701
|
+
async function processFiles(files, inputDir, outputDir, engine, options, parallelCount) {
|
|
3702
|
+
const startTime2 = Date.now();
|
|
3703
|
+
const results = new Array(files.length);
|
|
3704
|
+
let completed = 0;
|
|
3705
|
+
let nextIndex = 0;
|
|
3706
|
+
const processFile = async (inputPath, _index) => {
|
|
3707
|
+
const relativePath = relative(inputDir, inputPath);
|
|
3708
|
+
const outputPath = join(outputDir, relativePath);
|
|
3709
|
+
const fileStartTime = Date.now();
|
|
3710
|
+
try {
|
|
3711
|
+
const content = await readFile(inputPath, "utf-8");
|
|
3712
|
+
const result = await engine.translateContent({
|
|
3713
|
+
content,
|
|
3714
|
+
sourceLang: options.sourceLang,
|
|
3715
|
+
targetLang: options.targetLang,
|
|
3716
|
+
format: mapFormat2(options.format),
|
|
3717
|
+
glossaryPath: options.glossary,
|
|
3718
|
+
qualityThreshold: options.quality ? parseInt(options.quality, 10) : void 0,
|
|
3719
|
+
maxIterations: options.maxIterations ? parseInt(options.maxIterations, 10) : void 0,
|
|
3720
|
+
context: options.context
|
|
3721
|
+
});
|
|
3722
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
3723
|
+
await writeFile(outputPath, result.content, "utf-8");
|
|
3724
|
+
completed++;
|
|
3725
|
+
if (!options.quiet && !options.json) {
|
|
3726
|
+
const progress = `[${completed}/${files.length}]`;
|
|
3727
|
+
logger.success(`${progress} ${relativePath}`);
|
|
3728
|
+
}
|
|
3729
|
+
return {
|
|
3730
|
+
inputPath,
|
|
3731
|
+
outputPath,
|
|
3732
|
+
relativePath,
|
|
3733
|
+
success: true,
|
|
3734
|
+
result,
|
|
3735
|
+
duration: Date.now() - fileStartTime
|
|
3736
|
+
};
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
completed++;
|
|
3739
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3740
|
+
if (!options.quiet && !options.json) {
|
|
3741
|
+
const progress = `[${completed}/${files.length}]`;
|
|
3742
|
+
logger.error(`${progress} ${relativePath}: ${errorMessage}`);
|
|
3743
|
+
}
|
|
3744
|
+
return {
|
|
3745
|
+
inputPath,
|
|
3746
|
+
outputPath,
|
|
3747
|
+
relativePath,
|
|
3748
|
+
success: false,
|
|
3749
|
+
error: errorMessage,
|
|
3750
|
+
duration: Date.now() - fileStartTime
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
const worker = async () => {
|
|
3755
|
+
while (true) {
|
|
3756
|
+
const index = nextIndex++;
|
|
3757
|
+
if (index >= files.length) break;
|
|
3758
|
+
const inputPath = files[index];
|
|
3759
|
+
if (!inputPath) break;
|
|
3760
|
+
const result = await processFile(inputPath);
|
|
3761
|
+
results[index] = result;
|
|
3762
|
+
}
|
|
3763
|
+
};
|
|
3764
|
+
const workerCount = Math.min(parallelCount, files.length);
|
|
3765
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
3766
|
+
await Promise.all(workers);
|
|
3767
|
+
const successResults = results.filter((r) => r.success && r.result);
|
|
3768
|
+
const totalTokensInput = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.input ?? 0), 0);
|
|
3769
|
+
const totalTokensOutput = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.output ?? 0), 0);
|
|
3770
|
+
const totalCacheRead = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.cacheRead ?? 0), 0);
|
|
3771
|
+
const totalCacheWrite = successResults.reduce((sum, r) => sum + (r.result?.metadata.tokensUsed.cacheWrite ?? 0), 0);
|
|
3772
|
+
return {
|
|
3773
|
+
files: results,
|
|
3774
|
+
totalDuration: Date.now() - startTime2,
|
|
3775
|
+
successCount: results.filter((r) => r.success).length,
|
|
3776
|
+
failCount: results.filter((r) => !r.success).length,
|
|
3777
|
+
totalTokensInput,
|
|
3778
|
+
totalTokensOutput,
|
|
3779
|
+
totalCacheRead,
|
|
3780
|
+
totalCacheWrite
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
function outputResults(results, options) {
|
|
3784
|
+
if (options.json) {
|
|
3785
|
+
console.log(JSON.stringify({
|
|
3786
|
+
success: results.failCount === 0,
|
|
3787
|
+
totalFiles: results.files.length,
|
|
3788
|
+
successCount: results.successCount,
|
|
3789
|
+
failCount: results.failCount,
|
|
3790
|
+
totalDuration: results.totalDuration,
|
|
3791
|
+
tokensUsed: {
|
|
3792
|
+
input: results.totalTokensInput,
|
|
3793
|
+
output: results.totalTokensOutput,
|
|
3794
|
+
cacheRead: results.totalCacheRead,
|
|
3795
|
+
cacheWrite: results.totalCacheWrite
|
|
3796
|
+
},
|
|
3797
|
+
files: results.files.map((f) => ({
|
|
3798
|
+
input: f.relativePath,
|
|
3799
|
+
output: f.outputPath,
|
|
3800
|
+
success: f.success,
|
|
3801
|
+
error: f.error,
|
|
3802
|
+
duration: f.duration,
|
|
3803
|
+
quality: f.result?.metadata.averageQuality ?? 0,
|
|
3804
|
+
tokens: f.result ? {
|
|
3805
|
+
input: f.result.metadata.tokensUsed.input,
|
|
3806
|
+
output: f.result.metadata.tokensUsed.output
|
|
3807
|
+
} : void 0
|
|
3808
|
+
}))
|
|
3809
|
+
}, null, 2));
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
if (options.quiet) {
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
console.log("");
|
|
3816
|
+
console.log("\u2500".repeat(60));
|
|
3817
|
+
console.log(" Translation Summary");
|
|
3818
|
+
console.log("\u2500".repeat(60));
|
|
3819
|
+
console.log(` Files: ${results.successCount} succeeded, ${results.failCount} failed`);
|
|
3820
|
+
console.log(` Duration: ${(results.totalDuration / 1e3).toFixed(1)}s`);
|
|
3821
|
+
console.log(` Tokens: ${results.totalTokensInput.toLocaleString()} input / ${results.totalTokensOutput.toLocaleString()} output`);
|
|
3822
|
+
if (results.totalCacheRead > 0 || results.totalCacheWrite > 0) {
|
|
3823
|
+
console.log(` Cache: ${results.totalCacheRead.toLocaleString()} read / ${results.totalCacheWrite.toLocaleString()} write`);
|
|
3824
|
+
}
|
|
3825
|
+
if (results.failCount > 0) {
|
|
3826
|
+
console.log("");
|
|
3827
|
+
console.log(" Failed files:");
|
|
3828
|
+
for (const file of results.files.filter((f) => !f.success)) {
|
|
3829
|
+
console.log(` - ${file.relativePath}: ${file.error}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
console.log("\u2500".repeat(60));
|
|
3833
|
+
}
|
|
3834
|
+
function mapFormat2(format) {
|
|
3835
|
+
if (!format) return void 0;
|
|
3836
|
+
switch (format.toLowerCase()) {
|
|
3837
|
+
case "md":
|
|
3838
|
+
case "markdown":
|
|
3839
|
+
return "markdown";
|
|
3840
|
+
case "html":
|
|
3841
|
+
return "html";
|
|
3842
|
+
case "txt":
|
|
3843
|
+
case "text":
|
|
3844
|
+
return "text";
|
|
3845
|
+
default:
|
|
3846
|
+
return void 0;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
var defaultConfig2 = {
|
|
3850
|
+
version: "1.0",
|
|
3851
|
+
project: {
|
|
3852
|
+
name: "My Project",
|
|
3853
|
+
description: "Project description",
|
|
3854
|
+
purpose: "Technical documentation translation"
|
|
3855
|
+
},
|
|
3856
|
+
languages: {
|
|
3857
|
+
source: "en",
|
|
3858
|
+
targets: ["ko"]
|
|
3859
|
+
},
|
|
3860
|
+
provider: {
|
|
3861
|
+
default: "claude",
|
|
3862
|
+
model: "claude-sonnet-4-20250514"
|
|
3863
|
+
},
|
|
3864
|
+
quality: {
|
|
3865
|
+
threshold: 85,
|
|
3866
|
+
maxIterations: 4,
|
|
3867
|
+
evaluationMethod: "llm"
|
|
3868
|
+
},
|
|
3869
|
+
chunking: {
|
|
3870
|
+
maxTokens: 1024,
|
|
3871
|
+
overlapTokens: 150,
|
|
3872
|
+
preserveStructure: true
|
|
3873
|
+
},
|
|
3874
|
+
paths: {
|
|
3875
|
+
output: "./docs/{lang}",
|
|
3876
|
+
cache: "./.translate-cache"
|
|
3877
|
+
},
|
|
3878
|
+
ignore: ["**/node_modules/**", "**/*.test.md"]
|
|
3879
|
+
};
|
|
3880
|
+
var initCommand = new Command("init").description("Initialize project configuration").option("-f, --force", "Overwrite existing configuration").action(async (options) => {
|
|
3881
|
+
const configPath = join(process.cwd(), ".translaterc.json");
|
|
3882
|
+
if (!options.force) {
|
|
3883
|
+
try {
|
|
3884
|
+
await access(configPath);
|
|
3885
|
+
console.error(
|
|
3886
|
+
"Configuration file already exists. Use --force to overwrite."
|
|
3887
|
+
);
|
|
3888
|
+
process.exit(1);
|
|
3889
|
+
} catch {
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
try {
|
|
3893
|
+
await writeFile(configPath, JSON.stringify(defaultConfig2, null, 2));
|
|
3894
|
+
console.log("Created .translaterc.json");
|
|
3895
|
+
console.log("\nNext steps:");
|
|
3896
|
+
console.log("1. Edit .translaterc.json to configure your project");
|
|
3897
|
+
console.log("2. Set your API key: export ANTHROPIC_API_KEY=your-key");
|
|
3898
|
+
console.log("3. Run: llm-translate file <input> -s en -t ko");
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
console.error("Failed to create configuration file:", error);
|
|
3901
|
+
process.exit(1);
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
var glossaryCommand = new Command("glossary").description("Manage glossary (add, remove, list, validate)");
|
|
3905
|
+
glossaryCommand.command("list").description("List all terms in a glossary").argument("<path>", "Path to glossary file").option("--lang <lang>", "Filter by target language").action(async (path, options) => {
|
|
3906
|
+
try {
|
|
3907
|
+
const glossary = await loadGlossary2(path);
|
|
3908
|
+
console.log(`Glossary: ${glossary.metadata.name}`);
|
|
3909
|
+
console.log(`Source: ${glossary.metadata.sourceLang}`);
|
|
3910
|
+
console.log(`Targets: ${glossary.metadata.targetLangs.join(", ")}`);
|
|
3911
|
+
console.log(`Terms: ${glossary.terms.length}`);
|
|
3912
|
+
console.log("---");
|
|
3913
|
+
for (const term of glossary.terms) {
|
|
3914
|
+
if (term.doNotTranslate) {
|
|
3915
|
+
console.log(` ${term.source} \u2192 [do not translate]`);
|
|
3916
|
+
} else if (options.lang && term.targets[options.lang]) {
|
|
3917
|
+
console.log(` ${term.source} \u2192 ${term.targets[options.lang]}`);
|
|
3918
|
+
} else if (!options.lang) {
|
|
3919
|
+
const translations = Object.entries(term.targets).map(([lang, val]) => `${lang}: ${val}`).join(", ");
|
|
3920
|
+
console.log(` ${term.source} \u2192 ${translations || "[no translations]"}`);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
console.error("Failed to load glossary:", error);
|
|
3925
|
+
process.exit(1);
|
|
3926
|
+
}
|
|
3927
|
+
});
|
|
3928
|
+
glossaryCommand.command("validate").description("Validate a glossary file").argument("<path>", "Path to glossary file").action(async (path) => {
|
|
3929
|
+
try {
|
|
3930
|
+
const glossary = await loadGlossary2(path);
|
|
3931
|
+
const errors = validateGlossary(glossary);
|
|
3932
|
+
if (errors.length === 0) {
|
|
3933
|
+
console.log("Glossary is valid!");
|
|
3934
|
+
console.log(` Name: ${glossary.metadata.name}`);
|
|
3935
|
+
console.log(` Terms: ${glossary.terms.length}`);
|
|
3936
|
+
console.log(` Source: ${glossary.metadata.sourceLang}`);
|
|
3937
|
+
console.log(` Targets: ${glossary.metadata.targetLangs.join(", ")}`);
|
|
3938
|
+
} else {
|
|
3939
|
+
console.error("Glossary validation failed:");
|
|
3940
|
+
for (const error of errors) {
|
|
3941
|
+
console.error(` - ${error}`);
|
|
3942
|
+
}
|
|
3943
|
+
process.exit(6);
|
|
3944
|
+
}
|
|
3945
|
+
} catch (error) {
|
|
3946
|
+
console.error("Failed to load glossary:", error);
|
|
3947
|
+
process.exit(1);
|
|
3948
|
+
}
|
|
3949
|
+
});
|
|
3950
|
+
glossaryCommand.command("add").description("Add a term to the glossary").argument("<path>", "Path to glossary file").argument("<source>", "Source term").option("--target <lang:value...>", "Target translations (e.g., ko:\uBC88\uC5ED)").option("--context <text>", "Usage context").option("--do-not-translate", "Mark as do not translate").action(
|
|
3951
|
+
async (path, source, options) => {
|
|
3952
|
+
try {
|
|
3953
|
+
const glossary = await loadGlossary2(path);
|
|
3954
|
+
const existing = glossary.terms.find(
|
|
3955
|
+
(t) => t.source.toLowerCase() === source.toLowerCase()
|
|
3956
|
+
);
|
|
3957
|
+
if (existing) {
|
|
3958
|
+
console.error(`Term "${source}" already exists in glossary.`);
|
|
3959
|
+
process.exit(1);
|
|
3960
|
+
}
|
|
3961
|
+
const targets = {};
|
|
3962
|
+
if (options.target) {
|
|
3963
|
+
for (const t of options.target) {
|
|
3964
|
+
const [lang, ...rest] = t.split(":");
|
|
3965
|
+
if (lang && rest.length > 0) {
|
|
3966
|
+
targets[lang] = rest.join(":");
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
const newTerm = {
|
|
3971
|
+
source,
|
|
3972
|
+
targets,
|
|
3973
|
+
context: options.context,
|
|
3974
|
+
doNotTranslate: options.doNotTranslate
|
|
3975
|
+
};
|
|
3976
|
+
glossary.terms.push(newTerm);
|
|
3977
|
+
await writeFile(path, JSON.stringify(glossary, null, 2));
|
|
3978
|
+
console.log(`Added term: ${source}`);
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
console.error("Failed to add term:", error);
|
|
3981
|
+
process.exit(1);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
);
|
|
3985
|
+
glossaryCommand.command("remove").description("Remove a term from the glossary").argument("<path>", "Path to glossary file").argument("<source>", "Source term to remove").action(async (path, source) => {
|
|
3986
|
+
try {
|
|
3987
|
+
const glossary = await loadGlossary2(path);
|
|
3988
|
+
const index = glossary.terms.findIndex(
|
|
3989
|
+
(t) => t.source.toLowerCase() === source.toLowerCase()
|
|
3990
|
+
);
|
|
3991
|
+
if (index === -1) {
|
|
3992
|
+
console.error(`Term "${source}" not found in glossary.`);
|
|
3993
|
+
process.exit(1);
|
|
3994
|
+
}
|
|
3995
|
+
glossary.terms.splice(index, 1);
|
|
3996
|
+
await writeFile(path, JSON.stringify(glossary, null, 2));
|
|
3997
|
+
console.log(`Removed term: ${source}`);
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
console.error("Failed to remove term:", error);
|
|
4000
|
+
process.exit(1);
|
|
4001
|
+
}
|
|
4002
|
+
});
|
|
4003
|
+
async function loadGlossary2(path) {
|
|
4004
|
+
const content = await readFile(path, "utf-8");
|
|
4005
|
+
return JSON.parse(content);
|
|
4006
|
+
}
|
|
4007
|
+
function validateGlossary(glossary) {
|
|
4008
|
+
const errors = [];
|
|
4009
|
+
if (!glossary.metadata) {
|
|
4010
|
+
errors.push("Missing metadata section");
|
|
4011
|
+
} else {
|
|
4012
|
+
if (!glossary.metadata.name) {
|
|
4013
|
+
errors.push("Missing metadata.name");
|
|
4014
|
+
}
|
|
4015
|
+
if (!glossary.metadata.sourceLang) {
|
|
4016
|
+
errors.push("Missing metadata.sourceLang");
|
|
4017
|
+
}
|
|
4018
|
+
if (!glossary.metadata.targetLangs || glossary.metadata.targetLangs.length === 0) {
|
|
4019
|
+
errors.push("Missing or empty metadata.targetLangs");
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
if (!glossary.terms || !Array.isArray(glossary.terms)) {
|
|
4023
|
+
errors.push("Missing or invalid terms array");
|
|
4024
|
+
} else {
|
|
4025
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
4026
|
+
for (let i = 0; i < glossary.terms.length; i++) {
|
|
4027
|
+
const term = glossary.terms[i];
|
|
4028
|
+
if (!term) continue;
|
|
4029
|
+
if (!term.source) {
|
|
4030
|
+
errors.push(`Term at index ${i}: missing source`);
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
const normalizedSource = term.source.toLowerCase();
|
|
4034
|
+
if (seenSources.has(normalizedSource)) {
|
|
4035
|
+
errors.push(`Duplicate term: "${term.source}"`);
|
|
4036
|
+
}
|
|
4037
|
+
seenSources.add(normalizedSource);
|
|
4038
|
+
if (!term.doNotTranslate && Object.keys(term.targets).length === 0) {
|
|
4039
|
+
if (!term.doNotTranslateFor || term.doNotTranslateFor.length === 0) {
|
|
4040
|
+
errors.push(
|
|
4041
|
+
`Term "${term.source}": no translations and not marked as do-not-translate`
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return errors;
|
|
4048
|
+
}
|
|
4049
|
+
function createAuthMiddleware(config2) {
|
|
4050
|
+
return createMiddleware(async (c, next) => {
|
|
4051
|
+
if (!config2.enabled) {
|
|
4052
|
+
return next();
|
|
4053
|
+
}
|
|
4054
|
+
const expectedKey = config2.apiKey ?? process.env.TRANSLATE_API_KEY;
|
|
4055
|
+
if (!expectedKey) {
|
|
4056
|
+
return next();
|
|
4057
|
+
}
|
|
4058
|
+
let providedKey = c.req.header("X-API-Key");
|
|
4059
|
+
if (!providedKey) {
|
|
4060
|
+
const authHeader = c.req.header("Authorization");
|
|
4061
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
4062
|
+
providedKey = authHeader.slice(7);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
if (!providedKey) {
|
|
4066
|
+
throw new HTTPException(401, {
|
|
4067
|
+
message: "API key required. Provide via X-API-Key header or Authorization: Bearer <token>"
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
if (!timingSafeEqual(providedKey, expectedKey)) {
|
|
4071
|
+
throw new HTTPException(401, {
|
|
4072
|
+
message: "Invalid API key"
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
return next();
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
function timingSafeEqual(a, b) {
|
|
4079
|
+
if (a.length !== b.length) {
|
|
4080
|
+
let result2 = 1;
|
|
4081
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4082
|
+
for (let i = 0; i < maxLen; i++) {
|
|
4083
|
+
result2 |= (a.charCodeAt(i % a.length) || 0) ^ (b.charCodeAt(i % b.length) || 0);
|
|
4084
|
+
}
|
|
4085
|
+
return false;
|
|
4086
|
+
}
|
|
4087
|
+
let result = 0;
|
|
4088
|
+
for (let i = 0; i < a.length; i++) {
|
|
4089
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
4090
|
+
}
|
|
4091
|
+
return result === 0;
|
|
4092
|
+
}
|
|
4093
|
+
function createLoggerMiddleware(config2) {
|
|
4094
|
+
return createMiddleware(async (c, next) => {
|
|
4095
|
+
const start = Date.now();
|
|
4096
|
+
const requestId = generateRequestId();
|
|
4097
|
+
c.set("requestId", requestId);
|
|
4098
|
+
const method = c.req.method;
|
|
4099
|
+
const path = c.req.path;
|
|
4100
|
+
await next();
|
|
4101
|
+
const duration = Date.now() - start;
|
|
4102
|
+
const status = c.res.status;
|
|
4103
|
+
if (config2.json) {
|
|
4104
|
+
const entry = {
|
|
4105
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4106
|
+
requestId,
|
|
4107
|
+
method,
|
|
4108
|
+
path,
|
|
4109
|
+
status,
|
|
4110
|
+
duration,
|
|
4111
|
+
userAgent: c.req.header("User-Agent")
|
|
4112
|
+
};
|
|
4113
|
+
console.log(JSON.stringify(entry));
|
|
4114
|
+
} else {
|
|
4115
|
+
const statusColor = getStatusColor(status);
|
|
4116
|
+
console.log(`${statusColor}${status}\x1B[0m ${method} ${path} - ${duration}ms`);
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
function generateRequestId() {
|
|
4121
|
+
return Math.random().toString(36).substring(2, 10);
|
|
4122
|
+
}
|
|
4123
|
+
function getStatusColor(status) {
|
|
4124
|
+
if (status >= 500) {
|
|
4125
|
+
return "\x1B[31m";
|
|
4126
|
+
}
|
|
4127
|
+
if (status >= 400) {
|
|
4128
|
+
return "\x1B[33m";
|
|
4129
|
+
}
|
|
4130
|
+
if (status >= 300) {
|
|
4131
|
+
return "\x1B[36m";
|
|
4132
|
+
}
|
|
4133
|
+
return "\x1B[32m";
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
// src/server/routes/health.ts
|
|
4137
|
+
init_registry();
|
|
4138
|
+
var healthRouter = new Hono();
|
|
4139
|
+
var startTime = Date.now();
|
|
4140
|
+
healthRouter.get("/", async (c) => {
|
|
4141
|
+
const providers2 = getAvailableProviders();
|
|
4142
|
+
const providerStatus = providers2.map((name) => {
|
|
4143
|
+
const config2 = getProviderConfigFromEnv(name);
|
|
4144
|
+
let available = false;
|
|
4145
|
+
if (name === "ollama") {
|
|
4146
|
+
available = true;
|
|
4147
|
+
} else {
|
|
4148
|
+
available = !!config2.apiKey;
|
|
4149
|
+
}
|
|
4150
|
+
return { name, available };
|
|
4151
|
+
});
|
|
4152
|
+
const anyProviderAvailable = providerStatus.some((p) => p.available);
|
|
4153
|
+
const response = {
|
|
4154
|
+
status: anyProviderAvailable ? "healthy" : "degraded",
|
|
4155
|
+
version: process.env["npm_package_version"] ?? "0.1.0",
|
|
4156
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
4157
|
+
providers: providerStatus
|
|
4158
|
+
};
|
|
4159
|
+
const status = anyProviderAvailable ? 200 : 503;
|
|
4160
|
+
return c.json(response, status);
|
|
4161
|
+
});
|
|
4162
|
+
healthRouter.get("/live", (c) => {
|
|
4163
|
+
return c.json({ status: "ok" });
|
|
4164
|
+
});
|
|
4165
|
+
healthRouter.get("/ready", async (c) => {
|
|
4166
|
+
const providers2 = getAvailableProviders();
|
|
4167
|
+
const hasConfiguredProvider = providers2.some((name) => {
|
|
4168
|
+
if (name === "ollama") return true;
|
|
4169
|
+
const config2 = getProviderConfigFromEnv(name);
|
|
4170
|
+
return !!config2.apiKey;
|
|
4171
|
+
});
|
|
4172
|
+
if (hasConfiguredProvider) {
|
|
4173
|
+
return c.json({ status: "ready" });
|
|
4174
|
+
}
|
|
4175
|
+
return c.json({ status: "not_ready", reason: "No providers configured" }, 503);
|
|
4176
|
+
});
|
|
4177
|
+
var TranslationModeSchema = z.enum(["fast", "balanced", "quality"]);
|
|
4178
|
+
var InlineGlossaryTermSchema = z.object({
|
|
4179
|
+
source: z.string().min(1, "Source term is required"),
|
|
4180
|
+
target: z.string().min(1, "Target term is required"),
|
|
4181
|
+
context: z.string().optional(),
|
|
4182
|
+
caseSensitive: z.boolean().optional(),
|
|
4183
|
+
doNotTranslate: z.boolean().optional()
|
|
4184
|
+
});
|
|
4185
|
+
var TranslateRequestSchema = z.object({
|
|
4186
|
+
content: z.string().min(1, "Content is required"),
|
|
4187
|
+
sourceLang: z.string().min(2, "Source language code must be at least 2 characters").max(10, "Source language code must be at most 10 characters"),
|
|
4188
|
+
targetLang: z.string().min(2, "Target language code must be at least 2 characters").max(10, "Target language code must be at most 10 characters"),
|
|
4189
|
+
format: z.enum(["markdown", "html", "text"]).optional().default("text"),
|
|
4190
|
+
glossary: z.array(InlineGlossaryTermSchema).optional(),
|
|
4191
|
+
provider: z.enum(["claude", "openai", "ollama"]).optional(),
|
|
4192
|
+
model: z.string().optional(),
|
|
4193
|
+
mode: TranslationModeSchema.optional().default("balanced"),
|
|
4194
|
+
qualityThreshold: z.number().min(0).max(100).optional(),
|
|
4195
|
+
maxIterations: z.number().min(1).max(10).optional(),
|
|
4196
|
+
context: z.string().optional()
|
|
4197
|
+
});
|
|
4198
|
+
var MODE_PRESETS2 = {
|
|
4199
|
+
fast: { qualityThreshold: 0, maxIterations: 1 },
|
|
4200
|
+
balanced: { qualityThreshold: 75, maxIterations: 2 },
|
|
4201
|
+
quality: { qualityThreshold: 85, maxIterations: 4 }
|
|
4202
|
+
};
|
|
4203
|
+
|
|
4204
|
+
// src/server/routes/translate.ts
|
|
4205
|
+
init_engine();
|
|
4206
|
+
init_config();
|
|
4207
|
+
init_errors();
|
|
4208
|
+
var translateRouter = new Hono();
|
|
4209
|
+
translateRouter.post(
|
|
4210
|
+
"/",
|
|
4211
|
+
zValidator("json", TranslateRequestSchema, (result, c) => {
|
|
4212
|
+
if (!result.success) {
|
|
4213
|
+
const errors = result.error.errors.map((e) => ({
|
|
4214
|
+
field: e.path.join("."),
|
|
4215
|
+
message: e.message
|
|
4216
|
+
}));
|
|
4217
|
+
return c.json(
|
|
4218
|
+
{
|
|
4219
|
+
error: "Validation failed",
|
|
4220
|
+
code: "VALIDATION_ERROR",
|
|
4221
|
+
details: { errors }
|
|
4222
|
+
},
|
|
4223
|
+
400
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
return void 0;
|
|
4227
|
+
}),
|
|
4228
|
+
async (c) => {
|
|
4229
|
+
const body = c.req.valid("json");
|
|
4230
|
+
const requestId = c.get("requestId") ?? "unknown";
|
|
4231
|
+
const startTime2 = Date.now();
|
|
4232
|
+
try {
|
|
4233
|
+
const baseConfig = await loadConfig();
|
|
4234
|
+
const modeConfig = MODE_PRESETS2[body.mode ?? "balanced"];
|
|
4235
|
+
const config2 = {
|
|
4236
|
+
...baseConfig,
|
|
4237
|
+
languages: {
|
|
4238
|
+
...baseConfig.languages,
|
|
4239
|
+
source: body.sourceLang,
|
|
4240
|
+
targets: [body.targetLang]
|
|
4241
|
+
},
|
|
4242
|
+
provider: {
|
|
4243
|
+
...baseConfig.provider,
|
|
4244
|
+
default: body.provider ?? baseConfig.provider.default,
|
|
4245
|
+
model: body.model ?? baseConfig.provider.model
|
|
4246
|
+
},
|
|
4247
|
+
quality: {
|
|
4248
|
+
...baseConfig.quality,
|
|
4249
|
+
threshold: body.qualityThreshold ?? modeConfig.qualityThreshold,
|
|
4250
|
+
maxIterations: body.maxIterations ?? modeConfig.maxIterations
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
const engine = createTranslationEngine({
|
|
4254
|
+
config: config2,
|
|
4255
|
+
verbose: false,
|
|
4256
|
+
noCache: true
|
|
4257
|
+
});
|
|
4258
|
+
if (body.glossary && body.glossary.length > 0) {
|
|
4259
|
+
convertInlineGlossary(body.glossary, body.sourceLang, body.targetLang);
|
|
4260
|
+
}
|
|
4261
|
+
const result = await engine.translateContent({
|
|
4262
|
+
content: body.content,
|
|
4263
|
+
sourceLang: body.sourceLang,
|
|
4264
|
+
targetLang: body.targetLang,
|
|
4265
|
+
format: body.format,
|
|
4266
|
+
qualityThreshold: config2.quality.threshold,
|
|
4267
|
+
maxIterations: config2.quality.maxIterations,
|
|
4268
|
+
context: body.context
|
|
4269
|
+
});
|
|
4270
|
+
const duration = Date.now() - startTime2;
|
|
4271
|
+
const response = {
|
|
4272
|
+
translated: result.content,
|
|
4273
|
+
quality: result.metadata.averageQuality,
|
|
4274
|
+
iterations: result.metadata.totalIterations,
|
|
4275
|
+
tokensUsed: {
|
|
4276
|
+
input: result.metadata.tokensUsed.input,
|
|
4277
|
+
output: result.metadata.tokensUsed.output
|
|
4278
|
+
},
|
|
4279
|
+
glossaryCompliance: result.glossaryCompliance ? {
|
|
4280
|
+
applied: result.glossaryCompliance.applied,
|
|
4281
|
+
missed: result.glossaryCompliance.missed
|
|
4282
|
+
} : void 0,
|
|
4283
|
+
duration,
|
|
4284
|
+
provider: result.metadata.provider,
|
|
4285
|
+
model: result.metadata.model
|
|
4286
|
+
};
|
|
4287
|
+
return c.json(response, 200);
|
|
4288
|
+
} catch (error) {
|
|
4289
|
+
return handleTranslationError(c, error, requestId);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
);
|
|
4293
|
+
function convertInlineGlossary(terms, sourceLang, targetLang) {
|
|
4294
|
+
return {
|
|
4295
|
+
metadata: {
|
|
4296
|
+
name: "inline",
|
|
4297
|
+
sourceLang,
|
|
4298
|
+
targetLang,
|
|
4299
|
+
version: "1.0"
|
|
4300
|
+
},
|
|
4301
|
+
terms: terms.map(
|
|
4302
|
+
(term) => ({
|
|
4303
|
+
source: term.source,
|
|
4304
|
+
target: term.doNotTranslate ? term.source : term.target,
|
|
4305
|
+
context: term.context,
|
|
4306
|
+
caseSensitive: term.caseSensitive ?? false,
|
|
4307
|
+
doNotTranslate: term.doNotTranslate ?? false
|
|
4308
|
+
})
|
|
4309
|
+
)
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
function handleTranslationError(c, error, requestId) {
|
|
4313
|
+
if (error instanceof TranslationError) {
|
|
4314
|
+
const statusMap = {
|
|
4315
|
+
["PROVIDER_AUTH_FAILED" /* PROVIDER_AUTH_FAILED */]: 401,
|
|
4316
|
+
["PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */]: 429,
|
|
4317
|
+
["PROVIDER_ERROR" /* PROVIDER_ERROR */]: 502,
|
|
4318
|
+
["PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */]: 400,
|
|
4319
|
+
["QUALITY_THRESHOLD_NOT_MET" /* QUALITY_THRESHOLD_NOT_MET */]: 422,
|
|
4320
|
+
["GLOSSARY_INVALID" /* GLOSSARY_INVALID */]: 400,
|
|
4321
|
+
["GLOSSARY_NOT_FOUND" /* GLOSSARY_NOT_FOUND */]: 400,
|
|
4322
|
+
["CONFIG_INVALID" /* CONFIG_INVALID */]: 400,
|
|
4323
|
+
["UNSUPPORTED_FORMAT" /* UNSUPPORTED_FORMAT */]: 400
|
|
4324
|
+
};
|
|
4325
|
+
const status = statusMap[error.code] ?? 500;
|
|
4326
|
+
return c.json(
|
|
4327
|
+
{
|
|
4328
|
+
error: error.message,
|
|
4329
|
+
code: error.code,
|
|
4330
|
+
details: error.details
|
|
4331
|
+
},
|
|
4332
|
+
status
|
|
4333
|
+
);
|
|
4334
|
+
}
|
|
4335
|
+
console.error(`[${requestId}] Translation error:`, error);
|
|
4336
|
+
return c.json(
|
|
4337
|
+
{
|
|
4338
|
+
error: "Internal server error",
|
|
4339
|
+
code: "INTERNAL_ERROR"
|
|
4340
|
+
},
|
|
4341
|
+
500
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
// src/server/index.ts
|
|
4346
|
+
function createApp(options) {
|
|
4347
|
+
const app = new Hono();
|
|
4348
|
+
app.use("*", createLoggerMiddleware({
|
|
4349
|
+
json: options.jsonLogging ?? false
|
|
4350
|
+
}));
|
|
4351
|
+
if (options.enableCors) {
|
|
4352
|
+
app.use("*", cors({
|
|
4353
|
+
origin: "*",
|
|
4354
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
4355
|
+
allowHeaders: ["Content-Type", "Authorization", "X-API-Key"],
|
|
4356
|
+
exposeHeaders: ["X-Request-Id"],
|
|
4357
|
+
maxAge: 86400
|
|
4358
|
+
}));
|
|
4359
|
+
}
|
|
4360
|
+
app.route("/health", healthRouter);
|
|
4361
|
+
app.use("/translate/*", createAuthMiddleware({
|
|
4362
|
+
enabled: options.enableAuth,
|
|
4363
|
+
apiKey: options.apiKey
|
|
4364
|
+
}));
|
|
4365
|
+
app.use("/translate", createAuthMiddleware({
|
|
4366
|
+
enabled: options.enableAuth,
|
|
4367
|
+
apiKey: options.apiKey
|
|
4368
|
+
}));
|
|
4369
|
+
app.route("/translate", translateRouter);
|
|
4370
|
+
app.onError((error, c) => {
|
|
4371
|
+
if (error instanceof HTTPException) {
|
|
4372
|
+
return c.json(
|
|
4373
|
+
{
|
|
4374
|
+
error: error.message,
|
|
4375
|
+
code: "HTTP_ERROR"
|
|
4376
|
+
},
|
|
4377
|
+
error.status
|
|
4378
|
+
);
|
|
4379
|
+
}
|
|
4380
|
+
console.error("Unhandled error:", error);
|
|
4381
|
+
return c.json(
|
|
4382
|
+
{
|
|
4383
|
+
error: "Internal server error",
|
|
4384
|
+
code: "INTERNAL_ERROR"
|
|
4385
|
+
},
|
|
4386
|
+
500
|
|
4387
|
+
);
|
|
4388
|
+
});
|
|
4389
|
+
app.notFound((c) => {
|
|
4390
|
+
return c.json(
|
|
4391
|
+
{
|
|
4392
|
+
error: "Not found",
|
|
4393
|
+
code: "NOT_FOUND"
|
|
4394
|
+
},
|
|
4395
|
+
404
|
|
4396
|
+
);
|
|
4397
|
+
});
|
|
4398
|
+
return app;
|
|
4399
|
+
}
|
|
4400
|
+
function startServer(options) {
|
|
4401
|
+
const app = createApp(options);
|
|
4402
|
+
const server = serve({
|
|
4403
|
+
fetch: app.fetch,
|
|
4404
|
+
port: options.port,
|
|
4405
|
+
hostname: options.host
|
|
4406
|
+
});
|
|
4407
|
+
console.log(`
|
|
4408
|
+
llm-translate server started`);
|
|
4409
|
+
console.log(` - Address: http://${options.host}:${options.port}`);
|
|
4410
|
+
console.log(` - Health: http://${options.host}:${options.port}/health`);
|
|
4411
|
+
console.log(` - Translate: http://${options.host}:${options.port}/translate`);
|
|
4412
|
+
console.log(` - Auth: ${options.enableAuth ? "enabled" : "disabled"}`);
|
|
4413
|
+
console.log(` - CORS: ${options.enableCors ? "enabled" : "disabled"}`);
|
|
4414
|
+
console.log("");
|
|
4415
|
+
const shutdown = (signal) => {
|
|
4416
|
+
console.log(`
|
|
4417
|
+
Received ${signal}, shutting down gracefully...`);
|
|
4418
|
+
server.close((err) => {
|
|
4419
|
+
if (err) {
|
|
4420
|
+
console.error("Error during shutdown:", err);
|
|
4421
|
+
process.exit(1);
|
|
4422
|
+
}
|
|
4423
|
+
console.log("Server closed");
|
|
4424
|
+
process.exit(0);
|
|
4425
|
+
});
|
|
4426
|
+
setTimeout(() => {
|
|
4427
|
+
console.error("Forced shutdown after timeout");
|
|
4428
|
+
process.exit(1);
|
|
4429
|
+
}, 1e4);
|
|
4430
|
+
};
|
|
4431
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
4432
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
4433
|
+
return server;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
// src/cli/commands/serve.ts
|
|
4437
|
+
var serveCommand = new Command("serve").description("Start the translation API server").option(
|
|
4438
|
+
"-p, --port <number>",
|
|
4439
|
+
"Server port (env: TRANSLATE_PORT)",
|
|
4440
|
+
process.env["TRANSLATE_PORT"] ?? "3000"
|
|
4441
|
+
).option("-H, --host <string>", "Host to bind", "0.0.0.0").option("--no-auth", "Disable API key authentication").option("--cors", "Enable CORS for browser clients").option("--json", "Use JSON logging format (for containers)").action((options) => {
|
|
4442
|
+
const port = parseInt(options.port ?? "3000", 10);
|
|
4443
|
+
const host = options.host ?? "0.0.0.0";
|
|
4444
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
4445
|
+
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
4446
|
+
process.exit(2);
|
|
4447
|
+
}
|
|
4448
|
+
const enableAuth = options.auth !== false;
|
|
4449
|
+
if (enableAuth && !process.env["TRANSLATE_API_KEY"]) {
|
|
4450
|
+
console.warn(
|
|
4451
|
+
"Warning: TRANSLATE_API_KEY not set. API key authentication is disabled."
|
|
4452
|
+
);
|
|
4453
|
+
console.warn(
|
|
4454
|
+
"Set TRANSLATE_API_KEY environment variable to enable authentication.\n"
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
startServer({
|
|
4458
|
+
port,
|
|
4459
|
+
host,
|
|
4460
|
+
enableAuth,
|
|
4461
|
+
enableCors: options.cors ?? false,
|
|
4462
|
+
apiKey: process.env["TRANSLATE_API_KEY"],
|
|
4463
|
+
jsonLogging: options.json ?? false
|
|
4464
|
+
});
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
// src/cli/index.ts
|
|
4468
|
+
var program = new Command();
|
|
4469
|
+
program.name("llm-translate").description(
|
|
4470
|
+
"CLI-based document translation tool powered by LLMs with glossary enforcement"
|
|
4471
|
+
).version("0.1.0").enablePositionalOptions().passThroughOptions();
|
|
4472
|
+
program.option("-s, --source-lang <lang>", "Source language code").option("-t, --target-lang <lang>", "Target language code").option(
|
|
4473
|
+
"-c, --config <path>",
|
|
4474
|
+
"Path to config file (default: .translaterc.json)"
|
|
4475
|
+
).option("-v, --verbose", "Enable verbose logging").option("-q, --quiet", "Suppress non-error output");
|
|
4476
|
+
program.addCommand(fileCommand);
|
|
4477
|
+
program.addCommand(dirCommand);
|
|
4478
|
+
program.addCommand(initCommand);
|
|
4479
|
+
program.addCommand(glossaryCommand);
|
|
4480
|
+
program.addCommand(serveCommand);
|
|
4481
|
+
program.action(async (options) => {
|
|
4482
|
+
if (!process.stdin.isTTY) {
|
|
4483
|
+
const { handleStdinTranslation: handleStdinTranslation2 } = await Promise.resolve().then(() => (init_file(), file_exports));
|
|
4484
|
+
await handleStdinTranslation2(options);
|
|
4485
|
+
} else {
|
|
4486
|
+
program.help();
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
4490
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
4491
|
+
process.exit(1);
|
|
4492
|
+
});
|
|
4493
|
+
//# sourceMappingURL=index.js.map
|
|
4494
|
+
//# sourceMappingURL=index.js.map
|