tractatus/src/services/Translation.service.js
TheFlow 40601f7d27 refactor(lint): fix code style and unused variables across src/
- Fixed unused function parameters by prefixing with underscore
- Removed unused imports and variables
- Applied eslint --fix for automatic style fixes
  - Property shorthand
  - String template literals
  - Prefer const over let where appropriate
  - Spacing and formatting

Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues)

Related to CI lint failures in previous commit

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 20:15:26 +13:00

357 lines
10 KiB
JavaScript

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