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
JavaScript
;
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(", ")}".`;
}