/** * DeepL Translation Service * * Professional translation API for German and French translations * Preserves markdown formatting and handles large documents * * API Docs: https://www.deepl.com/docs-api */ const axios = require('axios'); class DeepLService { constructor() { this.apiKey = process.env.DEEPL_API_KEY; this.apiUrl = process.env.DEEPL_API_URL || 'https://api-free.deepl.com/v2'; if (!this.apiKey) { console.warn('[DeepL] API key not configured. Translation service disabled.'); } } /** * Check if DeepL service is available */ isAvailable() { return !!this.apiKey; } /** * Translate text to target language * * @param {string} text - Text to translate (supports markdown) * @param {string} targetLang - Target language code (DE, FR) * @param {object} options - Translation options * @returns {Promise<{text: string, detectedSourceLang: string}>} */ async translate(text, targetLang, options = {}) { if (!this.isAvailable()) { throw new Error('DeepL API key not configured'); } if (!text || text.trim().length === 0) { return { text: '', detectedSourceLang: 'EN' }; } const { sourceLang = 'EN', preserveFormatting = true, formality = 'default', // 'default', 'more', 'less' tagHandling = 'html' // 'html' or 'xml' - preserves markup } = options; try { const payload = { text: [text], source_lang: sourceLang, target_lang: targetLang.toUpperCase(), tag_handling: tagHandling }; // Add formality only for languages that support it (DE, FR, IT, ES, NL, PL, PT, RU) if (['DE', 'FR', 'IT', 'ES', 'NL', 'PL', 'PT', 'RU'].includes(targetLang.toUpperCase()) && formality !== 'default') { payload.formality = formality; } const response = await axios.post( `${this.apiUrl}/translate`, payload, { headers: { 'Authorization': `DeepL-Auth-Key ${this.apiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 // 30 second timeout } ); if (response.data && response.data.translations && response.data.translations.length > 0) { return { text: response.data.translations[0].text, detectedSourceLang: response.data.translations[0].detected_source_language }; } throw new Error('Invalid response from DeepL API'); } catch (error) { if (error.response) { // DeepL API error const status = error.response.status; const message = error.response.data?.message || 'Unknown error'; if (status === 403) { throw new Error('DeepL API authentication failed. Check API key.'); } else if (status === 456) { throw new Error('DeepL quota exceeded. Upgrade plan or wait for reset.'); } else if (status === 429) { throw new Error('DeepL rate limit exceeded. Please retry later.'); } else { throw new Error(`DeepL API error (${status}): ${message}`); } } throw new Error(`Translation failed: ${error.message}`); } } /** * Translate markdown document while preserving structure * * @param {string} markdown - Markdown content * @param {string} targetLang - Target language (DE, FR) * @returns {Promise} Translated markdown */ async translateMarkdown(markdown, targetLang) { if (!markdown || markdown.trim().length === 0) { return ''; } // DeepL's tag_handling: 'html' preserves markdown well // But we can also split by code blocks to protect them const result = await this.translate(markdown, targetLang, { tagHandling: 'html', preserveFormatting: true }); return result.text; } /** * Translate HTML content while preserving markup * * @param {string} html - HTML content * @param {string} targetLang - Target language (DE, FR) * @returns {Promise} Translated HTML */ async translateHTML(html, targetLang) { if (!html || html.trim().length === 0) { return ''; } const result = await this.translate(html, targetLang, { tagHandling: 'html', preserveFormatting: true }); return result.text; } /** * Translate a full document object * Translates: title, content_markdown, content_html, and toc * * @param {object} document - Document object * @param {string} targetLang - Target language (DE, FR) * @returns {Promise} Translation object ready for storage */ async translateDocument(document, targetLang) { if (!this.isAvailable()) { throw new Error('DeepL API key not configured'); } console.log(`[DeepL] Translating document "${document.title}" to ${targetLang}...`); const translation = { title: '', content_markdown: '', content_html: '', toc: [], metadata: { translated_by: 'deepl', translated_at: new Date(), reviewed: false, source_version: document.metadata?.version || '1.0' } }; try { // 1. Translate title const titleResult = await this.translate(document.title, targetLang); translation.title = titleResult.text; console.log(`[DeepL] ✓ Title translated`); // 2. Translate markdown content (preferred source) if (document.content_markdown) { translation.content_markdown = await this.translateMarkdown( document.content_markdown, targetLang ); console.log(`[DeepL] ✓ Markdown translated (${translation.content_markdown.length} chars)`); } // 3. Translate HTML content if (document.content_html) { translation.content_html = await this.translateHTML( document.content_html, targetLang ); console.log(`[DeepL] ✓ HTML translated (${translation.content_html.length} chars)`); } // 4. Translate table of contents if (document.toc && document.toc.length > 0) { translation.toc = await Promise.all( document.toc.map(async (item) => { const translatedTitle = await this.translate(item.title, targetLang); return { ...item, title: translatedTitle.text }; }) ); console.log(`[DeepL] ✓ ToC translated (${translation.toc.length} items)`); } console.log(`[DeepL] ✓ Document translation complete`); return translation; } catch (error) { console.error(`[DeepL] Translation failed:`, error.message); throw error; } } /** * Get usage statistics from DeepL API * * @returns {Promise} Usage stats {character_count, character_limit} */ async getUsage() { if (!this.isAvailable()) { throw new Error('DeepL API key not configured'); } try { const response = await axios.get( `${this.apiUrl}/usage`, { headers: { 'Authorization': `DeepL-Auth-Key ${this.apiKey}` } } ); return { character_count: response.data.character_count, character_limit: response.data.character_limit, percentage_used: (response.data.character_count / response.data.character_limit * 100).toFixed(2) }; } catch (error) { throw new Error(`Failed to get usage stats: ${error.message}`); } } /** * Get list of supported target languages * * @returns {Promise} List of language objects */ async getSupportedLanguages() { if (!this.isAvailable()) { throw new Error('DeepL API key not configured'); } try { const response = await axios.get( `${this.apiUrl}/languages`, { headers: { 'Authorization': `DeepL-Auth-Key ${this.apiKey}` }, params: { type: 'target' } } ); return response.data; } catch (error) { throw new Error(`Failed to get supported languages: ${error.message}`); } } } // Singleton instance const deeplService = new DeepLService(); module.exports = deeplService;