UNPKG

json-translated-ai

Version:

AI-powered translation tool supporting OpenAI GPT, Anthropic Claude, and Google Gemini models

323 lines (282 loc) 8.7 kB
import { getAIClient } from './ai-client-manager.js'; import { getCachedTranslation, setCachedTranslation, shouldSaveCache, updateTokenUsage, } from './cache-manager.js'; import { OPTIMIZATION_CONFIG } from './config.js'; import { validateLanguageCode } from './language-utils.js'; import { createSystemPrompt } from './system-message-utils.js'; import { translateText } from './translation-core.js'; import { translationFailureReporter } from './translation-failure-reporter.js'; export async function translateBatch( texts, targetLanguage, keys, model, retryCount = 0 ) { if (!Array.isArray(texts) || texts.length === 0) { return []; } const { validTexts, validKeys, cacheKeys } = prepareBatchData( texts, keys, targetLanguage ); if (validTexts.length === 0) { return texts; } const { cachedResults, uncachedTexts, uncachedIndices } = checkCacheAndSeparate(validTexts, validKeys); if (uncachedTexts.length === 0) { console.log(` 📋 Batch cache hit for ${validTexts.length} texts`); return createFinalResults(texts, cacheKeys); } if (uncachedTexts.length > 0) { console.log( ` 🤖 Batch translating ${uncachedTexts.length} texts (${cachedResults.length} cached)` ); // Note: Spinner management is handled by the calling processBatch function try { const client = getAIClient(model); if (!client) { throw new Error( 'AI client not initialized - check PROVIDER_KEY or OPENAI_API_KEY' ); } const validatedLanguage = validateLanguageCode(targetLanguage); const batchPrompt = createBatchPrompt(uncachedTexts); const systemPrompt = createSystemPrompt( validatedLanguage, targetLanguage, true // isBatch = true for batch translation ); const userPrompt = `Translate these texts to ${ validatedLanguage || targetLanguage }:\n\n${batchPrompt}`; const completion = await client.chat.completions.create( { model: model, messages: [ { role: 'system', content: systemPrompt, }, { role: 'user', content: userPrompt, }, ], max_tokens: OPTIMIZATION_CONFIG.MAX_TOKENS_PER_BATCH, temperature: 0.3, }, { timeout: OPTIMIZATION_CONFIG.BATCH_TIMEOUT, } ); if (!completion?.choices?.[0]?.message?.content) { throw new Error('Invalid response structure from OpenAI API'); } const translatedBatch = completion.choices[0].message.content.trim(); const translations = parseBatchResponse(translatedBatch); if (translations.length !== uncachedTexts.length) { throw new Error( `Expected ${uncachedTexts.length} translations, got ${translations.length}` ); } updateTokenUsage(completion); cacheTranslations( translations, uncachedIndices, cacheKeys, targetLanguage ); if (shouldSaveCache()) { const { saveCache } = await import('./cache-manager.js'); saveCache(); } await applyBatchRateLimiting(completion); const results = combineResults(texts, translations, uncachedIndices); console.log(` ✅ Batch translated ${uncachedTexts.length} texts`); return results; } catch (error) { return await handleBatchError( error, texts, keys, targetLanguage, model, retryCount ); } } return texts; } function prepareBatchData(texts, keys, targetLanguage) { const validTexts = texts.filter( (text) => typeof text === 'string' && text.trim() !== '' ); const validKeys = keys.filter( (_, index) => typeof texts[index] === 'string' && texts[index].trim() !== '' ); const cacheKeys = validTexts.map( (text) => `${text .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .toLowerCase()}_${targetLanguage}` ); return { validTexts, validKeys, cacheKeys }; } function checkCacheAndSeparate(validTexts, validKeys) { const cachedResults = []; const uncachedTexts = []; const uncachedIndices = []; validTexts.forEach((text, index) => { if (getCachedTranslation(text, validKeys[index])) { cachedResults[index] = getCachedTranslation(text, validKeys[index]); } else { uncachedTexts.push(text); uncachedIndices.push(index); } }); return { cachedResults, uncachedTexts, uncachedIndices }; } function createFinalResults(texts, cacheKeys) { return texts.map((text, index) => { if (typeof text === 'string' && text.trim() !== '') { return getCachedTranslation(text, cacheKeys[index]); } return text; }); } function createBatchPrompt(uncachedTexts) { return uncachedTexts .map((text, index) => `${index + 1}. "${text}"`) .join('\n'); } // System prompt creation is now handled by the centralized utility function parseBatchResponse(translatedBatch) { return translatedBatch .split('\n') .filter((line) => line.trim() && /^\d+\./.test(line)) .map((line) => line .replace(/^\d+\.\s*/, '') .replace(/^["']|["']$/g, '') .trim() ) .filter((text) => text.length > 0); } function cacheTranslations( translations, uncachedIndices, cacheKeys, targetLanguage ) { translations.forEach((translation, index) => { const originalIndex = uncachedIndices[index]; const originalText = cacheKeys[originalIndex].replace( `_${targetLanguage}`, '' ); setCachedTranslation(originalText, targetLanguage, translation); }); } async function applyBatchRateLimiting(completion) { const responseTime = Date.now() - (completion.usage?.created || Date.now()); const delay = Math.min(500 + responseTime / 20, 2000); await new Promise((resolve) => setTimeout(resolve, delay)); } function combineResults(texts, translations, uncachedIndices) { const results = [...texts]; translations.forEach((translation, index) => { const originalIndex = uncachedIndices[index]; results[originalIndex] = translation; }); return results; } async function handleBatchError( error, texts, keys, targetLanguage, model, retryCount = 0 ) { console.log(` ❌ Batch translation error:`, error.message); // Check if this is a timeout or rate limit error that might be retryable const isRetryableError = error.message.includes('timeout') || error.message.includes('rate limit') || error.status === 429 || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT'; // Only retry once for retryable errors to avoid infinite loops if (isRetryableError && retryCount < 1) { console.log(` 🔄 Attempting retry for batch (attempt ${retryCount + 1})`); try { // Wait a bit before retrying await new Promise((resolve) => setTimeout(resolve, 2000)); // Retry the batch translation with incremented retry count return await translateBatch( texts, targetLanguage, keys, model, retryCount + 1 ); } catch (retryError) { console.log(` ❌ Retry also failed for batch:`, retryError.message); } } // Fallback to individual translations if (OPTIMIZATION_CONFIG.ENABLE_INDIVIDUAL_FALLBACK) { console.log(` 🔄 Falling back to individual translations`); return await fallbackToIndividualTranslations( texts, keys, targetLanguage, model ); } throw error; } async function fallbackToIndividualTranslations( texts, keys, targetLanguage, model ) { // Note: Spinner management is handled by the calling processBatch function const fallbackResults = []; for (let i = 0; i < texts.length; i++) { if (typeof texts[i] === 'string' && texts[i].trim() !== '') { try { const fallbackTranslation = await translateText( texts[i], targetLanguage, keys[i], 0, model ); fallbackResults[i] = fallbackTranslation; } catch (fallbackError) { console.log( ` ❌ Fallback failed for text ${i}:`, fallbackError.message ); // Record the failure const targetFile = `${targetLanguage}.json`; translationFailureReporter.recordFailure(targetFile, keys[i], texts[i]); fallbackResults[i] = texts[i]; } } else { fallbackResults[i] = texts[i]; } } // Note: Spinner management is handled by the calling processBatch function return fallbackResults; }