Files
votianlt/app/lib/services/translation_service.dart

536 lines
17 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, String> 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<void> 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<bool> _checkAvailability() async {
switch (TranslationConfig.activeBackend) {
case TranslationBackend.lmStudio:
return _checkLmStudioAvailability();
case TranslationBackend.moonshot:
return _checkMoonshotAvailability();
}
}
Future<bool> _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<dynamic>?;
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<bool> _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<Map<String, dynamic>>('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<Map<String, dynamic>>('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<String> 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<List<String>> translateList(
List<String> texts, {
String? sourceLanguage,
}) async {
if (texts.isEmpty) return texts;
final results = <String>[];
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<String> _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<String> _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<String> _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<dynamic>?;
if (choices == null || choices.isEmpty) {
throw Exception('Leere Antwort von $backendLabel');
}
final message = choices[0]['message'] as Map<String, dynamic>?;
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<String> _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<bool> isModelAvailable() async {
return _checkAvailability();
}
/// Gibt detaillierte Verfügbarkeitsinformationen zurück
Future<Map<String, dynamic>> 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<MapEntry<String, String>> 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<void> dispose() async {
_client.close();
_isAvailable = false;
developer.log('TranslationService disposed', name: 'TranslationService');
}
}