tractatus/src/services/DeepL.service.js
TheFlow e6b98173fe fix(i18n): add axios dependency and fix DeepL API parameters
- Install axios for DeepL HTTP requests
- Remove unsupported preserve_formatting parameter from DeepL API calls
- Add formality parameter only for supported languages (DE, FR, etc.)
- Tested successfully: 'Hello, World!' → 'Hallo, Welt!'

DeepL API Status:
- API key configured (free tier: 500k chars/month)
- Current usage: 12,131 / 500,000 characters (2.43%)
- Remaining quota: 487,869 characters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 00:59:05 +13:00

288 lines
8.1 KiB
JavaScript

/**
* 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<string>} 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<string>} 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<object>} 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<object>} 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<Array>} 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;