- 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>
288 lines
8.1 KiB
JavaScript
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;
|