@llm-translate/cli 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.dockerignore +51 -0
  2. package/.env.example +33 -0
  3. package/.github/workflows/docs-pages.yml +57 -0
  4. package/.github/workflows/release.yml +49 -0
  5. package/.translaterc.json +44 -0
  6. package/CLAUDE.md +243 -0
  7. package/Dockerfile +55 -0
  8. package/README.md +371 -0
  9. package/RFC.md +1595 -0
  10. package/dist/cli/index.d.ts +2 -0
  11. package/dist/cli/index.js +4494 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.d.ts +1152 -0
  14. package/dist/index.js +3841 -0
  15. package/dist/index.js.map +1 -0
  16. package/docker-compose.yml +56 -0
  17. package/docs/.vitepress/config.ts +161 -0
  18. package/docs/api/agent.md +262 -0
  19. package/docs/api/engine.md +274 -0
  20. package/docs/api/index.md +171 -0
  21. package/docs/api/providers.md +304 -0
  22. package/docs/changelog.md +64 -0
  23. package/docs/cli/dir.md +243 -0
  24. package/docs/cli/file.md +213 -0
  25. package/docs/cli/glossary.md +273 -0
  26. package/docs/cli/index.md +129 -0
  27. package/docs/cli/init.md +158 -0
  28. package/docs/cli/serve.md +211 -0
  29. package/docs/glossary.json +235 -0
  30. package/docs/guide/chunking.md +272 -0
  31. package/docs/guide/configuration.md +139 -0
  32. package/docs/guide/cost-optimization.md +237 -0
  33. package/docs/guide/docker.md +371 -0
  34. package/docs/guide/getting-started.md +150 -0
  35. package/docs/guide/glossary.md +241 -0
  36. package/docs/guide/index.md +86 -0
  37. package/docs/guide/ollama.md +515 -0
  38. package/docs/guide/prompt-caching.md +221 -0
  39. package/docs/guide/providers.md +232 -0
  40. package/docs/guide/quality-control.md +206 -0
  41. package/docs/guide/vitepress-integration.md +265 -0
  42. package/docs/index.md +63 -0
  43. package/docs/ja/api/agent.md +262 -0
  44. package/docs/ja/api/engine.md +274 -0
  45. package/docs/ja/api/index.md +171 -0
  46. package/docs/ja/api/providers.md +304 -0
  47. package/docs/ja/changelog.md +64 -0
  48. package/docs/ja/cli/dir.md +243 -0
  49. package/docs/ja/cli/file.md +213 -0
  50. package/docs/ja/cli/glossary.md +273 -0
  51. package/docs/ja/cli/index.md +111 -0
  52. package/docs/ja/cli/init.md +158 -0
  53. package/docs/ja/guide/chunking.md +271 -0
  54. package/docs/ja/guide/configuration.md +139 -0
  55. package/docs/ja/guide/cost-optimization.md +30 -0
  56. package/docs/ja/guide/getting-started.md +150 -0
  57. package/docs/ja/guide/glossary.md +214 -0
  58. package/docs/ja/guide/index.md +32 -0
  59. package/docs/ja/guide/ollama.md +410 -0
  60. package/docs/ja/guide/prompt-caching.md +221 -0
  61. package/docs/ja/guide/providers.md +232 -0
  62. package/docs/ja/guide/quality-control.md +137 -0
  63. package/docs/ja/guide/vitepress-integration.md +265 -0
  64. package/docs/ja/index.md +58 -0
  65. package/docs/ko/api/agent.md +262 -0
  66. package/docs/ko/api/engine.md +274 -0
  67. package/docs/ko/api/index.md +171 -0
  68. package/docs/ko/api/providers.md +304 -0
  69. package/docs/ko/changelog.md +64 -0
  70. package/docs/ko/cli/dir.md +243 -0
  71. package/docs/ko/cli/file.md +213 -0
  72. package/docs/ko/cli/glossary.md +273 -0
  73. package/docs/ko/cli/index.md +111 -0
  74. package/docs/ko/cli/init.md +158 -0
  75. package/docs/ko/guide/chunking.md +271 -0
  76. package/docs/ko/guide/configuration.md +139 -0
  77. package/docs/ko/guide/cost-optimization.md +30 -0
  78. package/docs/ko/guide/getting-started.md +150 -0
  79. package/docs/ko/guide/glossary.md +214 -0
  80. package/docs/ko/guide/index.md +32 -0
  81. package/docs/ko/guide/ollama.md +410 -0
  82. package/docs/ko/guide/prompt-caching.md +221 -0
  83. package/docs/ko/guide/providers.md +232 -0
  84. package/docs/ko/guide/quality-control.md +137 -0
  85. package/docs/ko/guide/vitepress-integration.md +265 -0
  86. package/docs/ko/index.md +58 -0
  87. package/docs/zh/api/agent.md +262 -0
  88. package/docs/zh/api/engine.md +274 -0
  89. package/docs/zh/api/index.md +171 -0
  90. package/docs/zh/api/providers.md +304 -0
  91. package/docs/zh/changelog.md +64 -0
  92. package/docs/zh/cli/dir.md +243 -0
  93. package/docs/zh/cli/file.md +213 -0
  94. package/docs/zh/cli/glossary.md +273 -0
  95. package/docs/zh/cli/index.md +111 -0
  96. package/docs/zh/cli/init.md +158 -0
  97. package/docs/zh/guide/chunking.md +271 -0
  98. package/docs/zh/guide/configuration.md +139 -0
  99. package/docs/zh/guide/cost-optimization.md +30 -0
  100. package/docs/zh/guide/getting-started.md +150 -0
  101. package/docs/zh/guide/glossary.md +214 -0
  102. package/docs/zh/guide/index.md +32 -0
  103. package/docs/zh/guide/ollama.md +410 -0
  104. package/docs/zh/guide/prompt-caching.md +221 -0
  105. package/docs/zh/guide/providers.md +232 -0
  106. package/docs/zh/guide/quality-control.md +137 -0
  107. package/docs/zh/guide/vitepress-integration.md +265 -0
  108. package/docs/zh/index.md +58 -0
  109. package/package.json +91 -0
  110. package/release.config.mjs +15 -0
  111. package/schemas/glossary.schema.json +110 -0
  112. package/src/cli/commands/dir.ts +469 -0
  113. package/src/cli/commands/file.ts +291 -0
  114. package/src/cli/commands/glossary.ts +221 -0
  115. package/src/cli/commands/init.ts +68 -0
  116. package/src/cli/commands/serve.ts +60 -0
  117. package/src/cli/index.ts +64 -0
  118. package/src/cli/options.ts +59 -0
  119. package/src/core/agent.ts +1119 -0
  120. package/src/core/chunker.ts +391 -0
  121. package/src/core/engine.ts +634 -0
  122. package/src/errors.ts +188 -0
  123. package/src/index.ts +147 -0
  124. package/src/integrations/vitepress.ts +549 -0
  125. package/src/parsers/markdown.ts +383 -0
  126. package/src/providers/claude.ts +259 -0
  127. package/src/providers/interface.ts +109 -0
  128. package/src/providers/ollama.ts +379 -0
  129. package/src/providers/openai.ts +308 -0
  130. package/src/providers/registry.ts +153 -0
  131. package/src/server/index.ts +152 -0
  132. package/src/server/middleware/auth.ts +93 -0
  133. package/src/server/middleware/logger.ts +90 -0
  134. package/src/server/routes/health.ts +84 -0
  135. package/src/server/routes/translate.ts +210 -0
  136. package/src/server/types.ts +138 -0
  137. package/src/services/cache.ts +899 -0
  138. package/src/services/config.ts +217 -0
  139. package/src/services/glossary.ts +247 -0
  140. package/src/types/analysis.ts +164 -0
  141. package/src/types/index.ts +265 -0
  142. package/src/types/modes.ts +121 -0
  143. package/src/types/mqm.ts +157 -0
  144. package/src/utils/logger.ts +141 -0
  145. package/src/utils/tokens.ts +116 -0
  146. package/tests/fixtures/glossaries/ml-glossary.json +53 -0
  147. package/tests/fixtures/input/lynq-installation.ko.md +350 -0
  148. package/tests/fixtures/input/lynq-installation.md +350 -0
  149. package/tests/fixtures/input/simple.ko.md +27 -0
  150. package/tests/fixtures/input/simple.md +27 -0
  151. package/tests/unit/chunker.test.ts +229 -0
  152. package/tests/unit/glossary.test.ts +146 -0
  153. package/tests/unit/markdown.test.ts +205 -0
  154. package/tests/unit/tokens.test.ts +81 -0
  155. package/tsconfig.json +28 -0
  156. package/tsup.config.ts +34 -0
  157. package/vitest.config.ts +16 -0
@@ -0,0 +1,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