@llm-translate/cli 1.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createMiddleware } from 'hono/factory';
|
|
2
|
+
import type { Context, Next } from 'hono';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Types
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export interface LoggerConfig {
|
|
9
|
+
json: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface LogEntry {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
requestId: string;
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
status: number;
|
|
18
|
+
duration: number;
|
|
19
|
+
userAgent?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Logger Middleware
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Request logging middleware with structured JSON output for containers
|
|
28
|
+
*/
|
|
29
|
+
export function createLoggerMiddleware(config: LoggerConfig) {
|
|
30
|
+
return createMiddleware(async (c: Context, next: Next) => {
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
const requestId = generateRequestId();
|
|
33
|
+
|
|
34
|
+
// Store request ID for correlation in other middleware/handlers
|
|
35
|
+
c.set('requestId', requestId);
|
|
36
|
+
|
|
37
|
+
const method = c.req.method;
|
|
38
|
+
const path = c.req.path;
|
|
39
|
+
|
|
40
|
+
await next();
|
|
41
|
+
|
|
42
|
+
const duration = Date.now() - start;
|
|
43
|
+
const status = c.res.status;
|
|
44
|
+
|
|
45
|
+
if (config.json) {
|
|
46
|
+
// Structured JSON logging for container environments
|
|
47
|
+
const entry: LogEntry = {
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
requestId,
|
|
50
|
+
method,
|
|
51
|
+
path,
|
|
52
|
+
status,
|
|
53
|
+
duration,
|
|
54
|
+
userAgent: c.req.header('User-Agent'),
|
|
55
|
+
};
|
|
56
|
+
console.log(JSON.stringify(entry));
|
|
57
|
+
} else {
|
|
58
|
+
// Human-readable logging for development
|
|
59
|
+
const statusColor = getStatusColor(status);
|
|
60
|
+
console.log(`${statusColor}${status}\x1b[0m ${method} ${path} - ${duration}ms`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Helper Functions
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a short unique request ID
|
|
71
|
+
*/
|
|
72
|
+
function generateRequestId(): string {
|
|
73
|
+
return Math.random().toString(36).substring(2, 10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get ANSI color code based on HTTP status
|
|
78
|
+
*/
|
|
79
|
+
function getStatusColor(status: number): string {
|
|
80
|
+
if (status >= 500) {
|
|
81
|
+
return '\x1b[31m'; // Red for server errors
|
|
82
|
+
}
|
|
83
|
+
if (status >= 400) {
|
|
84
|
+
return '\x1b[33m'; // Yellow for client errors
|
|
85
|
+
}
|
|
86
|
+
if (status >= 300) {
|
|
87
|
+
return '\x1b[36m'; // Cyan for redirects
|
|
88
|
+
}
|
|
89
|
+
return '\x1b[32m'; // Green for success
|
|
90
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { HealthResponse } from '../types.js';
|
|
3
|
+
import {
|
|
4
|
+
getAvailableProviders,
|
|
5
|
+
getProviderConfigFromEnv,
|
|
6
|
+
} from '../../providers/registry.js';
|
|
7
|
+
import type { ProviderName } from '../../types/index.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Health Router
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const healthRouter = new Hono();
|
|
14
|
+
|
|
15
|
+
// Track server start time for uptime calculation
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* GET /health - Comprehensive health check endpoint
|
|
20
|
+
* Suitable for k8s liveness/readiness probes
|
|
21
|
+
*/
|
|
22
|
+
healthRouter.get('/', async (c) => {
|
|
23
|
+
const providers = getAvailableProviders();
|
|
24
|
+
|
|
25
|
+
const providerStatus = providers.map((name: ProviderName) => {
|
|
26
|
+
const config = getProviderConfigFromEnv(name);
|
|
27
|
+
|
|
28
|
+
// Check if provider has required configuration
|
|
29
|
+
let available = false;
|
|
30
|
+
if (name === 'ollama') {
|
|
31
|
+
// Ollama doesn't require API key, just assumes server is running
|
|
32
|
+
available = true;
|
|
33
|
+
} else {
|
|
34
|
+
available = !!config.apiKey;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { name, available };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const anyProviderAvailable = providerStatus.some((p) => p.available);
|
|
41
|
+
|
|
42
|
+
const response: HealthResponse = {
|
|
43
|
+
status: anyProviderAvailable ? 'healthy' : 'degraded',
|
|
44
|
+
version: process.env['npm_package_version'] ?? '0.1.0',
|
|
45
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
46
|
+
providers: providerStatus,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Return 503 if no providers available (for k8s readiness probe)
|
|
50
|
+
const status = anyProviderAvailable ? 200 : 503;
|
|
51
|
+
|
|
52
|
+
return c.json(response, status);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* GET /health/live - Simple liveness probe
|
|
57
|
+
* Returns 200 as long as the server is running
|
|
58
|
+
*/
|
|
59
|
+
healthRouter.get('/live', (c) => {
|
|
60
|
+
return c.json({ status: 'ok' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* GET /health/ready - Readiness probe
|
|
65
|
+
* Returns 200 if the server is ready to accept requests
|
|
66
|
+
*/
|
|
67
|
+
healthRouter.get('/ready', async (c) => {
|
|
68
|
+
const providers = getAvailableProviders();
|
|
69
|
+
|
|
70
|
+
// Check if at least one provider is configured
|
|
71
|
+
const hasConfiguredProvider = providers.some((name: ProviderName) => {
|
|
72
|
+
if (name === 'ollama') return true;
|
|
73
|
+
const config = getProviderConfigFromEnv(name);
|
|
74
|
+
return !!config.apiKey;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (hasConfiguredProvider) {
|
|
78
|
+
return c.json({ status: 'ready' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return c.json({ status: 'not_ready', reason: 'No providers configured' }, 503);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export { healthRouter };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Hono, type Context } from 'hono';
|
|
2
|
+
import { zValidator } from '@hono/zod-validator';
|
|
3
|
+
import {
|
|
4
|
+
TranslateRequestSchema,
|
|
5
|
+
MODE_PRESETS,
|
|
6
|
+
type TranslateResponse,
|
|
7
|
+
type ErrorResponse,
|
|
8
|
+
type InlineGlossaryTerm,
|
|
9
|
+
type HonoVariables,
|
|
10
|
+
} from '../types.js';
|
|
11
|
+
import { createTranslationEngine } from '../../core/engine.js';
|
|
12
|
+
import { loadConfig } from '../../services/config.js';
|
|
13
|
+
import { TranslationError, ErrorCode } from '../../errors.js';
|
|
14
|
+
import type { ResolvedGlossary, ResolvedGlossaryTerm } from '../../types/index.js';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Translate Router
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const translateRouter = new Hono<{ Variables: HonoVariables }>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* POST /translate - Main translation endpoint
|
|
24
|
+
*/
|
|
25
|
+
translateRouter.post(
|
|
26
|
+
'/',
|
|
27
|
+
zValidator('json', TranslateRequestSchema, (result, c) => {
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
const errors = result.error.errors.map((e) => ({
|
|
30
|
+
field: e.path.join('.'),
|
|
31
|
+
message: e.message,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
return c.json<ErrorResponse>(
|
|
35
|
+
{
|
|
36
|
+
error: 'Validation failed',
|
|
37
|
+
code: 'VALIDATION_ERROR',
|
|
38
|
+
details: { errors },
|
|
39
|
+
},
|
|
40
|
+
400
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
// Validation successful - continue to handler
|
|
44
|
+
return undefined;
|
|
45
|
+
}),
|
|
46
|
+
async (c) => {
|
|
47
|
+
const body = c.req.valid('json');
|
|
48
|
+
const requestId = c.get('requestId') ?? 'unknown';
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Load base config
|
|
53
|
+
const baseConfig = await loadConfig();
|
|
54
|
+
|
|
55
|
+
// Get mode presets
|
|
56
|
+
const modeConfig = MODE_PRESETS[body.mode ?? 'balanced'];
|
|
57
|
+
|
|
58
|
+
// Build config with overrides
|
|
59
|
+
const config = {
|
|
60
|
+
...baseConfig,
|
|
61
|
+
languages: {
|
|
62
|
+
...baseConfig.languages,
|
|
63
|
+
source: body.sourceLang,
|
|
64
|
+
targets: [body.targetLang],
|
|
65
|
+
},
|
|
66
|
+
provider: {
|
|
67
|
+
...baseConfig.provider,
|
|
68
|
+
default: body.provider ?? baseConfig.provider.default,
|
|
69
|
+
model: body.model ?? baseConfig.provider.model,
|
|
70
|
+
},
|
|
71
|
+
quality: {
|
|
72
|
+
...baseConfig.quality,
|
|
73
|
+
threshold: body.qualityThreshold ?? modeConfig.qualityThreshold,
|
|
74
|
+
maxIterations: body.maxIterations ?? modeConfig.maxIterations,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Create engine (API mode doesn't use file cache)
|
|
79
|
+
const engine = createTranslationEngine({
|
|
80
|
+
config,
|
|
81
|
+
verbose: false,
|
|
82
|
+
noCache: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Convert inline glossary to resolved format if provided
|
|
86
|
+
// Note: glossary support via inline terms will be implemented
|
|
87
|
+
// when TranslationEngine supports passing glossary directly
|
|
88
|
+
if (body.glossary && body.glossary.length > 0) {
|
|
89
|
+
// TODO: Pass glossary to engine when supported
|
|
90
|
+
convertInlineGlossary(body.glossary, body.sourceLang, body.targetLang);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Translate content
|
|
94
|
+
const result = await engine.translateContent({
|
|
95
|
+
content: body.content,
|
|
96
|
+
sourceLang: body.sourceLang,
|
|
97
|
+
targetLang: body.targetLang,
|
|
98
|
+
format: body.format,
|
|
99
|
+
qualityThreshold: config.quality.threshold,
|
|
100
|
+
maxIterations: config.quality.maxIterations,
|
|
101
|
+
context: body.context,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const duration = Date.now() - startTime;
|
|
105
|
+
|
|
106
|
+
const response: TranslateResponse = {
|
|
107
|
+
translated: result.content,
|
|
108
|
+
quality: result.metadata.averageQuality,
|
|
109
|
+
iterations: result.metadata.totalIterations,
|
|
110
|
+
tokensUsed: {
|
|
111
|
+
input: result.metadata.tokensUsed.input,
|
|
112
|
+
output: result.metadata.tokensUsed.output,
|
|
113
|
+
},
|
|
114
|
+
glossaryCompliance: result.glossaryCompliance
|
|
115
|
+
? {
|
|
116
|
+
applied: result.glossaryCompliance.applied,
|
|
117
|
+
missed: result.glossaryCompliance.missed,
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
duration,
|
|
121
|
+
provider: result.metadata.provider,
|
|
122
|
+
model: result.metadata.model,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return c.json(response, 200);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return handleTranslationError(c, error, requestId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Helper Functions
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert inline glossary terms to resolved glossary format
|
|
138
|
+
*/
|
|
139
|
+
function convertInlineGlossary(
|
|
140
|
+
terms: InlineGlossaryTerm[],
|
|
141
|
+
sourceLang: string,
|
|
142
|
+
targetLang: string
|
|
143
|
+
): ResolvedGlossary {
|
|
144
|
+
return {
|
|
145
|
+
metadata: {
|
|
146
|
+
name: 'inline',
|
|
147
|
+
sourceLang,
|
|
148
|
+
targetLang,
|
|
149
|
+
version: '1.0',
|
|
150
|
+
},
|
|
151
|
+
terms: terms.map(
|
|
152
|
+
(term): ResolvedGlossaryTerm => ({
|
|
153
|
+
source: term.source,
|
|
154
|
+
target: term.doNotTranslate ? term.source : term.target,
|
|
155
|
+
context: term.context,
|
|
156
|
+
caseSensitive: term.caseSensitive ?? false,
|
|
157
|
+
doNotTranslate: term.doNotTranslate ?? false,
|
|
158
|
+
})
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle translation errors and return appropriate HTTP response
|
|
165
|
+
*/
|
|
166
|
+
type StatusCode = 200 | 400 | 401 | 422 | 429 | 500 | 502;
|
|
167
|
+
|
|
168
|
+
function handleTranslationError(
|
|
169
|
+
c: Context<{ Variables: HonoVariables }>,
|
|
170
|
+
error: unknown,
|
|
171
|
+
requestId: string
|
|
172
|
+
): Response {
|
|
173
|
+
if (error instanceof TranslationError) {
|
|
174
|
+
const statusMap: Record<string, StatusCode> = {
|
|
175
|
+
[ErrorCode.PROVIDER_AUTH_FAILED]: 401,
|
|
176
|
+
[ErrorCode.PROVIDER_RATE_LIMITED]: 429,
|
|
177
|
+
[ErrorCode.PROVIDER_ERROR]: 502,
|
|
178
|
+
[ErrorCode.PROVIDER_NOT_FOUND]: 400,
|
|
179
|
+
[ErrorCode.QUALITY_THRESHOLD_NOT_MET]: 422,
|
|
180
|
+
[ErrorCode.GLOSSARY_INVALID]: 400,
|
|
181
|
+
[ErrorCode.GLOSSARY_NOT_FOUND]: 400,
|
|
182
|
+
[ErrorCode.CONFIG_INVALID]: 400,
|
|
183
|
+
[ErrorCode.UNSUPPORTED_FORMAT]: 400,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const status: StatusCode = statusMap[error.code] ?? 500;
|
|
187
|
+
|
|
188
|
+
return c.json<ErrorResponse>(
|
|
189
|
+
{
|
|
190
|
+
error: error.message,
|
|
191
|
+
code: error.code,
|
|
192
|
+
details: error.details,
|
|
193
|
+
},
|
|
194
|
+
status
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Unknown error
|
|
199
|
+
console.error(`[${requestId}] Translation error:`, error);
|
|
200
|
+
|
|
201
|
+
return c.json<ErrorResponse>(
|
|
202
|
+
{
|
|
203
|
+
error: 'Internal server error',
|
|
204
|
+
code: 'INTERNAL_ERROR',
|
|
205
|
+
},
|
|
206
|
+
500
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { translateRouter };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Request Validation Schemas
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Translation mode enum
|
|
9
|
+
*/
|
|
10
|
+
export const TranslationModeSchema = z.enum(['fast', 'balanced', 'quality']);
|
|
11
|
+
export type TranslationMode = z.infer<typeof TranslationModeSchema>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Inline glossary term schema (simplified for API)
|
|
15
|
+
*/
|
|
16
|
+
export const InlineGlossaryTermSchema = z.object({
|
|
17
|
+
source: z.string().min(1, 'Source term is required'),
|
|
18
|
+
target: z.string().min(1, 'Target term is required'),
|
|
19
|
+
context: z.string().optional(),
|
|
20
|
+
caseSensitive: z.boolean().optional(),
|
|
21
|
+
doNotTranslate: z.boolean().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type InlineGlossaryTerm = z.infer<typeof InlineGlossaryTermSchema>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /translate request body schema
|
|
28
|
+
*/
|
|
29
|
+
export const TranslateRequestSchema = z.object({
|
|
30
|
+
content: z.string().min(1, 'Content is required'),
|
|
31
|
+
sourceLang: z
|
|
32
|
+
.string()
|
|
33
|
+
.min(2, 'Source language code must be at least 2 characters')
|
|
34
|
+
.max(10, 'Source language code must be at most 10 characters'),
|
|
35
|
+
targetLang: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(2, 'Target language code must be at least 2 characters')
|
|
38
|
+
.max(10, 'Target language code must be at most 10 characters'),
|
|
39
|
+
format: z.enum(['markdown', 'html', 'text']).optional().default('text'),
|
|
40
|
+
glossary: z.array(InlineGlossaryTermSchema).optional(),
|
|
41
|
+
provider: z.enum(['claude', 'openai', 'ollama']).optional(),
|
|
42
|
+
model: z.string().optional(),
|
|
43
|
+
mode: TranslationModeSchema.optional().default('balanced'),
|
|
44
|
+
qualityThreshold: z.number().min(0).max(100).optional(),
|
|
45
|
+
maxIterations: z.number().min(1).max(10).optional(),
|
|
46
|
+
context: z.string().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type TranslateRequest = z.infer<typeof TranslateRequestSchema>;
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Response Types
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* POST /translate response
|
|
57
|
+
*/
|
|
58
|
+
export interface TranslateResponse {
|
|
59
|
+
translated: string;
|
|
60
|
+
quality: number;
|
|
61
|
+
iterations: number;
|
|
62
|
+
tokensUsed: {
|
|
63
|
+
input: number;
|
|
64
|
+
output: number;
|
|
65
|
+
};
|
|
66
|
+
glossaryCompliance?: {
|
|
67
|
+
applied: string[];
|
|
68
|
+
missed: string[];
|
|
69
|
+
};
|
|
70
|
+
duration: number;
|
|
71
|
+
provider: string;
|
|
72
|
+
model: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GET /health response
|
|
77
|
+
*/
|
|
78
|
+
export interface HealthResponse {
|
|
79
|
+
status: 'healthy' | 'degraded';
|
|
80
|
+
version: string;
|
|
81
|
+
uptime: number;
|
|
82
|
+
providers: {
|
|
83
|
+
name: string;
|
|
84
|
+
available: boolean;
|
|
85
|
+
}[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Error response format
|
|
90
|
+
*/
|
|
91
|
+
export interface ErrorResponse {
|
|
92
|
+
error: string;
|
|
93
|
+
code: string;
|
|
94
|
+
details?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Server Configuration
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Server configuration options
|
|
103
|
+
*/
|
|
104
|
+
export interface ServerConfig {
|
|
105
|
+
port: number;
|
|
106
|
+
host: string;
|
|
107
|
+
enableAuth: boolean;
|
|
108
|
+
enableCors: boolean;
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
jsonLogging?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Hono Context Variables
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Custom variables stored in Hono context
|
|
119
|
+
*/
|
|
120
|
+
export interface HonoVariables {
|
|
121
|
+
requestId: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Mode Presets
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mode configuration presets
|
|
130
|
+
*/
|
|
131
|
+
export const MODE_PRESETS: Record<
|
|
132
|
+
TranslationMode,
|
|
133
|
+
{ qualityThreshold: number; maxIterations: number }
|
|
134
|
+
> = {
|
|
135
|
+
fast: { qualityThreshold: 0, maxIterations: 1 },
|
|
136
|
+
balanced: { qualityThreshold: 75, maxIterations: 2 },
|
|
137
|
+
quality: { qualityThreshold: 85, maxIterations: 4 },
|
|
138
|
+
};
|