/** * Translation Service using DeepL API * Sovereign translation service for Tractatus multilingual submissions * Based on proven family-history implementation */ const axios = require('axios'); const crypto = require('crypto'); const NodeCache = require('node-cache'); const logger = require('../utils/logger.util'); class TranslationService { constructor() { // DeepL API configuration this.apiUrl = process.env.DEEPL_API_URL || 'https://api-free.deepl.com/v2'; this.apiKey = process.env.DEEPL_API_KEY; // Cache translations for 24 hours to reduce API calls this.cache = new NodeCache({ stdTTL: 86400 }); // Supported language pairs (as per Tractatus SubmissionTracking schema) this.supportedLanguages = { 'en': 'English', 'fr': 'French', 'de': 'German', 'es': 'Spanish', 'pt': 'Portuguese', 'zh': 'Chinese', 'ja': 'Japanese', 'ar': 'Arabic', 'mi': 'Te Reo Māori (not supported by DeepL - fallback to EN)', 'it': 'Italian', 'nl': 'Dutch', 'pl': 'Polish', 'ru': 'Russian' }; // Track service availability this.isAvailable = false; this.lastHealthCheck = null; // Check service availability on startup this.checkAvailability(); } /** * Check if DeepL service is available */ async checkAvailability() { try { if (!this.apiKey || this.apiKey === 'your-deepl-api-key-here') { logger.warn('[Translation] DeepL API key not configured'); logger.warn('[Translation] Set DEEPL_API_KEY in .env file'); this.isAvailable = false; return false; } // Test with a simple translation const response = await axios.post(`${this.apiUrl}/translate`, `auth_key=${encodeURIComponent(this.apiKey)}&text=${encodeURIComponent('test')}&target_lang=FR`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 5000 } ); this.isAvailable = true; this.lastHealthCheck = new Date(); logger.info('✅ DeepL translation service available'); return true; } catch (error) { this.isAvailable = false; logger.warn(`[Translation] DeepL service not available: ${error.message}`); if (error.response?.status === 403) { logger.error('[Translation] Invalid DeepL API key'); } return false; } } /** * Generate cache key for translation */ getCacheKey(text, sourceLang, targetLang) { const hash = crypto.createHash('md5') .update(`${sourceLang}:${targetLang}:${text}`) .digest('hex'); return `trans_${hash}`; } /** * Translate text between languages * @param {string} text - Text to translate * @param {string} sourceLang - Source language code (e.g., 'en') * @param {string} targetLang - Target language code (e.g., 'fr') * @param {Object} options - Additional options * @returns {Object} Translation result */ async translate(text, sourceLang, targetLang, options = {}) { // Quick validation if (!text || sourceLang === targetLang) { return { translatedText: text, cached: false, sourceLang, targetLang }; } // Check service availability (with periodic recheck) if (!this.isAvailable || !this.lastHealthCheck || Date.now() - this.lastHealthCheck > 300000) { // 5 minutes await this.checkAvailability(); } if (!this.isAvailable) { logger.error('[Translation] DeepL service not available'); return { translatedText: text, cached: false, sourceLang, targetLang, error: true, errorMessage: 'DeepL translation service not available. Please check DEEPL_API_KEY.' }; } // Check cache first const cacheKey = this.getCacheKey(text, sourceLang, targetLang); const cached = this.cache.get(cacheKey); if (cached && !options.skipCache) { return { translatedText: cached, cached: true, sourceLang, targetLang }; } try { // Prepare parameters for DeepL API const params = new URLSearchParams(); params.append('auth_key', this.apiKey); params.append('text', text); params.append('target_lang', targetLang.toUpperCase()); // Only add source_lang if not auto-detecting if (sourceLang && sourceLang !== 'auto') { params.append('source_lang', sourceLang.toUpperCase()); } // Add HTML handling if needed if (options.format === 'html') { params.append('tag_handling', 'html'); params.append('preserve_formatting', '1'); } // Add formality setting (more formal for publications) if (options.formality) { params.append('formality', options.formality); // 'default', 'more', or 'less' } else { params.append('formality', 'more'); // Default to formal for submissions } // Make translation request const response = await axios.post(`${this.apiUrl}/translate`, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 30000 // 30 second timeout }); const translation = response.data.translations[0]; const translatedText = translation.text; const detectedLang = translation.detected_source_language; // Cache the result if (translatedText && !options.skipCache) { this.cache.set(cacheKey, translatedText); } logger.info(`[Translation] ${sourceLang} → ${targetLang}: ${text.substring(0, 50)}... (${translatedText.length} chars)`); return { translatedText, cached: false, sourceLang: detectedLang ? detectedLang.toLowerCase() : sourceLang, targetLang, detectedLang: detectedLang ? detectedLang.toLowerCase() : null }; } catch (error) { logger.error('[Translation] DeepL API error:', error.message); if (error.response) { logger.error('[Translation] Response status:', error.response.status); logger.error('[Translation] Response data:', error.response.data); } // Return original text with error flag return { translatedText: text, error: true, errorMessage: `Translation failed: ${error.message}`, cached: false, sourceLang, targetLang }; } } /** * Translate multiple texts in batch * @param {Array} texts - Array of texts to translate * @param {string} sourceLang - Source language * @param {string} targetLang - Target language * @returns {Array} Array of translation results */ async translateBatch(texts, sourceLang, targetLang) { if (!Array.isArray(texts) || texts.length === 0) { return []; } // DeepL supports up to 50 texts per request const batchSize = 50; const results = []; for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); try { // Prepare parameters const params = new URLSearchParams(); params.append('auth_key', this.apiKey); // Add each text as a separate parameter batch.forEach(text => params.append('text', text)); params.append('target_lang', targetLang.toUpperCase()); if (sourceLang && sourceLang !== 'auto') { params.append('source_lang', sourceLang.toUpperCase()); } params.append('formality', 'more'); // Formal for submissions const response = await axios.post(`${this.apiUrl}/translate`, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 60000 // 60 seconds for batch }); const translations = response.data.translations.map(t => ({ translatedText: t.text, detectedLang: t.detected_source_language ? t.detected_source_language.toLowerCase() : null, cached: false, sourceLang: t.detected_source_language ? t.detected_source_language.toLowerCase() : sourceLang, targetLang })); results.push(...translations); } catch (error) { logger.error('[Translation] Batch translation error:', error.message); // Return original texts for failed batch results.push(...batch.map(text => ({ translatedText: text, error: true, errorMessage: error.message, cached: false, sourceLang, targetLang }))); } // Small delay between batches to respect rate limits if (i + batchSize < texts.length) { await new Promise(resolve => setTimeout(resolve, 100)); } } return results; } /** * Get DeepL API usage statistics */ async getUsageStats() { if (!this.isAvailable) { return { error: 'Service not available' }; } try { const response = await axios.get(`${this.apiUrl}/usage`, { params: { auth_key: this.apiKey }, timeout: 5000 }); return { characterCount: response.data.character_count, characterLimit: response.data.character_limit, percentageUsed: ((response.data.character_count / response.data.character_limit) * 100).toFixed(2) }; } catch (error) { logger.error('[Translation] Usage check error:', error.message); return { error: error.message }; } } /** * Get cache statistics */ getCacheStats() { return { keys: this.cache.keys().length, hits: this.cache.getStats().hits, misses: this.cache.getStats().misses }; } /** * Clear translation cache */ clearCache() { this.cache.flushAll(); logger.info('[Translation] Cache cleared'); } /** * Get service status */ getStatus() { return { available: this.isAvailable, lastHealthCheck: this.lastHealthCheck, apiUrl: this.apiUrl, cacheStats: this.getCacheStats(), supportedLanguages: this.supportedLanguages, service: 'DeepL', apiKeyConfigured: !!(this.apiKey && this.apiKey !== 'your-deepl-api-key-here') }; } } // Create singleton instance let instance = null; module.exports = { getInstance: () => { if (!instance) { instance = new TranslationService(); } return instance; }, TranslationService };