diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7cbc8ff0..bb0def43 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1106,19 +1106,31 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future _translateIncomingContactMessage( + Future translateContactMessage( String contactKeyHex, - Message message, - ) async { + Message message, { + bool manualTranslation = false, + }) async { try { + if (message.translatedText?.trim().isNotEmpty == true || + (!manualTranslation && + message.translationStatus != MessageTranslationStatus.none)) { + return null; + } final service = _translationService; if (service == null || - !service.shouldTranslateIncoming( - text: message.text, - isCli: message.isCli, - isOutgoing: message.isOutgoing, - )) { - return; + !(manualTranslation + ? service.canTranslateIncoming( + text: message.text, + isCli: message.isCli, + isOutgoing: message.isOutgoing, + ) + : service.shouldAutoTranslateIncoming( + text: message.text, + isCli: message.isCli, + isOutgoing: message.isOutgoing, + ))) { + return null; } final targetLanguageCode = service.resolvedIncomingLanguageCode( _appSettingsService?.settings.languageOverride, @@ -1128,7 +1140,7 @@ class MeshCoreConnector extends ChangeNotifier { targetLanguageCode: targetLanguageCode, ); if (result == null) { - return; + return null; } final translated = result.status == MessageTranslationStatus.completed ? result.translatedText @@ -1143,24 +1155,38 @@ class MeshCoreConnector extends ChangeNotifier { translationModelId: result.modelId, ), ); + return result; } catch (error) { appLogger.warn('Translation failed for contact message: $error'); + return null; } } - Future _translateIncomingChannelMessage( + Future translateChannelMessage( int channelIndex, - ChannelMessage message, - ) async { + ChannelMessage message, { + bool manualTranslation = false, + }) async { try { + if (message.translatedText?.trim().isNotEmpty == true || + (!manualTranslation && + message.translationStatus != MessageTranslationStatus.none)) { + return null; + } final service = _translationService; if (service == null || - !service.shouldTranslateIncoming( - text: message.text, - isCli: false, - isOutgoing: message.isOutgoing, - )) { - return; + !(manualTranslation + ? service.canTranslateIncoming( + text: message.text, + isCli: false, + isOutgoing: message.isOutgoing, + ) + : service.shouldAutoTranslateIncoming( + text: message.text, + isCli: false, + isOutgoing: message.isOutgoing, + ))) { + return null; } final targetLanguageCode = service.resolvedIncomingLanguageCode( _appSettingsService?.settings.languageOverride, @@ -1170,11 +1196,16 @@ class MeshCoreConnector extends ChangeNotifier { targetLanguageCode: targetLanguageCode, ); if (result == null) { - return; + return null; } - final translated = result.status == MessageTranslationStatus.completed + var translated = result.status == MessageTranslationStatus.completed ? result.translatedText : null; + // Strip replyInfo prefix from translated text to match stored message.text + if (translated != null) { + final regex = RegExp(r'^@\[[^\]]+\]\s+', dotAll: true); + translated = translated.replaceFirst(regex, ''); + } _updateStoredChannelMessage( channelIndex, message.messageId, @@ -1185,8 +1216,10 @@ class MeshCoreConnector extends ChangeNotifier { translationModelId: result.modelId, ), ); + return result; } catch (error) { appLogger.warn('Translation failed for channel message: $error'); + return null; } } @@ -4373,6 +4406,12 @@ class MeshCoreConnector extends ChangeNotifier { void _handleIncomingMessage(Uint8List frame) async { if (_selfPublicKey == null) return; + // If we're syncing the queued messages, advance the queue immediately + // before any potentially long async work (like translation/notifications). + if (_isSyncingQueuedMessages) { + _handleQueuedMessageReceived(); + } + var message = _parseContactMessage(frame); // If message parsing failed due to unknown contact, refresh contacts and retry @@ -4438,35 +4477,52 @@ class MeshCoreConnector extends ChangeNotifier { } } _addMessage(message.senderKeyHex, message); - if (!message.isOutgoing) { - unawaited( - _translateIncomingContactMessage(message.senderKeyHex, message), - ); - } _maybeIncrementContactUnread(message); notifyListeners(); - // Show notification for new incoming message + // Show notification for new incoming message (run async with translation) if (!message.isOutgoing && !message.isCli && _appSettingsService != null) { final settings = _appSettingsService!.settings; if (settings.notificationsEnabled && settings.notifyOnNewMessage) { - if (contact?.type == advTypeChat) { - _notificationService.showMessageNotification( - contactName: contact?.name ?? 'Unknown', - message: message.text, - contactId: message.senderKeyHex, - badgeCount: getTotalUnreadCount(), + final msg = message; // capture for closure + final c = contact; // capture contact reference + unawaited(() async { + final translationResult = await translateContactMessage( + msg.senderKeyHex, + msg, ); - } else if (contact?.type == advTypeRoom) { - _notificationService.showMessageNotification( - contactName: contact?.name ?? 'Unknown Room', - message: message.text, - contactId: message.senderKeyHex, - badgeCount: getTotalUnreadCount(), - ); - } + if (c?.type == advTypeChat) { + final resolvedText = + (translationResult != null && + translationResult.status == + MessageTranslationStatus.completed && + translationResult.translatedText.trim().isNotEmpty) + ? translationResult.translatedText.trim() + : msg.text.trim(); + await _notificationService.showMessageNotification( + contactName: c?.name ?? 'Unknown', + message: resolvedText, + contactId: msg.senderKeyHex, + badgeCount: getTotalUnreadCount(), + ); + } else if (c?.type == advTypeRoom) { + final resolvedText = + (translationResult != null && + translationResult.status == + MessageTranslationStatus.completed && + translationResult.translatedText.trim().isNotEmpty) + ? translationResult.translatedText.trim() + : msg.text.trim(); + await _notificationService.showMessageNotification( + contactName: c?.name ?? 'Unknown Room', + message: resolvedText, + contactId: msg.senderKeyHex, + badgeCount: getTotalUnreadCount(), + ); + } + }()); } } _handleQueuedMessageReceived(); @@ -4740,6 +4796,7 @@ class MeshCoreConnector extends ChangeNotifier { void _maybeNotifyChannelMessage( ChannelMessage message, { String? channelName, + TranslationResult? translationResult, }) { if (message.isOutgoing || _appSettingsService == null) return; final channelIndex = message.channelIndex; @@ -4753,16 +4810,30 @@ class MeshCoreConnector extends ChangeNotifier { final label = channelName ?? _channelDisplayName(channelIndex); if (_appSettingsService!.isChannelMuted(label)) return; - _notificationService.showChannelMessageNotification( - channelName: label, - senderName: message.senderName, - message: message.text, - channelIndex: channelIndex, - badgeCount: getTotalUnreadCount(), - ); + // Reuse translation result only if completed and non-empty; else use original text + final resolvedText = + (translationResult != null && + translationResult.status == MessageTranslationStatus.completed && + translationResult.translatedText.trim().isNotEmpty) + ? translationResult.translatedText.trim() + : message.text.trim(); + unawaited(() async { + await _notificationService.showChannelMessageNotification( + channelName: label, + senderName: message.senderName, + message: resolvedText, + channelIndex: message.channelIndex, + badgeCount: getTotalUnreadCount(), + ); + }()); } - void _handleIncomingChannelMessage(Uint8List frame) { + void _handleIncomingChannelMessage(Uint8List frame) async { + // If we're syncing the queued messages, advance the queue immediately + // before any potentially long async work (like translation/notifications). + if (_isSyncingQueuedMessages) { + _handleQueuedMessageReceived(); + } final parsed = ChannelMessage.fromFrame(frame); if (parsed != null && parsed.channelIndex != null) { if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) { @@ -4781,15 +4852,17 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: message.pathBytes, ); final isNew = _addChannelMessage(message.channelIndex!, message); - if (isNew && !message.isOutgoing) { - unawaited( - _translateIncomingChannelMessage(message.channelIndex!, message), - ); - } _maybeIncrementChannelUnread(message, isNew: isNew); notifyListeners(); - if (isNew) { - _maybeNotifyChannelMessage(message); + if (isNew && !message.isOutgoing) { + final msg = message; // capture for closure + unawaited(() async { + final translationResult = await translateChannelMessage( + msg.channelIndex!, + msg, + ); + _maybeNotifyChannelMessage(msg, translationResult: translationResult); + }()); } _handleQueuedMessageReceived(); } else if (_isSyncingQueuedMessages) { @@ -4797,7 +4870,7 @@ class MeshCoreConnector extends ChangeNotifier { } } - void _handleLogRxData(Uint8List frame) { + void _handleLogRxData(Uint8List frame) async { if (frame.length < 4) return; try { final reader = BufferReader(frame); @@ -4865,16 +4938,24 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: message.pathBytes, ); final isNew = _addChannelMessage(channel.index, message); - if (isNew && !message.isOutgoing) { - unawaited(_translateIncomingChannelMessage(channel.index, message)); - } _maybeIncrementChannelUnread(message, isNew: isNew); notifyListeners(); if (isNew) { - final label = channel.name.isEmpty - ? 'Channel ${channel.index}' - : channel.name; - _maybeNotifyChannelMessage(message, channelName: label); + // Run translation + notification asynchronously to avoid blocking + unawaited(() async { + final translationResult = await translateChannelMessage( + channel.index, + message, + ); + final label = channel.name.isEmpty + ? 'Channel ${channel.index}' + : channel.name; + _maybeNotifyChannelMessage( + message, + channelName: label, + translationResult: translationResult, + ); + }()); } return; } catch (e) { diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 45536cf6..c70ca78d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2099,6 +2099,9 @@ "translation_composerTitle": "Преведете преди да изпратите", "translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.", "translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.", + "translation_autoIncomingTitle": "Автоматичен превод на съобщения", + "translation_autoIncomingSubtitle": "Превежда автоматично съобщенията за известия, както и за чатове или канали.", + "translation_translateMessage": "Преведи съобщението", "translation_targetLanguage": "Целеви език", "translation_useAppLanguage": "Използвайте езика на приложението", "translation_downloadedModelLabel": "Изтегнат модел", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7f491d4f..4a9be70b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2127,6 +2127,9 @@ "translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.", "translation_enableTitle": "Aktivieren Sie die Übersetzung", "translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.", + "translation_autoIncomingTitle": "Nachrichten automatisch übersetzen", + "translation_autoIncomingSubtitle": "Übersetzt Nachrichten für Benachrichtigungen sowie für Chats oder Kanäle automatisch.", + "translation_translateMessage": "Nachricht übersetzen", "translation_targetLanguage": "Zielsprache", "translation_useAppLanguage": "Verwenden Sie die App-Sprache", "translation_downloadedModelLabel": "Heruntergeladenes Modell", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 456fab56..b3c5774a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2288,6 +2288,9 @@ "translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.", "translation_composerTitle": "Translate before sending", "translation_composerSubtitle": "Controls the default state of the composer translation icon.", + "translation_autoIncomingTitle": "Auto-translate incoming messages", + "translation_autoIncomingSubtitle": "Translates Messages for notification and for chat or channel automatically.", + "translation_translateMessage": "Translate message", "translation_targetLanguage": "Target language", "translation_useAppLanguage": "Use app language", "translation_downloadedModelLabel": "Downloaded model", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d4cf27da..b85e7652 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2128,6 +2128,9 @@ "translation_enableTitle": "Habilitar la traducción", "translation_composerTitle": "Traducir antes de enviar", "translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.", + "translation_autoIncomingTitle": "Traducir mensajes automáticamente", + "translation_autoIncomingSubtitle": "Traduce mensajes para notificaciones y para chats o canales automáticamente.", + "translation_translateMessage": "Traducir mensaje", "translation_targetLanguage": "Idioma de destino", "translation_useAppLanguage": "Utilizar el idioma de la aplicación", "translation_downloadedModelLabel": "Modelo descargado", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7817c20d..fd6b53e0 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2099,6 +2099,9 @@ "translation_title": "Traduction", "translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.", "translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.", + "translation_autoIncomingTitle": "Traduire automatiquement les messages", + "translation_autoIncomingSubtitle": "Traduit automatiquement les messages pour les notifications et pour les discussions ou les canaux.", + "translation_translateMessage": "Traduire le message", "translation_targetLanguage": "Langue cible", "translation_useAppLanguage": "Utiliser la langue de l'application", "translation_downloadedModelLabel": "Modèle téléchargé", diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 97426183..d41c62f2 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2137,6 +2137,9 @@ "translation_enableSubtitle": "Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.", "translation_composerTitle": "Fordítsa el, mielőtt elküldi", "translation_composerSubtitle": "Ellenőrzi a zeneszerző fordítási ikon alapértékét.", + "translation_autoIncomingTitle": "Üzenetek automatikus fordítása", + "translation_autoIncomingSubtitle": "Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.", + "translation_translateMessage": "Üzenet fordítása", "translation_targetLanguage": "Célnyelv", "translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.", "translation_downloadedModelLabel": "Letöltött modell", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 664bceef..e60cfc4a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2100,6 +2100,9 @@ "translation_enableTitle": "Abilitare la traduzione", "translation_title": "Traduzione", "translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.", + "translation_autoIncomingTitle": "Traduci automaticamente i messaggi", + "translation_autoIncomingSubtitle": "Traduce automaticamente i messaggi per le notifiche e per le chat o i canali.", + "translation_translateMessage": "Traduci messaggio", "translation_targetLanguage": "Lingua di destinazione", "translation_useAppLanguage": "Utilizza la lingua dell'app", "translation_downloadedModelLabel": "Modello scaricato", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 9d2cef21..c68fc98d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2137,6 +2137,9 @@ "translation_composerTitle": "送信する前に翻訳する", "translation_enableTitle": "翻訳機能を有効にする", "translation_composerSubtitle": "作曲家翻訳アイコンのデフォルト状態を制御する。", + "translation_autoIncomingTitle": "メッセージを自動翻訳", + "translation_autoIncomingSubtitle": "通知やチャット、チャンネルのメッセージを自動的に翻訳します。", + "translation_translateMessage": "メッセージを翻訳", "translation_targetLanguage": "翻訳対象言語", "translation_useAppLanguage": "アプリの言語設定", "translation_downloadedModelLabel": "ダウンロードしたモデル", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index d778429b..7418068c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2137,6 +2137,9 @@ "translation_enableTitle": "번역 기능 활성화", "translation_composerTitle": "보내기 전에 번역", "translation_composerSubtitle": "컴포저 번역 아이콘의 기본 상태를 제어합니다.", + "translation_autoIncomingTitle": "메시지 자동 번역", + "translation_autoIncomingSubtitle": "알림과 채팅 또는 채널의 메시지를 자동으로 번역합니다.", + "translation_translateMessage": "메시지 번역", "translation_targetLanguage": "목표 언어", "translation_useAppLanguage": "앱 언어 사용", "translation_downloadedModelLabel": "다운로드한 모델", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 159d755c..b8c6558c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -7114,6 +7114,24 @@ abstract class AppLocalizations { /// **'Controls the default state of the composer translation icon.'** String get translation_composerSubtitle; + /// No description provided for @translation_autoIncomingTitle. + /// + /// In en, this message translates to: + /// **'Auto-translate incoming messages'** + String get translation_autoIncomingTitle; + + /// No description provided for @translation_autoIncomingSubtitle. + /// + /// In en, this message translates to: + /// **'Translates Messages for notification and for chat or channel automatically.'** + String get translation_autoIncomingSubtitle; + + /// No description provided for @translation_translateMessage. + /// + /// In en, this message translates to: + /// **'Translate message'** + String get translation_translateMessage; + /// No description provided for @translation_targetLanguage. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 409803b2..19883f16 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -4161,6 +4161,16 @@ class AppLocalizationsBg extends AppLocalizations { String get translation_composerSubtitle => 'Контролира началния статус на иконата за превод, създадена от композитора.'; + @override + String get translation_autoIncomingTitle => 'Автоматичен превод на съобщения'; + + @override + String get translation_autoIncomingSubtitle => + 'Превежда автоматично съобщенията за известия, както и за чатове или канали.'; + + @override + String get translation_translateMessage => 'Преведи съобщението'; + @override String get translation_targetLanguage => 'Целеви език'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9f06906d..20d64c39 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4176,6 +4176,17 @@ class AppLocalizationsDe extends AppLocalizations { String get translation_composerSubtitle => 'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.'; + @override + String get translation_autoIncomingTitle => + 'Nachrichten automatisch übersetzen'; + + @override + String get translation_autoIncomingSubtitle => + 'Übersetzt Nachrichten für Benachrichtigungen sowie für Chats oder Kanäle automatisch.'; + + @override + String get translation_translateMessage => 'Nachricht übersetzen'; + @override String get translation_targetLanguage => 'Zielsprache'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6def06b7..5da877e1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4085,6 +4085,17 @@ class AppLocalizationsEn extends AppLocalizations { String get translation_composerSubtitle => 'Controls the default state of the composer translation icon.'; + @override + String get translation_autoIncomingTitle => + 'Auto-translate incoming messages'; + + @override + String get translation_autoIncomingSubtitle => + 'Translates Messages for notification and for chat or channel automatically.'; + + @override + String get translation_translateMessage => 'Translate message'; + @override String get translation_targetLanguage => 'Target language'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index bb4f40e8..f55bb1c7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4163,6 +4163,17 @@ class AppLocalizationsEs extends AppLocalizations { String get translation_composerSubtitle => 'Controla el estado predeterminado del icono de traducción del compositor.'; + @override + String get translation_autoIncomingTitle => + 'Traducir mensajes automáticamente'; + + @override + String get translation_autoIncomingSubtitle => + 'Traduce mensajes para notificaciones y para chats o canales automáticamente.'; + + @override + String get translation_translateMessage => 'Traducir mensaje'; + @override String get translation_targetLanguage => 'Idioma de destino'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 44e40657..a1770dc3 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4193,6 +4193,17 @@ class AppLocalizationsFr extends AppLocalizations { String get translation_composerSubtitle => 'Contrôle l\'état par défaut de l\'icône de traduction du composant.'; + @override + String get translation_autoIncomingTitle => + 'Traduire automatiquement les messages'; + + @override + String get translation_autoIncomingSubtitle => + 'Traduit automatiquement les messages pour les notifications et pour les discussions ou les canaux.'; + + @override + String get translation_translateMessage => 'Traduire le message'; + @override String get translation_targetLanguage => 'Langue cible'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 298a6fd6..ee41febf 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -4181,6 +4181,16 @@ class AppLocalizationsHu extends AppLocalizations { String get translation_composerSubtitle => 'Ellenőrzi a zeneszerző fordítási ikon alapértékét.'; + @override + String get translation_autoIncomingTitle => 'Üzenetek automatikus fordítása'; + + @override + String get translation_autoIncomingSubtitle => + 'Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.'; + + @override + String get translation_translateMessage => 'Üzenet fordítása'; + @override String get translation_targetLanguage => 'Célnyelv'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e3f7ee58..55804809 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4169,6 +4169,17 @@ class AppLocalizationsIt extends AppLocalizations { String get translation_composerSubtitle => 'Controlla lo stato predefinito dell\'icona di traduzione del compositore.'; + @override + String get translation_autoIncomingTitle => + 'Traduci automaticamente i messaggi'; + + @override + String get translation_autoIncomingSubtitle => + 'Traduce automaticamente i messaggi per le notifiche e per le chat o i canali.'; + + @override + String get translation_translateMessage => 'Traduci messaggio'; + @override String get translation_targetLanguage => 'Lingua di destinazione'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 263a906e..844f3b66 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3941,6 +3941,16 @@ class AppLocalizationsJa extends AppLocalizations { @override String get translation_composerSubtitle => '作曲家翻訳アイコンのデフォルト状態を制御する。'; + @override + String get translation_autoIncomingTitle => 'メッセージを自動翻訳'; + + @override + String get translation_autoIncomingSubtitle => + '通知やチャット、チャンネルのメッセージを自動的に翻訳します。'; + + @override + String get translation_translateMessage => 'メッセージを翻訳'; + @override String get translation_targetLanguage => '翻訳対象言語'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index f9c0d760..1bc02940 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3942,6 +3942,16 @@ class AppLocalizationsKo extends AppLocalizations { @override String get translation_composerSubtitle => '컴포저 번역 아이콘의 기본 상태를 제어합니다.'; + @override + String get translation_autoIncomingTitle => '메시지 자동 번역'; + + @override + String get translation_autoIncomingSubtitle => + '알림과 채팅 또는 채널의 메시지를 자동으로 번역합니다.'; + + @override + String get translation_translateMessage => '메시지 번역'; + @override String get translation_targetLanguage => '목표 언어'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ad140bf3..cf78b5e7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4146,6 +4146,16 @@ class AppLocalizationsNl extends AppLocalizations { String get translation_composerSubtitle => 'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.'; + @override + String get translation_autoIncomingTitle => 'Berichten automatisch vertalen'; + + @override + String get translation_autoIncomingSubtitle => + 'Vertaalt berichten automatisch voor meldingen en voor chats of kanalen.'; + + @override + String get translation_translateMessage => 'Bericht vertalen'; + @override String get translation_targetLanguage => 'Doeltaal'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 6c7c3696..e4b33f9f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -4184,6 +4184,17 @@ class AppLocalizationsPl extends AppLocalizations { String get translation_composerSubtitle => 'Kontroluje domyślny stan ikony tłumaczenia w edytorze.'; + @override + String get translation_autoIncomingTitle => + 'Automatycznie tłumacz wiadomości'; + + @override + String get translation_autoIncomingSubtitle => + 'Automatycznie tłumaczy wiadomości do powiadomień oraz do czatów lub kanałów.'; + + @override + String get translation_translateMessage => 'Przetłumacz wiadomość'; + @override String get translation_targetLanguage => 'Język docelowy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e6fd49e3..f0ad632b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -4159,6 +4159,17 @@ class AppLocalizationsPt extends AppLocalizations { String get translation_composerSubtitle => 'Controla o estado padrão do ícone de tradução do compositor.'; + @override + String get translation_autoIncomingTitle => + 'Traduzir mensagens automaticamente'; + + @override + String get translation_autoIncomingSubtitle => + 'Traduz automaticamente mensagens para notificações e para chats ou canais.'; + + @override + String get translation_translateMessage => 'Traduzir mensagem'; + @override String get translation_targetLanguage => 'Língua-alvo'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 25452003..46ef46b6 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -4177,6 +4177,17 @@ class AppLocalizationsRu extends AppLocalizations { String get translation_composerSubtitle => 'Управляет исходным состоянием значка перевода, предоставляемого редактором.'; + @override + String get translation_autoIncomingTitle => + 'Автоматически переводить сообщения'; + + @override + String get translation_autoIncomingSubtitle => + 'Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.'; + + @override + String get translation_translateMessage => 'Перевести сообщение'; + @override String get translation_targetLanguage => 'Целевой язык'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 91461385..29a0b8a9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -4141,6 +4141,16 @@ class AppLocalizationsSk extends AppLocalizations { String get translation_composerSubtitle => 'Riadi výchoce stav ikony pre preklad, ktorú používa program.'; + @override + String get translation_autoIncomingTitle => 'Automaticky prekladať správy'; + + @override + String get translation_autoIncomingSubtitle => + 'Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.'; + + @override + String get translation_translateMessage => 'Preložiť správu'; + @override String get translation_targetLanguage => 'Cieľový jazyk'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 48103647..125aa604 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -4139,6 +4139,16 @@ class AppLocalizationsSl extends AppLocalizations { String get translation_composerSubtitle => 'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.'; + @override + String get translation_autoIncomingTitle => 'Samodejno prevajaj sporočila'; + + @override + String get translation_autoIncomingSubtitle => + 'Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.'; + + @override + String get translation_translateMessage => 'Prevedi sporočilo'; + @override String get translation_targetLanguage => 'Ciljna jezika'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 7453c18f..4bf43719 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -4113,6 +4113,17 @@ class AppLocalizationsSv extends AppLocalizations { String get translation_composerSubtitle => 'Styr standardtillståndet för kompositorns översättningsikon.'; + @override + String get translation_autoIncomingTitle => + 'Översätt meddelanden automatiskt'; + + @override + String get translation_autoIncomingSubtitle => + 'Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.'; + + @override + String get translation_translateMessage => 'Översätt meddelande'; + @override String get translation_targetLanguage => 'Målmedvetet språk'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9c30fd8e..58f381cf 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -4176,6 +4176,17 @@ class AppLocalizationsUk extends AppLocalizations { String get translation_composerSubtitle => 'Контролює стан ікон перекладу, який використовується за замовчуванням.'; + @override + String get translation_autoIncomingTitle => + 'Автоматично перекладати повідомлення'; + + @override + String get translation_autoIncomingSubtitle => + 'Автоматично перекладає повідомлення для сповіщень, а також для чатів і каналів.'; + + @override + String get translation_translateMessage => 'Перекласти повідомлення'; + @override String get translation_targetLanguage => 'Цільова мова'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d2304a66..ec03b8e8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3816,6 +3816,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。'; + @override + String get translation_autoIncomingTitle => '自动翻译消息'; + + @override + String get translation_autoIncomingSubtitle => '自动为通知以及聊天或频道翻译消息。'; + + @override + String get translation_translateMessage => '翻译消息'; + @override String get translation_targetLanguage => '目标语言'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7e83215b..68d53148 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2101,6 +2101,9 @@ "translation_composerTitle": "Vertaal voor verzending", "translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.", "translation_useAppLanguage": "Gebruik de taal van de app", + "translation_autoIncomingTitle": "Berichten automatisch vertalen", + "translation_autoIncomingSubtitle": "Vertaalt berichten automatisch voor meldingen en voor chats of kanalen.", + "translation_translateMessage": "Bericht vertalen", "translation_targetLanguage": "Doeltaal", "translation_downloadedModelLabel": "Gedownloade model", "translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 4366e273..f79a589a 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2137,6 +2137,9 @@ "translation_enableTitle": "Włącz tłumaczenie", "translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.", "translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.", + "translation_autoIncomingTitle": "Automatycznie tłumacz wiadomości", + "translation_autoIncomingSubtitle": "Automatycznie tłumaczy wiadomości do powiadomień oraz do czatów lub kanałów.", + "translation_translateMessage": "Przetłumacz wiadomość", "translation_targetLanguage": "Język docelowy", "translation_useAppLanguage": "Użyj języka aplikacji", "translation_downloadedModelLabel": "Pobudowany model", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6b8abbbc..994ff8a2 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2100,6 +2100,9 @@ "translation_enableTitle": "Ativar a tradução", "translation_title": "Tradução", "translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.", + "translation_autoIncomingTitle": "Traduzir mensagens automaticamente", + "translation_autoIncomingSubtitle": "Traduz automaticamente mensagens para notificações e para chats ou canais.", + "translation_translateMessage": "Traduzir mensagem", "translation_targetLanguage": "Língua-alvo", "translation_useAppLanguage": "Utilize o idioma da aplicação", "translation_downloadedModelLabel": "Modelo baixado", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 5d5bcfc3..2b0978a6 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1268,6 +1268,9 @@ "translation_title": "Перевод", "translation_enableTitle": "Включить перевод", "translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.", + "translation_autoIncomingTitle": "Автоматически переводить сообщения", + "translation_autoIncomingSubtitle": "Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.", + "translation_translateMessage": "Перевести сообщение", "translation_targetLanguage": "Целевой язык", "translation_useAppLanguage": "Используйте язык приложения", "translation_downloadedModelLabel": "Загруженная модель", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 713c3976..1a65b35f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2100,6 +2100,9 @@ "translation_composerTitle": "Preložte pred odeslaním", "translation_title": "Preklad", "translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.", + "translation_autoIncomingTitle": "Automaticky prekladať správy", + "translation_autoIncomingSubtitle": "Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.", + "translation_translateMessage": "Preložiť správu", "translation_targetLanguage": "Cieľový jazyk", "translation_useAppLanguage": "Použite jazyk aplikácie", "translation_downloadedModelLabel": "Stiahnutý model", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 00930c85..eff2db17 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2099,6 +2099,9 @@ "translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.", "translation_enableTitle": "Omogočite prevod", "translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.", + "translation_autoIncomingTitle": "Samodejno prevajaj sporočila", + "translation_autoIncomingSubtitle": "Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.", + "translation_translateMessage": "Prevedi sporočilo", "translation_targetLanguage": "Ciljna jezika", "translation_useAppLanguage": "Uporabite jezik aplikacije", "translation_downloadedModelLabel": "Naložen model", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 00fd3b08..044abbc7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2100,6 +2100,9 @@ "translation_title": "Översättning", "translation_composerTitle": "Översätt innan du skickar", "translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.", + "translation_autoIncomingTitle": "Översätt meddelanden automatiskt", + "translation_autoIncomingSubtitle": "Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.", + "translation_translateMessage": "Översätt meddelande", "translation_targetLanguage": "Målmedvetet språk", "translation_useAppLanguage": "Använd appens språk", "translation_downloadedModelLabel": "Nedladdad modell", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 8330d364..37300095 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2109,6 +2109,9 @@ "translation_enableTitle": "Увімкнути переклад", "translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.", "translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.", + "translation_autoIncomingTitle": "Автоматично перекладати повідомлення", + "translation_autoIncomingSubtitle": "Автоматично перекладає повідомлення для сповіщень, а також для чатів і каналів.", + "translation_translateMessage": "Перекласти повідомлення", "translation_targetLanguage": "Цільова мова", "translation_useAppLanguage": "Використовувати мову застосунку", "translation_downloadedModelLabel": "Завантажений шаблон", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1e289315..5603034f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2105,6 +2105,9 @@ "translation_composerTitle": "在发送之前进行翻译", "translation_enableTitle": "启用翻译功能", "translation_composerSubtitle": "控制作曲家翻译图标的默认状态。", + "translation_autoIncomingTitle": "自动翻译消息", + "translation_autoIncomingSubtitle": "自动为通知以及聊天或频道翻译消息。", + "translation_translateMessage": "翻译消息", "translation_targetLanguage": "目标语言", "translation_useAppLanguage": "使用应用程序语言", "translation_downloadedModelLabel": "下载的模型", diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 4e95311f..6995b6ed 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -113,6 +113,7 @@ class AppSettings { final int tcpServerPort; final bool jumpToOldestUnread; final bool translationEnabled; + final bool autoTranslateIncomingMessages; final String? translationTargetLanguageCode; final bool composerTranslationEnabled; final String? translationModelSourceUrl; @@ -166,6 +167,7 @@ class AppSettings { this.tcpServerPort = 0, this.jumpToOldestUnread = false, this.translationEnabled = false, + this.autoTranslateIncomingMessages = true, this.translationTargetLanguageCode, this.composerTranslationEnabled = false, this.translationModelSourceUrl, @@ -226,6 +228,7 @@ class AppSettings { 'tcp_server_port': tcpServerPort, 'jump_to_oldest_unread': jumpToOldestUnread, 'translation_enabled': translationEnabled, + 'auto_translate_incoming_messages': autoTranslateIncomingMessages, 'translation_target_language_code': translationTargetLanguageCode, 'composer_translation_enabled': composerTranslationEnabled, 'translation_model_source_url': translationModelSourceUrl, @@ -307,6 +310,8 @@ class AppSettings { tcpServerPort: json['tcp_server_port'] as int? ?? 0, jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false, translationEnabled: json['translation_enabled'] as bool? ?? false, + autoTranslateIncomingMessages: + json['auto_translate_incoming_messages'] as bool? ?? true, translationTargetLanguageCode: json['translation_target_language_code'] as String?, composerTranslationEnabled: @@ -396,6 +401,7 @@ class AppSettings { int? tcpServerPort, bool? jumpToOldestUnread, bool? translationEnabled, + bool? autoTranslateIncomingMessages, Object? translationTargetLanguageCode = _unset, bool? composerTranslationEnabled, Object? translationModelSourceUrl = _unset, @@ -453,6 +459,8 @@ class AppSettings { tcpServerPort: tcpServerPort ?? this.tcpServerPort, jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread, translationEnabled: translationEnabled ?? this.translationEnabled, + autoTranslateIncomingMessages: + autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages, translationTargetLanguageCode: translationTargetLanguageCode == _unset ? this.translationTargetLanguageCode : translationTargetLanguageCode as String?, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 94b3efe8..4d791381 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -559,6 +559,7 @@ class AppSettingsScreen extends StatelessWidget { TranslationService translationService, ) { final settings = settingsService.settings; + final translationEnabled = settings.translationEnabled; return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -579,11 +580,41 @@ class AppSettingsScreen extends StatelessWidget { ), const Divider(height: 1), SwitchListTile( - secondary: const Icon(Icons.outgoing_mail), - title: Text(context.l10n.translation_composerTitle), - subtitle: Text(context.l10n.translation_composerSubtitle), + secondary: Icon( + Icons.auto_awesome_outlined, + color: translationEnabled ? null : Colors.grey, + ), + title: Text( + context.l10n.translation_autoIncomingTitle, + style: TextStyle(color: translationEnabled ? null : Colors.grey), + ), + subtitle: Text( + context.l10n.translation_autoIncomingSubtitle, + style: TextStyle(color: translationEnabled ? null : Colors.grey), + ), + value: settings.autoTranslateIncomingMessages, + onChanged: translationEnabled + ? settingsService.setAutoTranslateIncomingMessages + : null, + ), + const Divider(height: 1), + SwitchListTile( + secondary: Icon( + Icons.outgoing_mail, + color: translationEnabled ? null : Colors.grey, + ), + title: Text( + context.l10n.translation_composerTitle, + style: TextStyle(color: translationEnabled ? null : Colors.grey), + ), + subtitle: Text( + context.l10n.translation_composerSubtitle, + style: TextStyle(color: translationEnabled ? null : Colors.grey), + ), value: settings.composerTranslationEnabled, - onChanged: settingsService.setComposerTranslationEnabled, + onChanged: translationEnabled + ? settingsService.setComposerTranslationEnabled + : null, ), const Divider(height: 1), ListTile( diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index be72eaa8..f34bab04 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; @@ -1386,6 +1387,15 @@ class _ChannelChatScreenState extends State { } void _showMessageActions(ChannelMessage message) { + final translationService = context.read(); + final canTranslateMessage = + translationService.canTranslateIncoming( + text: message.text, + isCli: false, + isOutgoing: message.isOutgoing, + ) && + (message.translatedText?.trim().isEmpty ?? true); + showModalBottomSheet( context: context, builder: (sheetContext) => SafeArea( @@ -1427,6 +1437,21 @@ class _ChannelChatScreenState extends State { _copyMessageText(message.text); }, ), + if (canTranslateMessage) + ListTile( + leading: const Icon(Icons.translate), + title: Text(context.l10n.translation_translateMessage), + onTap: () { + Navigator.pop(sheetContext); + unawaited( + context.read().translateChannelMessage( + widget.channel.index, + message, + manualTranslation: true, + ), + ); + }, + ), if (!message.isOutgoing) ListTile( leading: const Icon(Icons.mark_chat_unread_outlined), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 8b05b8ce..0737a7d0 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -273,7 +273,7 @@ class _ChannelsScreenState extends State ), buildDefaultDragHandles: false, itemCount: filteredChannels.length, - onReorder: (oldIndex, newIndex) { + onReorderItem: (oldIndex, newIndex) { if (newIndex > oldIndex) newIndex -= 1; final reordered = List.from( filteredChannels, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 1548b44e..7c71d597 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1578,6 +1578,15 @@ class _ChatScreenState extends State { } void _showMessageActions(Message message, Contact contact) { + final translationService = context.read(); + final canTranslateMessage = + translationService.canTranslateIncoming( + text: message.text, + isCli: message.isCli, + isOutgoing: message.isOutgoing, + ) && + (message.translatedText?.trim().isEmpty ?? true); + showModalBottomSheet( context: context, builder: (sheetContext) => SafeArea( @@ -1611,6 +1620,21 @@ class _ChatScreenState extends State { _copyMessageText(message.text); }, ), + if (canTranslateMessage) + ListTile( + leading: const Icon(Icons.translate), + title: Text(context.l10n.translation_translateMessage), + onTap: () { + Navigator.pop(sheetContext); + unawaited( + context.read().translateContactMessage( + widget.contact.publicKeyHex, + message, + manualTranslation: true, + ), + ); + }, + ), if (!message.isOutgoing) ListTile( leading: const Icon(Icons.mark_chat_unread_outlined), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 7b3d5848..88ff0f47 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -235,6 +235,12 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(translationEnabled: value)); } + Future setAutoTranslateIncomingMessages(bool value) async { + await updateSettings( + _settings.copyWith(autoTranslateIncomingMessages: value), + ); + } + Future setTranslationTargetLanguageCode(String? value) async { await updateSettings( _settings.copyWith(translationTargetLanguageCode: value), diff --git a/lib/services/translation_service.dart b/lib/services/translation_service.dart index 294a7848..7b1d7f5f 100644 --- a/lib/services/translation_service.dart +++ b/lib/services/translation_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:llamadart/llamadart.dart'; +import 'package:flutter_langdetect/flutter_langdetect.dart'; import '../models/app_settings.dart'; import '../models/translation_support.dart'; @@ -41,7 +42,10 @@ class TranslationService extends ChangeNotifier { TranslationService( this._appSettingsService, { TranslationFileStore? fileStore, - }) : _fileStore = fileStore ?? TranslationFileStore(); + }) : _fileStore = fileStore ?? TranslationFileStore() { + // Initialize langdetect once at service construction. + _langDetectInit = initLangDetect(); + } bool _isBusy = false; bool _isDownloading = false; @@ -51,6 +55,7 @@ class TranslationService extends ChangeNotifier { LlamaEngine? _engine; String? _loadedModelPath; String? _failedModelPath; + Future? _langDetectInit; int _downloadedBytes = 0; int? _downloadTotalBytes; String? _downloadFileName; @@ -84,7 +89,22 @@ class TranslationService extends ChangeNotifier { 'en'; } - bool shouldTranslateIncoming({ + bool shouldAutoTranslateIncoming({ + required String text, + required bool isCli, + required bool isOutgoing, + }) { + if (!_settings.autoTranslateIncomingMessages) { + return false; + } + return canTranslateIncoming( + text: text, + isCli: isCli, + isOutgoing: isOutgoing, + ); + } + + bool canTranslateIncoming({ required String text, required bool isCli, required bool isOutgoing, @@ -368,7 +388,9 @@ class TranslationService extends ChangeNotifier { if (targetLanguageCode == null || !_isPlainTextEligible(text)) { return null; } - final detectedLanguageCode = await detectLanguage(text); + final detectedLanguageCode = await detectLanguage( + _stripReplyInfoForDetection(text), + ); if (detectedLanguageCode != null && detectedLanguageCode == targetLanguageCode) { return const TranslationResult( @@ -409,7 +431,9 @@ class TranslationService extends ChangeNotifier { if (targetLanguageCode == null || !_isPlainTextEligible(text)) { return null; } - final detectedLanguageCode = await detectLanguage(text); + final detectedLanguageCode = await detectLanguage( + _stripReplyInfoForDetection(text), + ); if (detectedLanguageCode != null && detectedLanguageCode == targetLanguageCode) { return const TranslationResult( @@ -436,7 +460,26 @@ class TranslationService extends ChangeNotifier { } Future detectLanguage(String text) async { - return _heuristicLanguageCode(text); + try { + // Ensure the detector is initialized (constructor starts init). + await (_langDetectInit ??= initLangDetect()); + final code = detect(text); + if (code.isEmpty) return null; + return code; + } catch (error) { + _lastError = error.toString(); + appLogger.warn('Language detection failed: $error'); + notifyListeners(); + return null; + } + } + + String _stripReplyInfoForDetection(String text) { + final match = RegExp( + r'@\[([^\]]+)\]\s+(.+)$', + dotAll: true, + ).firstMatch(text); + return match?.group(2) ?? text; } Future _translateText({ @@ -518,72 +561,6 @@ class TranslationService extends ChangeNotifier { trimmed.startsWith('r:')); } - String? _heuristicLanguageCode(String text) { - final trimmed = text.trim(); - if (trimmed.isEmpty) { - return null; - } - - if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) { - return 'ja'; - } - if (RegExp(r'[가-힣]').hasMatch(text)) { - return 'ko'; - } - if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) { - return 'zh'; - } - - final lower = trimmed.toLowerCase(); - final patterns = { - 'uk': r'\b(привіт|дякую|будь|ласка|як|де|не|так|це|є|най|ще|може|для)\b', - 'ru': - r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b', - 'bg': r'\b(ще|няма|благодаря|моля|това|какво|тук|ние|вие|не|със|за)\b', - 'de': - r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b', - 'en': - r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b', - 'es': - r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b', - 'fr': - r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b', - 'it': - r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b', - 'pt': - r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b', - 'nl': - r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b', - 'sv': - r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b', - 'pl': - r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b', - 'sk': r'\b(je|na|so|že|do|od|za|si|to|ten|tá|tí|ako|má|nie|som|sa)\b', - 'sl': r'\b(in|je|na|se|da|za|od|ne|to|ta|so|kako|bo|sem|si)\b', - 'hu': - r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b', - }; - - final scores = {}; - for (final entry in patterns.entries) { - scores[entry.key] = RegExp( - entry.value, - caseSensitive: false, - ).allMatches(lower).length; - } - - final sorted = scores.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); - if (sorted.isEmpty || sorted.first.value == 0) { - return null; - } - if (sorted.length > 1 && sorted.first.value == sorted[1].value) { - return null; - } - - return sorted.first.key; - } - String _languageLabel(String code) { for (final option in supportedTranslationLanguages) { if (option.code == code) { diff --git a/pubspec.yaml b/pubspec.yaml index 7b43dded..72cca35f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,7 @@ dependencies: ml_algo: ^16.0.0 ml_dataframe: ^1.0.0 llamadart: '>=0.6.8 <0.7.0' + flutter_langdetect: ^0.0.1 hooks: user_defines: