refactor: Projektstruktur in app/ und backend/ aufgeteilt

This commit is contained in:
2026-03-24 15:06:44 +01:00
parent 5f5d5995c5
commit 2673ef658d
449 changed files with 28551 additions and 167 deletions

View File

@@ -0,0 +1,535 @@
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');
}
}