@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,56 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ # ==============================================================================
5
+ # llm-translate API Server
6
+ # ==============================================================================
7
+ llm-translate:
8
+ build: .
9
+ container_name: llm-translate-api
10
+ restart: unless-stopped
11
+ ports:
12
+ - "${TRANSLATE_PORT:-3000}:3000"
13
+ environment:
14
+ - NODE_ENV=production
15
+ - TRANSLATE_API_KEY=${TRANSLATE_API_KEY}
16
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
17
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
18
+ - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://ollama:11434}
19
+ healthcheck:
20
+ test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health/live').then(r => r.ok ? process.exit(0) : process.exit(1))"]
21
+ interval: 30s
22
+ timeout: 5s
23
+ retries: 3
24
+ start_period: 10s
25
+ logging:
26
+ driver: json-file
27
+ options:
28
+ max-size: "10m"
29
+ max-file: "3"
30
+ deploy:
31
+ resources:
32
+ limits:
33
+ memory: 512M
34
+ reservations:
35
+ memory: 256M
36
+
37
+ # ==============================================================================
38
+ # Optional: Local Ollama service for self-hosted LLM
39
+ # Enable with: docker compose --profile with-ollama up
40
+ # ==============================================================================
41
+ ollama:
42
+ image: ollama/ollama:latest
43
+ container_name: ollama
44
+ profiles:
45
+ - with-ollama
46
+ ports:
47
+ - "11434:11434"
48
+ volumes:
49
+ - ollama-models:/root/.ollama
50
+ deploy:
51
+ resources:
52
+ limits:
53
+ memory: 4G
54
+
55
+ volumes:
56
+ ollama-models:
@@ -0,0 +1,161 @@
1
+ import { defineConfig } from 'vitepress';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { generateLocaleConfig } from '../../dist/index.js';
5
+
6
+ // Get docs directory path relative to this config file
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const docsDir = resolve(__dirname, '..');
9
+
10
+ // Generate locale configurations automatically
11
+ const locales = generateLocaleConfig(docsDir, {
12
+ defaultLocale: 'en',
13
+ locales: ['ko', 'ja', 'zh'],
14
+ labels: {
15
+ en: 'English',
16
+ ko: '한국어',
17
+ ja: '日本語',
18
+ zh: '中文',
19
+ },
20
+ descriptions: {
21
+ en: 'LLM-powered document translation CLI with glossary enforcement',
22
+ ko: 'LLM 기반 문서 번역 CLI - 용어집 적용 및 품질 관리',
23
+ ja: 'LLM駆動のドキュメント翻訳CLI - 用語集の適用と品質管理',
24
+ zh: 'LLM驱动的文档翻译CLI - 术语表应用和质量管理',
25
+ },
26
+ sidebarDirs: ['guide', 'cli', 'api'],
27
+ useTitleFromHeading: true,
28
+ translations: {
29
+ ko: {
30
+ editLinkText: 'GitHub에서 이 페이지 편집하기',
31
+ docFooter: { prev: '이전 페이지', next: '다음 페이지' },
32
+ outline: { label: '목차' },
33
+ lastUpdated: { text: '최종 업데이트' },
34
+ returnToTopLabel: '맨 위로',
35
+ sidebarMenuLabel: '메뉴',
36
+ darkModeSwitchLabel: '다크 모드',
37
+ },
38
+ ja: {
39
+ editLinkText: 'GitHubでこのページを編集する',
40
+ docFooter: { prev: '前のページ', next: '次のページ' },
41
+ outline: { label: '目次' },
42
+ lastUpdated: { text: '最終更新' },
43
+ returnToTopLabel: 'トップへ戻る',
44
+ sidebarMenuLabel: 'メニュー',
45
+ darkModeSwitchLabel: 'ダークモード',
46
+ },
47
+ zh: {
48
+ editLinkText: '在 GitHub 上编辑此页',
49
+ docFooter: { prev: '上一页', next: '下一页' },
50
+ outline: { label: '目录' },
51
+ lastUpdated: { text: '最后更新' },
52
+ returnToTopLabel: '返回顶部',
53
+ sidebarMenuLabel: '菜单',
54
+ darkModeSwitchLabel: '深色模式',
55
+ },
56
+ },
57
+ });
58
+
59
+ // Shared configuration
60
+ const shared = defineConfig({
61
+ title: 'llm-translate',
62
+ base: '/llm-translate/',
63
+ head: [['link', { rel: 'icon', href: '/favicon.ico' }]],
64
+
65
+ themeConfig: {
66
+ logo: '/logo.svg',
67
+ socialLinks: [
68
+ { icon: 'github', link: 'https://github.com/selenehyun/llm-translate' },
69
+ ],
70
+ search: {
71
+ provider: 'local',
72
+ options: {
73
+ locales: {
74
+ ko: {
75
+ translations: {
76
+ button: {
77
+ buttonText: '검색',
78
+ buttonAriaLabel: '검색',
79
+ },
80
+ modal: {
81
+ displayDetails: '상세 목록 표시',
82
+ resetButtonTitle: '검색 지우기',
83
+ backButtonTitle: '검색 닫기',
84
+ noResultsText: '결과를 찾을 수 없습니다',
85
+ footer: {
86
+ selectText: '선택',
87
+ selectKeyAriaLabel: '선택하기',
88
+ navigateText: '탐색',
89
+ navigateUpKeyAriaLabel: '위로',
90
+ navigateDownKeyAriaLabel: '아래로',
91
+ closeText: '닫기',
92
+ closeKeyAriaLabel: 'esc',
93
+ },
94
+ },
95
+ },
96
+ },
97
+ ja: {
98
+ translations: {
99
+ button: {
100
+ buttonText: '検索',
101
+ buttonAriaLabel: '検索',
102
+ },
103
+ modal: {
104
+ displayDetails: '詳細リストを表示',
105
+ resetButtonTitle: '検索をクリア',
106
+ backButtonTitle: '検索を閉じる',
107
+ noResultsText: '結果が見つかりませんでした',
108
+ footer: {
109
+ selectText: '選択',
110
+ selectKeyAriaLabel: '選択する',
111
+ navigateText: '移動',
112
+ navigateUpKeyAriaLabel: '上へ',
113
+ navigateDownKeyAriaLabel: '下へ',
114
+ closeText: '閉じる',
115
+ closeKeyAriaLabel: 'esc',
116
+ },
117
+ },
118
+ },
119
+ },
120
+ zh: {
121
+ translations: {
122
+ button: {
123
+ buttonText: '搜索',
124
+ buttonAriaLabel: '搜索',
125
+ },
126
+ modal: {
127
+ displayDetails: '显示详细列表',
128
+ resetButtonTitle: '清除搜索',
129
+ backButtonTitle: '关闭搜索',
130
+ noResultsText: '未找到结果',
131
+ footer: {
132
+ selectText: '选择',
133
+ selectKeyAriaLabel: '选择',
134
+ navigateText: '导航',
135
+ navigateUpKeyAriaLabel: '向上',
136
+ navigateDownKeyAriaLabel: '向下',
137
+ closeText: '关闭',
138
+ closeKeyAriaLabel: 'esc',
139
+ },
140
+ },
141
+ },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ editLink: {
147
+ pattern: 'https://github.com/selenehyun/llm-translate/edit/main/docs/:path',
148
+ text: 'Edit this page on GitHub',
149
+ },
150
+ footer: {
151
+ message: 'Released under the MIT License.',
152
+ copyright: 'Copyright © 2025-present',
153
+ },
154
+ },
155
+ });
156
+
157
+ // Main config with locales
158
+ export default defineConfig({
159
+ ...shared,
160
+ locales,
161
+ });
@@ -0,0 +1,262 @@
1
+ # TranslationAgent
2
+
3
+ ::: info Translations
4
+ All non-English documentation is automatically translated using Claude Sonnet 4.
5
+ :::
6
+
7
+ Low-level translation agent implementing the Self-Refine algorithm.
8
+
9
+ ## Constructor
10
+
11
+ ```typescript
12
+ import { TranslationAgent } from '@llm-translate/cli';
13
+
14
+ const agent = new TranslationAgent(options: TranslationAgentOptions);
15
+ ```
16
+
17
+ ### Options
18
+
19
+ ```typescript
20
+ interface TranslationAgentOptions {
21
+ provider: LLMProvider;
22
+ qualityThreshold?: number; // Default: 85
23
+ maxIterations?: number; // Default: 4
24
+ verbose?: boolean; // Default: false
25
+ strictQuality?: boolean; // Default: false
26
+ enableCaching?: boolean; // Default: true for Claude
27
+ }
28
+ ```
29
+
30
+ ## Methods
31
+
32
+ ### translate
33
+
34
+ Translate content using Self-Refine loop.
35
+
36
+ ```typescript
37
+ const result = await agent.translate(request: TranslationRequest);
38
+ ```
39
+
40
+ #### Request
41
+
42
+ ```typescript
43
+ interface TranslationRequest {
44
+ content: string; // Source text to translate
45
+ sourceLang: string; // Source language code
46
+ targetLang: string; // Target language code
47
+ glossary?: ResolvedGlossary; // Resolved glossary for target language
48
+ context?: {
49
+ documentPurpose?: string;
50
+ previousChunks?: string[];
51
+ documentSummary?: string;
52
+ };
53
+ }
54
+ ```
55
+
56
+ #### Result
57
+
58
+ ```typescript
59
+ interface TranslationResult {
60
+ content: string; // Translated text
61
+ metadata: {
62
+ qualityScore: number;
63
+ qualityThreshold: number;
64
+ thresholdMet: boolean;
65
+ iterations: number;
66
+ tokensUsed: {
67
+ input: number;
68
+ output: number;
69
+ cacheRead?: number;
70
+ cacheWrite?: number;
71
+ };
72
+ duration: number;
73
+ provider: string;
74
+ model: string;
75
+ };
76
+ glossaryCompliance?: {
77
+ applied: string[]; // Terms successfully applied
78
+ missed: string[]; // Terms not found in translation
79
+ };
80
+ }
81
+ ```
82
+
83
+ ## Self-Refine Algorithm
84
+
85
+ The agent implements this iterative refinement process:
86
+
87
+ ```typescript
88
+ // Pseudocode
89
+ async translate(request) {
90
+ // 1. Initial translation with glossary
91
+ let translation = await generateInitialTranslation(request);
92
+ let iterations = 1;
93
+
94
+ while (iterations < maxIterations) {
95
+ // 2. Evaluate quality
96
+ const evaluation = await evaluateQuality(translation);
97
+
98
+ if (evaluation.score >= qualityThreshold) {
99
+ break; // Quality met
100
+ }
101
+
102
+ // 3. Generate improvement suggestions
103
+ const suggestions = await generateReflection(translation);
104
+
105
+ // 4. Apply improvements
106
+ translation = await improveTranslation(translation, suggestions);
107
+ iterations++;
108
+ }
109
+
110
+ return { content: translation, metadata: { ... } };
111
+ }
112
+ ```
113
+
114
+ ## Example Usage
115
+
116
+ ### Basic Translation
117
+
118
+ ```typescript
119
+ import { TranslationAgent, createClaudeProvider } from '@llm-translate/cli';
120
+
121
+ const provider = createClaudeProvider();
122
+ const agent = new TranslationAgent({
123
+ provider,
124
+ qualityThreshold: 85,
125
+ });
126
+
127
+ const result = await agent.translate({
128
+ content: 'Hello, world!',
129
+ sourceLang: 'en',
130
+ targetLang: 'ko',
131
+ });
132
+
133
+ console.log(result.content); // 안녕하세요, 세계!
134
+ console.log(result.metadata.qualityScore); // 92
135
+ ```
136
+
137
+ ### With Glossary
138
+
139
+ ```typescript
140
+ import { loadGlossary, resolveGlossary } from '@llm-translate/cli';
141
+
142
+ const glossary = await loadGlossary('./glossary.json');
143
+ const resolved = resolveGlossary(glossary, 'ko');
144
+
145
+ const result = await agent.translate({
146
+ content: 'The component renders a prop.',
147
+ sourceLang: 'en',
148
+ targetLang: 'ko',
149
+ glossary: resolved,
150
+ });
151
+
152
+ console.log(result.glossaryCompliance);
153
+ // { applied: ['component', 'prop'], missed: [] }
154
+ ```
155
+
156
+ ### With Context
157
+
158
+ ```typescript
159
+ const result = await agent.translate({
160
+ content: 'Click the button to continue.',
161
+ sourceLang: 'en',
162
+ targetLang: 'ko',
163
+ context: {
164
+ documentPurpose: 'User interface instructions',
165
+ previousChunks: [
166
+ '이전 단계에서 설정을 완료했습니다.', // Previous translation
167
+ ],
168
+ },
169
+ });
170
+ ```
171
+
172
+ ### Strict Quality Mode
173
+
174
+ ```typescript
175
+ import { TranslationError, ErrorCode } from '@llm-translate/cli';
176
+
177
+ const agent = new TranslationAgent({
178
+ provider,
179
+ qualityThreshold: 95,
180
+ strictQuality: true, // Throw if threshold not met
181
+ });
182
+
183
+ try {
184
+ const result = await agent.translate(request);
185
+ } catch (error) {
186
+ if (error instanceof TranslationError &&
187
+ error.code === ErrorCode.QUALITY_THRESHOLD_NOT_MET) {
188
+ console.log(`Quality: ${error.details.score}/${error.details.threshold}`);
189
+ console.log(`Issues: ${error.details.issues.join(', ')}`);
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Quality Evaluation
195
+
196
+ The agent evaluates translations on four criteria:
197
+
198
+ | Criterion | Weight | Description |
199
+ |-----------|--------|-------------|
200
+ | Semantic Accuracy | 40% | Meaning preservation |
201
+ | Fluency | 25% | Natural language flow |
202
+ | Glossary Compliance | 20% | Term consistency |
203
+ | Format Preservation | 15% | Structure maintenance |
204
+
205
+ ### Evaluation Result
206
+
207
+ ```typescript
208
+ interface QualityEvaluation {
209
+ score: number; // 0-100
210
+ breakdown: {
211
+ accuracy: number; // 0-40
212
+ fluency: number; // 0-25
213
+ glossary: number; // 0-20
214
+ format: number; // 0-15
215
+ };
216
+ issues: string[]; // Specific problems identified
217
+ }
218
+ ```
219
+
220
+ ## Prompt Caching
221
+
222
+ When `enableCaching` is true (default for Claude), the agent structures prompts for caching:
223
+
224
+ ```
225
+ ┌─────────────────────────────────┐
226
+ │ System Instructions (CACHED) │ ← Reused across chunks
227
+ ├─────────────────────────────────┤
228
+ │ Glossary (CACHED) │ ← Reused across chunks
229
+ ├─────────────────────────────────┤
230
+ │ Source Text (NOT cached) │ ← Changes per chunk
231
+ └─────────────────────────────────┘
232
+ ```
233
+
234
+ This can reduce costs by 40-50% for multi-chunk documents.
235
+
236
+ ## Advanced: Custom Evaluation
237
+
238
+ Override the default evaluation logic:
239
+
240
+ ```typescript
241
+ class CustomAgent extends TranslationAgent {
242
+ protected async evaluateQuality(
243
+ source: string,
244
+ translation: string,
245
+ sourceLang: string,
246
+ targetLang: string
247
+ ): Promise<QualityEvaluation> {
248
+ // Custom evaluation logic
249
+ const baseEval = await super.evaluateQuality(
250
+ source, translation, sourceLang, targetLang
251
+ );
252
+
253
+ // Add custom checks
254
+ if (translation.length < source.length * 0.5) {
255
+ baseEval.score -= 10;
256
+ baseEval.issues.push('Translation suspiciously short');
257
+ }
258
+
259
+ return baseEval;
260
+ }
261
+ }
262
+ ```