import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:votianlt_app/config/translation_config.dart'; import 'package:votianlt_app/services/dart_mq.dart'; import 'package:votianlt_app/services/developer.dart' as developer; import 'package:votianlt_app/app_state.dart'; /// Service für Übersetzungen – unterstützt LM Studio (lokal) und Moonshot AI (Cloud). /// /// Das aktive Backend wird in [TranslationConfig.activeBackend] konfiguriert. /// Verwendet das Singleton-Pattern wie andere Services in der App. /// Übersetzt in die vom Benutzer in der App eingestellte Sprache. class TranslationService { static final TranslationService _instance = TranslationService._internal(); factory TranslationService() => _instance; TranslationService._internal(); static const String _chatCompletionsEndpoint = '/chat/completions'; // HTTP Client final http.Client _client = http.Client(); // Verfügbarkeitsstatus bool _isAvailable = false; // Aktuell eingestellte Zielsprache (aus der App) String get _targetLanguageCode => AppState().languageCode; /// Gibt an ob das Übersetzungsbackend verfügbar ist bool get isAvailable => _isAvailable; /// Name des aktiven Backends (für Logs und DartMQ-Nachrichten) String get _backendName => switch (TranslationConfig.activeBackend) { TranslationBackend.lmStudio => 'lm-studio', TranslationBackend.moonshot => 'moonshot-ai', }; // Verfügbare Sprachen für Übersetzung (alle unterstützten App-Sprachen) static final Map supportedLanguages = { 'de': 'German', 'en': 'English', 'es': 'Spanish', 'fr': 'French', 'pl': 'Polish', 'ru': 'Russian', 'tr': 'Turkish', 'et': 'Estonian', 'lv': 'Latvian', 'lt': 'Lithuanian', }; /// Initialisiert den Translation Service Future initialize() async { try { // Auf Sprachänderungen hören _listenToLanguageChanges(); // Verfügbarkeit prüfen _isAvailable = await _checkAvailability(); _notifyInitialization(); developer.log( 'TranslationService initialisiert - Backend: $_backendName, Zielsprache: ${supportedLanguages[_targetLanguageCode]}', name: 'TranslationService', ); } catch (e) { developer.log('Fehler bei Initialisierung des TranslationService: $e', name: 'TranslationService'); _isAvailable = false; } } /// Prüft ob das konfigurierte Backend erreichbar ist Future _checkAvailability() async { switch (TranslationConfig.activeBackend) { case TranslationBackend.lmStudio: return _checkLmStudioAvailability(); case TranslationBackend.moonshot: return _checkMoonshotAvailability(); } } Future _checkLmStudioAvailability() async { try { final response = await _client .get(Uri.parse('${TranslationConfig.lmStudioBaseUrl}/v1/models')) .timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final data = jsonDecode(response.body); final models = data['data'] as List?; developer.log( 'LM Studio verbunden - Verfügbare Modelle: ${models?.length ?? 0}', name: 'TranslationService', ); return true; } return false; } catch (e) { developer.log('LM Studio nicht erreichbar: $e', name: 'TranslationService'); return false; } } Future _checkMoonshotAvailability() async { try { final response = await _client .get( Uri.parse('${TranslationConfig.moonshotBaseUrl}/models'), headers: {'Authorization': 'Bearer ${TranslationConfig.moonshotApiKey}'}, ) .timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { developer.log('Moonshot AI verbunden - API erreichbar', name: 'TranslationService'); return true; } return false; } catch (e) { developer.log('Moonshot AI nicht erreichbar: $e', name: 'TranslationService'); return false; } } /// Sendet Initialisierungs-Notification über DartMQ void _notifyInitialization() { DartMQ().publish>('translation/service_initialized', { 'language': _targetLanguageCode, 'backend': _backendName, 'isAvailable': isAvailable, 'endpoint': _activeEndpoint, }); } /// Hört auf Sprachänderungen und aktualisiert den Service void _listenToLanguageChanges() { localeNotifier.addListener(() { final newLanguage = AppState().languageCode; developer.log( 'Sprache in App geändert zu: ${supportedLanguages[newLanguage] ?? newLanguage}', name: 'TranslationService', ); DartMQ().publish>('translation/language_changed', { 'language': newLanguage, 'displayName': supportedLanguages[newLanguage], 'backend': _backendName, }); }); } /// Basis-URL des aktiven Backends String get _activeEndpoint => switch (TranslationConfig.activeBackend) { TranslationBackend.lmStudio => TranslationConfig.lmStudioBaseUrl, TranslationBackend.moonshot => TranslationConfig.moonshotBaseUrl, }; /// Übersetzt einen Text in die vom Benutzer eingestellte Sprache /// /// [text] - Der zu übersetzende Text /// [sourceLanguage] - Die Ausgangssprache (optional, wird automatisch erkannt wenn null) /// /// Gibt den übersetzten Text zurück oder den Originaltext bei Fehlern Future translate( String text, { String? sourceLanguage, }) async { if (text.isEmpty) { return text; } // Bei rein numerischem Text oder sehr kurzem Text nicht übersetzen if (_shouldSkipTranslation(text)) { developer.log('Übersetzung übersprungen (kein Text): "$text"', name: 'TranslationService'); return text; } // Zielsprache aus der App holen final targetCode = _targetLanguageCode; // Wenn Quelle gleich Ziel, nicht übersetzen final detectedSource = sourceLanguage ?? await _detectLanguage(text); if (detectedSource == targetCode) { developer.log('Übersetzung übersprungen (Quelle = Ziel): "$text"', name: 'TranslationService'); return text; } try { final translatedText = await _translate(text, detectedSource, targetCode); developer.log( 'Übersetzung [${supportedLanguages[detectedSource]} -> ${supportedLanguages[targetCode]}]:\n' ' Original: "$text"\n' ' Übersetzt: "$translatedText"', name: 'TranslationService', ); return translatedText; } catch (e) { developer.log( 'Fehler bei der Übersetzung: $e\n Original: "$text"', name: 'TranslationService', ); return text; // Bei Fehler Original zurückgeben } } /// Übersetzt eine Liste von Texten in die vom Benutzer eingestellte Sprache Future> translateList( List texts, { String? sourceLanguage, }) async { if (texts.isEmpty) return texts; final results = []; final targetCode = _targetLanguageCode; developer.log( 'Starte Batch-Übersetzung von ${texts.length} Texten nach ${supportedLanguages[targetCode]}', name: 'TranslationService', ); for (int i = 0; i < texts.length; i++) { final text = texts[i]; if (text.isEmpty || _shouldSkipTranslation(text)) { results.add(text); continue; } try { final detectedSource = sourceLanguage ?? await _detectLanguage(text); if (detectedSource == targetCode) { results.add(text); continue; } final translatedText = await _translate(text, detectedSource, targetCode); developer.log( 'Batch [${i + 1}/${texts.length}] [${supportedLanguages[detectedSource]} -> ${supportedLanguages[targetCode]}]:\n' ' Original: "$text"\n' ' Übersetzt: "$translatedText"', name: 'TranslationService', ); results.add(translatedText); } catch (e) { developer.log( 'Fehler bei Batch-Übersetzung [${i + 1}/${texts.length}]: $e\n Original: "$text"', name: 'TranslationService', ); results.add(text); } } developer.log( 'Batch-Übersetzung abgeschlossen: ${texts.length} Texte verarbeitet', name: 'TranslationService', ); return results; } /// Dispatcht die Übersetzung an das konfigurierte Backend Future _translate(String text, String sourceCode, String targetCode) { switch (TranslationConfig.activeBackend) { case TranslationBackend.lmStudio: return _translateWithLmStudio(text, sourceCode, targetCode); case TranslationBackend.moonshot: return _translateWithMoonshot(text, sourceCode, targetCode); } } /// Übersetzung mit LM Studio REST API (lokales Modell, kein API-Key) Future _translateWithLmStudio( String text, String sourceCode, String targetCode, ) async { final targetName = supportedLanguages[targetCode] ?? targetCode; final sourceName = supportedLanguages[sourceCode] ?? sourceCode; final systemPrompt = 'You are a professional translator. Translate the user input from $sourceName to $targetName. ' 'Return ONLY the translation, without any additional text, explanations, or quotes.'; final requestBody = { 'model': TranslationConfig.lmStudioModel, 'messages': [ {'role': 'system', 'content': systemPrompt}, {'role': 'user', 'content': text}, ], 'temperature': 0.1, 'max_tokens': 2048, 'stream': false, }; developer.log( 'Sende Übersetzungsanfrage an LM Studio: $sourceName -> $targetName (${text.length} Zeichen)', name: 'TranslationService', ); final response = await _client .post( Uri.parse('${TranslationConfig.lmStudioBaseUrl}$_chatCompletionsEndpoint'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ) .timeout(const Duration(seconds: 30)); if (response.statusCode != 200) { throw Exception('LM Studio API Fehler: ${response.statusCode} - ${response.body}'); } return _extractTranslation(response.body, 'LM Studio'); } /// Übersetzung mit Moonshot AI Cloud API (Kimi, API-Key erforderlich) Future _translateWithMoonshot( String text, String sourceCode, String targetCode, ) async { final targetName = supportedLanguages[targetCode] ?? targetCode; final sourceName = supportedLanguages[sourceCode] ?? sourceCode; final systemPrompt = 'You are a professional translator. Translate the user input from $sourceName to $targetName. ' 'Return ONLY the translation, without any additional text, explanations, or quotes.'; final requestBody = { 'model': TranslationConfig.moonshotModel, 'messages': [ {'role': 'system', 'content': systemPrompt}, {'role': 'user', 'content': text}, ], 'temperature': 0.1, 'max_tokens': 2048, 'stream': false, }; developer.log( 'Sende Übersetzungsanfrage an Moonshot AI: $sourceName -> $targetName (${text.length} Zeichen)', name: 'TranslationService', ); final response = await _client .post( Uri.parse('${TranslationConfig.moonshotBaseUrl}$_chatCompletionsEndpoint'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ${TranslationConfig.moonshotApiKey}', }, body: jsonEncode(requestBody), ) .timeout(const Duration(seconds: 30)); if (response.statusCode != 200) { throw Exception('Moonshot AI API Fehler: ${response.statusCode} - ${response.body}'); } return _extractTranslation(response.body, 'Moonshot AI'); } /// Extrahiert den Übersetzungstext aus der OpenAI-kompatiblen API-Antwort String _extractTranslation(String responseBody, String backendLabel) { final data = jsonDecode(responseBody); final choices = data['choices'] as List?; if (choices == null || choices.isEmpty) { throw Exception('Leere Antwort von $backendLabel'); } final message = choices[0]['message'] as Map?; String translated = message?['content']?.toString().trim() ?? ''; // Anführungszeichen entfernen falls vorhanden if ((translated.startsWith('"') && translated.endsWith('"')) || (translated.startsWith("'") && translated.endsWith("'"))) { translated = translated.substring(1, translated.length - 1); } return translated; } /// Hilfsmethode: Erkennt die Sprache eines Textes Future _detectLanguage(String text) async { // Für kurze Texte: Default zu Englisch if (text.length < 10) { return 'en'; } // Einfache Heuristik basierend auf häufigen Wörtern/Zeichen final lowerText = text.toLowerCase(); // Deutsche Wörter prüfen final germanWords = ['der', 'die', 'das', 'und', 'ist', 'zu', 'den', 'mit', 'von', 'für']; if (germanWords.any((word) => lowerText.contains(' $word ') || lowerText.startsWith('$word '))) { return 'de'; } // Französische Wörter prüfen final frenchWords = ['le', 'la', 'les', 'et', 'est', 'pour', 'dans', 'sur', 'avec', 'une']; if (frenchWords.any((word) => lowerText.contains(' $word ') || lowerText.startsWith('$word '))) { return 'fr'; } // Spanische Wörter prüfen final spanishWords = ['el', 'la', 'los', 'las', 'y', 'es', 'para', 'con', 'por', 'del']; if (spanishWords.any((word) => lowerText.contains(' $word ') || lowerText.startsWith('$word '))) { return 'es'; } // Polnische Wörter prüfen final polishWords = ['jest', 'i', 'w', 'na', 'do', 'nie', 'się', 'tego', 'tej']; if (polishWords.any((word) => lowerText.contains(' $word ') || lowerText.startsWith('$word '))) { return 'pl'; } // Russische/Cyrillische Zeichen prüfen if (RegExp(r'[а-яА-Я]').hasMatch(text)) { return 'ru'; } // Türkische Zeichen prüfen if (RegExp(r'[çğıöşüÇĞİÖŞÜ]').hasMatch(text)) { return 'tr'; } // Estnische Zeichen prüfen if (RegExp(r'[äöüõÄÖÜÕ]').hasMatch(text)) { return 'et'; } // Lettische Zeichen prüfen if (RegExp(r'[āčēģīķļņšūžĀČĒĢĪĶĻŅŠŪŽ]').hasMatch(text)) { return 'lv'; } // Litauische Zeichen prüfen if (RegExp(r'[ąčęėįšųūžĄČĘĖĮŠŲŪŽ]').hasMatch(text)) { return 'lt'; } // Arabische Zeichen prüfen if (RegExp(r'[\u0600-\u06FF]').hasMatch(text)) { return 'ar'; } // Chinesische/Japanische/Koreanische Zeichen prüfen if (RegExp(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]').hasMatch(text)) { return 'zh'; } // Default: Englisch return 'en'; } /// Prüft ob die Übersetzung übersprungen werden sollte bool _shouldSkipTranslation(String text) { // Numerische Werte nicht übersetzen if (RegExp(r'^\d+$').hasMatch(text.trim())) { return true; } // Sehr kurze Codes nicht übersetzen if (text.trim().length <= 2) { return true; } // E-Mail Adressen nicht übersetzen if (text.contains('@') && text.contains('.')) { return true; } // URLs nicht übersetzen if (text.startsWith('http://') || text.startsWith('https://')) { return true; } return false; } /// Prüft ob ein Übersetzungsmodell verfügbar ist Future isModelAvailable() async { return _checkAvailability(); } /// Gibt detaillierte Verfügbarkeitsinformationen zurück Future> getAvailabilityInfo() async { final isOnline = await _checkAvailability(); return { 'isAvailable': isOnline, 'backend': _backendName, 'targetLanguage': _targetLanguageCode, 'endpoint': _activeEndpoint, 'platform': Platform.operatingSystem, }; } /// Gibt die aktuell eingestellte Zielsprache zurück String get targetLanguageCode => _targetLanguageCode; /// Gibt den Anzeigenamen der aktuellen Sprache zurück String get targetLanguageDisplayName { return supportedLanguages[_targetLanguageCode] ?? _targetLanguageCode; } /// Gibt eine Liste aller verfügbaren Sprachen zurück List> getAvailableLanguages() { return supportedLanguages.entries.toList(); } /// Gibt den Anzeigenamen einer Sprache zurück String getLanguageDisplayName(String code) { return supportedLanguages[code] ?? code; } /// Schließt den Service und gibt Ressourcen frei Future dispose() async { _client.close(); _isAvailable = false; developer.log('TranslationService disposed', name: 'TranslationService'); } }