UNPKG

i18n-llm-translate

Version:

Automatically translates namespace-based JSON translation files across multiple languages from any source language

375 lines (374 loc) 17.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createDeepSeekTranslateEngine = createDeepSeekTranslateEngine; const util_1 = require("../../util"); const logger_1 = require("../../logger"); const openai_1 = __importStar(require("openai")); const break_silent_error_1 = require("../../break-silent-error"); const openrouter_1 = require("../../engines/estimate/openrouter"); const iso_639_1_1 = __importDefault(require("iso-639-1")); const DEFAULT_MODEL = "v4-flash"; const ABSOLUTE_CONTEXT = [ "You are a professional translator.", "You must output JSON and only JSON.", "Keep all object keys and JSON structure exactly the same.", "Only translate string values.", "Variables enclosed in {} are coded and their names should remain unchanged.", "IMPORTANT: Ensure that each translation is in the correct target language. Double-check that you are translating to the specific language requested for each language code." ]; function createDeepSeekTranslateEngine(config) { if (!config.apiKey) { throw new Error("DeepSeek > Missing apiKey"); } const model = config.model || DEFAULT_MODEL; const apiModel = toDeepSeekApiModel(model); const estimateEngine = (0, openrouter_1.createOpenRouterEstimateEngine)({ provider: "deepseek", model: config.estimateModel || apiModel }); const tokenUsage = { inputTokens: 0, outputTokens: 0 }; const MAX_CHUNK_SIZE = Math.min(100, Math.max(5, config.chunkSize || 50)); const TIMEOUT_MS = (config.timeoutSeconds || 25) * 1000; const MAX_RETRIES = config.maxRetries || 10; const RATE_LIMIT_MULTIPLIER = config.rateLimitRetryMultiplier || 4; const RATE_LIMIT_EXTRA_DELAY = config.rateLimitRetryExtraDelay || 1200; const deepseek = new openai_1.default({ apiKey: config.apiKey, baseURL: "https://api.deepseek.com", timeout: TIMEOUT_MS }); let globalTimeoutPromise = null; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function computeRateLimitWaitMs(error) { const apiErr = rateLimitApiErrorFromChain(error); const retryAfterMsRaw = headerValue(apiErr === null || apiErr === void 0 ? void 0 : apiErr.headers, "retry-after-ms"); if (retryAfterMsRaw) { const n = parseInt(retryAfterMsRaw, 10); if (!Number.isNaN(n)) return n * RATE_LIMIT_MULTIPLIER + RATE_LIMIT_EXTRA_DELAY; } const retryAfterSecRaw = headerValue(apiErr === null || apiErr === void 0 ? void 0 : apiErr.headers, "retry-after"); if (retryAfterSecRaw) { const n = parseInt(retryAfterSecRaw, 10); if (!Number.isNaN(n)) return n * 1000 * RATE_LIMIT_MULTIPLIER + RATE_LIMIT_EXTRA_DELAY; } return 1000 + RATE_LIMIT_EXTRA_DELAY; } function prepareTranslationsAndChunkThem(flatBaseTranslations, languagesTranslations) { const chunks = []; let currentChunkSize = 0; let currentChunkBaseTranslations = {}; let currentChunkShape = {}; function pushChunk() { chunks.push({ baseTranslations: (0, util_1.unflattenObject)(currentChunkBaseTranslations), targetTranslationsShape: (0, util_1.unflattenObject)(currentChunkShape) }); currentChunkSize = 0; currentChunkBaseTranslations = {}; currentChunkShape = {}; } for (let languageCode in languagesTranslations) { const flat = Object.entries((0, util_1.flattenObject)(languagesTranslations[languageCode])); do { currentChunkSize++; // language code property const rest = Math.min(MAX_CHUNK_SIZE - currentChunkSize, flat.length); const chunkPart = flat.splice(0, rest); for (const [key] of chunkPart) { currentChunkBaseTranslations[key] = flatBaseTranslations[key]; currentChunkShape[`${languageCode}.${key}`] = ""; } currentChunkSize += rest; if (currentChunkSize == MAX_CHUNK_SIZE) { pushChunk(); } } while (flat.length); if (currentChunkSize >= MAX_CHUNK_SIZE - 1) { pushChunk(); } } if (currentChunkSize) pushChunk(); return chunks; } function withRetry(operation, options) { return __awaiter(this, void 0, void 0, function* () { const logger = options.logger || logger_1.defaultLogger; let lastError; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { if (globalTimeoutPromise) { const startTime = Date.now(); yield globalTimeoutPromise; const waitTime = Date.now() - startTime; if (waitTime > 0) { logger.engineDebug("DeepSeek", `Waited ${waitTime}ms for global timeout synchronization`); } } return yield operation(); } catch (error) { lastError = error; if (isDeepSeekInsufficientQuotaError(error)) { throw new break_silent_error_1.BreakSilentError("DeepSeek: insufficient quota / billing — not retrying.", error); } const isRateLimit = isTransientDeepSeekRateLimit(error); const isTimeout = !isRateLimit && error instanceof Error && (error instanceof openai_1.APIConnectionTimeoutError || error.message.includes("timeout") || error.message.includes("ETIMEDOUT") || error.name === "TimeoutError" || error.name === "APIConnectionTimeoutError" || error.constructor.name === "APIConnectionTimeoutError"); if ((isTimeout || isRateLimit) && attempt < MAX_RETRIES) { if (isRateLimit) { const waitTime = computeRateLimitWaitMs(error); logger.engineDebug("DeepSeek", `Rate limit hit on attempt ${attempt}/${MAX_RETRIES}, waiting ${waitTime}ms before retry...`); const sleepPromise = sleep(waitTime); globalTimeoutPromise = sleepPromise; yield sleepPromise; globalTimeoutPromise = null; } else { const timeoutWaitTime = 2000; logger.engineDebug("DeepSeek", `Timeout on attempt ${attempt}/${MAX_RETRIES}, waiting ${timeoutWaitTime}ms before retry...`); const sleepPromise = sleep(timeoutWaitTime); globalTimeoutPromise = sleepPromise; yield sleepPromise; globalTimeoutPromise = null; } continue; } throw error; } } throw lastError; }); } function fetchTranslations(chunks, options) { return __awaiter(this, void 0, void 0, function* () { const logger = options.logger || logger_1.defaultLogger; const systemContext = [ ...ABSOLUTE_CONTEXT, toLanguagesContext(options.baseLanguageCode, options.targetLanguageCodes), ...options.applicationContextEntries ].join(" "); function fetchChunk(chunk) { return __awaiter(this, void 0, void 0, function* () { return yield withRetry(() => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; const response = yield deepseek.chat.completions.create({ model: apiModel, response_format: { type: "json_object" }, messages: [ { role: "system", content: systemContext }, { role: "user", content: JSON.stringify({ task: "Translate baseTranslations into targetTranslationsShape. Return only the completed targetTranslationsShape object.", baseTranslations: chunk.baseTranslations, targetTranslationsShape: chunk.targetTranslationsShape }) } ] }); tokenUsage.inputTokens += ((_a = response.usage) === null || _a === void 0 ? void 0 : _a.prompt_tokens) || 0; tokenUsage.outputTokens += ((_b = response.usage) === null || _b === void 0 ? void 0 : _b.completion_tokens) || 0; const content = (_d = (_c = response.choices[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content; if (!content) { throw new Error("Received empty response from DeepSeek API."); } return parseJsonObject(content); }), options); }); } const mergedFlat = {}; let fetchedCount = 0; const translatedChunks = yield Promise.all(chunks.map((chunk, i) => __awaiter(this, void 0, void 0, function* () { if (chunks.length > 1) { logger.engineDebug("DeepSeek", `Fetching chunk ${i + 1}/${chunks.length}`); } const result = yield fetchChunk(chunk); fetchedCount++; logger.engineDebug("DeepSeek", `Fetched chunk ${i + 1} (${fetchedCount}/${chunks.length})`); return result; }))); logger.engineDebug("DeepSeek", "Finished fetching all chunks"); for (const translatedChunk of translatedChunks) { const flatChunk = (0, util_1.flattenObject)(translatedChunk); for (const path in flatChunk) { mergedFlat[path] = flatChunk[path]; } } return (0, util_1.unflattenObject)(mergedFlat); }); } return { name: `DeepSeek (${model})`, type: "llm", canBeTrustedWithVariablesTranslation: true, initializeEstimate() { return __awaiter(this, void 0, void 0, function* () { tokenUsage.inputTokens = 0; tokenUsage.outputTokens = 0; return yield estimateEngine.initialize(); }); }, estimatePrice(usage) { return estimateEngine.estimatePrice(usage); }, getUsage() { return Object.assign({}, tokenUsage); }, translate(translations, options) { return __awaiter(this, void 0, void 0, function* () { const languagesTranslations = {}; for (let targetLanguageCode of options.targetLanguageCodes) { languagesTranslations[targetLanguageCode] = translations; } const chunks = prepareTranslationsAndChunkThem((0, util_1.flattenObject)(translations), languagesTranslations); return yield fetchTranslations(chunks, options); }); }, translateMissed(missingTranslations, options) { return __awaiter(this, void 0, void 0, function* () { const chunks = prepareTranslationsAndChunkThem((0, util_1.flattenObject)(missingTranslations.baseLanguageTranslations), missingTranslations.targetLanguageTranslationsKeys); return yield fetchTranslations(chunks, options); }); } }; } function isDeepSeekInsufficientQuotaError(error) { var _a; const api = rateLimitApiErrorFromChain(error); const nested = api === null || api === void 0 ? void 0 : api.error; const code = (_a = api === null || api === void 0 ? void 0 : api.code) !== null && _a !== void 0 ? _a : nested === null || nested === void 0 ? void 0 : nested.code; if (code === "insufficient_quota") return true; const msg = error instanceof Error ? error.message : (nested === null || nested === void 0 ? void 0 : nested.message) || ""; if (/insufficient.*balance|insufficient.*quota|billing|top.?up/i.test(msg)) return true; return false; } function isTransientDeepSeekRateLimit(error) { if (isDeepSeekInsufficientQuotaError(error)) return false; const api = rateLimitApiErrorFromChain(error); if (api && api.status === 429) return true; if (error instanceof Error && /^429(\s|$)/.test(error.message)) return true; return false; } function rateLimitApiErrorFromChain(error) { let e = error; const seen = new Set(); while (e instanceof Error && !seen.has(e)) { seen.add(e); if (e instanceof openai_1.RateLimitError) return e; if (e instanceof openai_1.APIError && e.status === 429) return e; if (e instanceof openai_1.APIError && (e.code === "insufficient_quota" || e.code === "rate_limit_exceeded")) { return e; } e = e.cause; } return null; } function headerValue(headers, name) { var _a; if (headers == null) return undefined; const h = headers; if (typeof h.get === "function") { return (_a = h.get(name)) !== null && _a !== void 0 ? _a : undefined; } const found = Object.keys(headers).find(k => k.toLowerCase() === name.toLowerCase()); return found ? String(headers[found]) : undefined; } function parseJsonObject(content) { try { return JSON.parse(content); } catch (_a) { const firstIndex = content.indexOf("{"); const lastIndex = content.lastIndexOf("}"); if (firstIndex === -1 || lastIndex === -1 || firstIndex > lastIndex) { throw new Error("DeepSeek response is not valid JSON."); } return JSON.parse(content.substring(firstIndex, lastIndex + 1)); } } function toDeepSeekApiModel(model) { if (model.startsWith("deepseek-")) return model; return `deepseek-${model}`; } function toLanguagesContext(baseLanguageCode, languages) { const getLanguageName = (code) => { let name = iso_639_1_1.default.getName(code); if (!name && code.includes("-")) { const languagePart = code.split("-")[0]; name = iso_639_1_1.default.getName(languagePart); if (name) { const regionPart = code.split("-")[1].toUpperCase(); return `${name} (${regionPart})`; } } return name || code; }; const baseLanguageName = getLanguageName(baseLanguageCode); const targetLanguagesWithNames = languages.map(code => `${code} (${getLanguageName(code)})`); return `You are translating from language with code "${baseLanguageCode}" (${baseLanguageName}) to the following language codes: "${targetLanguagesWithNames.join(", ")}".`; }