feat: add message translation support

- Introduced translation functionality in chat screen, allowing users to translate messages before sending.
- Added MessageTranslationButton to the input bar for enabling/disabling translation.
- Implemented translation service to handle incoming and outgoing text translations using llama models.
- Enhanced message storage to include original and translated text, language codes, and translation status.
- Created UI components for displaying translated messages and managing translation options.
- Added translation model management, including downloading and storing models locally.
- Updated app settings to manage translation preferences and model selections.
This commit is contained in:
zjs81 2026-04-02 19:09:17 -07:00
parent 82adbd761b
commit 9bf649e2c6
57 changed files with 4879 additions and 184 deletions

View file

@ -14,6 +14,7 @@ import '../models/companion_radio_stats.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_selection.dart';
import '../models/translation_support.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import '../services/app_debug_log_service.dart';
@ -26,6 +27,7 @@ import '../services/path_history_service.dart';
import '../services/app_settings_service.dart';
import '../services/background_service.dart';
import '../services/timeout_prediction_service.dart';
import '../services/translation_service.dart';
import '../services/notification_service.dart';
import 'meshcore_connector_usb.dart';
import 'meshcore_connector_tcp.dart';
@ -180,6 +182,7 @@ class MeshCoreConnector extends ChangeNotifier {
bool _isLoadingChannels = false;
bool _hasLoadedChannels = false;
TimeoutPredictionService? _timeoutPredictionService;
TranslationService? _translationService;
// Intentionally global (not per-contact): tracks overall network activity.
// Frequent RX from any source indicates a busy network with more collisions.
DateTime _lastRxTime = DateTime.now();
@ -482,7 +485,7 @@ class MeshCoreConnector extends ChangeNotifier {
String _messageMergeKey(Message message) {
final messageId = message.messageId;
if (messageId != null && messageId.isNotEmpty) {
if (messageId.isNotEmpty) {
return 'id:$messageId';
}
return 'fallback:${message.senderKeyHex}:${message.isOutgoing}:${message.isCli}:${message.timestamp.millisecondsSinceEpoch}:${message.text}';
@ -721,6 +724,7 @@ class MeshCoreConnector extends ChangeNotifier {
required MessageRetryService retryService,
required PathHistoryService pathHistoryService,
AppSettingsService? appSettingsService,
TranslationService? translationService,
BleDebugLogService? bleDebugLogService,
AppDebugLogService? appDebugLogService,
BackgroundService? backgroundService,
@ -729,6 +733,7 @@ class MeshCoreConnector extends ChangeNotifier {
_retryService = retryService;
_pathHistoryService = pathHistoryService;
_appSettingsService = appSettingsService;
_translationService = translationService;
_bleDebugLogService = bleDebugLogService;
_appDebugLogService = appDebugLogService;
_backgroundService = backgroundService;
@ -952,6 +957,126 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> _translateIncomingContactMessage(
String contactKeyHex,
Message message,
) async {
try {
final service = _translationService;
if (service == null ||
!service.shouldTranslateIncoming(
text: message.text,
isCli: message.isCli,
isOutgoing: message.isOutgoing,
)) {
return;
}
final targetLanguageCode = service.resolvedIncomingLanguageCode(
_appSettingsService?.settings.languageOverride,
);
final result = await service.translateIncomingText(
text: message.text,
targetLanguageCode: targetLanguageCode,
);
if (result == null) {
return;
}
final translated = result.status == MessageTranslationStatus.completed
? result.translatedText
: null;
_updateStoredContactMessage(
contactKeyHex,
message.messageId,
(current) => current.copyWith(
translatedText: translated,
translatedLanguageCode: result.detectedLanguageCode,
translationStatus: result.status,
translationModelId: result.modelId,
),
);
} catch (error) {
appLogger.warn('Translation failed for contact message: $error');
}
}
Future<void> _translateIncomingChannelMessage(
int channelIndex,
ChannelMessage message,
) async {
try {
final service = _translationService;
if (service == null ||
!service.shouldTranslateIncoming(
text: message.text,
isCli: false,
isOutgoing: message.isOutgoing,
)) {
return;
}
final targetLanguageCode = service.resolvedIncomingLanguageCode(
_appSettingsService?.settings.languageOverride,
);
final result = await service.translateIncomingText(
text: message.text,
targetLanguageCode: targetLanguageCode,
);
if (result == null) {
return;
}
final translated = result.status == MessageTranslationStatus.completed
? result.translatedText
: null;
_updateStoredChannelMessage(
channelIndex,
message.messageId,
(current) => current.copyWith(
translatedText: translated,
translatedLanguageCode: result.detectedLanguageCode,
translationStatus: result.status,
translationModelId: result.modelId,
),
);
} catch (error) {
appLogger.warn('Translation failed for channel message: $error');
}
}
void _updateStoredContactMessage(
String contactKeyHex,
String messageId,
Message Function(Message current) update,
) {
final messages = _conversations[contactKeyHex];
if (messages == null) {
return;
}
final index = messages.indexWhere((entry) => entry.messageId == messageId);
if (index < 0) {
return;
}
messages[index] = update(messages[index]);
_messageStore.saveMessages(contactKeyHex, messages);
notifyListeners();
}
void _updateStoredChannelMessage(
int channelIndex,
String messageId,
ChannelMessage Function(ChannelMessage current) update,
) {
final messages = _channelMessages[channelIndex];
if (messages == null) {
return;
}
final index = messages.indexWhere((entry) => entry.messageId == messageId);
if (index < 0) {
return;
}
messages[index] = update(messages[index]);
_channelMessageStore.saveChannelMessages(channelIndex, messages);
notifyListeners();
}
void _recordPathResult(
String contactPubKeyHex,
PathSelection selection,
@ -2116,6 +2241,7 @@ class MeshCoreConnector extends ChangeNotifier {
_channelSyncTimeout?.cancel();
_channelSyncTimeout = null;
_channelSyncRetries = 0;
await _translationService?.releaseModel();
if (!skipBleDeviceDisconnect) {
try {
@ -2395,7 +2521,13 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildGetContactByKeyFrame(pubKey));
}
Future<void> sendMessage(Contact contact, String text) async {
Future<void> sendMessage(
Contact contact,
String text, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
}) async {
if (!isConnected || text.isEmpty) return;
// Check if this is a reaction - apply locally with pending status and route through retry service
@ -2426,7 +2558,13 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (_retryService != null) {
await _retryService!.sendMessageWithRetry(contact: contact, text: text);
await _retryService!.sendMessageWithRetry(
contact: contact,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
} else {
// Fallback to old behavior if retry service not initialized
final resolved = resolvePathSelection(contact);
@ -2435,6 +2573,9 @@ class MeshCoreConnector extends ChangeNotifier {
text,
pathLength: resolved.useFlood ? -1 : resolved.hopCount,
pathBytes: Uint8List.fromList(resolved.pathBytes),
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
_addMessage(contact.publicKeyHex, message);
notifyListeners();
@ -2740,7 +2881,13 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> sendChannelMessage(Channel channel, String text) async {
Future<void> sendChannelMessage(
Channel channel,
String text, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
}) async {
if (!isConnected || text.isEmpty) return;
// Check if this is a reaction - if so, process it immediately instead of adding as a message
@ -2787,6 +2934,9 @@ class MeshCoreConnector extends ChangeNotifier {
text,
_selfName ?? 'Me',
channel.index,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
_addChannelMessage(channel.index, message);
_pendingChannelSentQueue.add(message.messageId);
@ -4061,6 +4211,11 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
_addMessage(message.senderKeyHex, message);
if (!message.isOutgoing) {
unawaited(
_translateIncomingContactMessage(message.senderKeyHex, message),
);
}
_maybeIncrementContactUnread(message);
notifyListeners();
@ -4283,6 +4438,11 @@ 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) {
@ -4362,6 +4522,9 @@ 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) {
@ -5056,6 +5219,11 @@ class MeshCoreConnector extends ChangeNotifier {
senderKey: message.senderKey,
senderName: message.senderName,
text: replyInfo.actualMessage,
originalText: message.originalText,
translatedText: message.translatedText,
translatedLanguageCode: message.translatedLanguageCode,
translationStatus: message.translationStatus,
translationModelId: message.translationModelId,
timestamp: message.timestamp,
isOutgoing: message.isOutgoing,
status: message.status,

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Извличане на данни за радиото…",
"radioStats_settingsTile": "Статистически данни за радиостанции",
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableTitle": "Активирайте превода",
"translation_title": "Превод",
"translation_composerTitle": "Преведете преди да изпратите",
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
"translation_targetLanguage": "Целеви език",
"translation_useAppLanguage": "Използвайте езика на приложението",
"translation_downloadedModelLabel": "Изтегнат модел",
"translation_presetModelLabel": "Предварително конфигуриран модел от Hugging Face",
"translation_manualUrlLabel": "URL на ръководството",
"translation_downloadModel": "Изтеглете модела",
"translation_downloading": "Изтегляне...",
"translation_working": "Работа...",
"translation_stop": "Спрете",
"translation_mergingChunks": "Съединяване на изтеглените части в един файл...",
"translation_downloadedModels": "Изтеглени модели",
"translation_deleteModel": "Изтриване на модела",
"translation_modelDownloaded": "Моделът за превод е изтеглен.",
"translation_downloadStopped": "Изтеглянето беше прекъснато.",
"translation_downloadFailed": "Не успях да изтегля: {error}",
"translation_enterUrlFirst": "Въведете първо URL адрес на модела.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingHidePin": "Скрий ПИН",
"scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth",
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).",
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
"scanner_linuxPairingShowPin": "Покажи PIN",
"scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth",
"scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)."
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Изпращайте съобщения на оригиналния въведен език.",
"translation_translateBeforeSending": "Преведете преди да изпратите",
"translation_messageTranslation": "Превод на съобщението",
"translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.",
"translation_translateTo": "Превеждане на {language}",
"translation_translationOptions": "Опции за превод",
"translation_systemLanguage": "Език на системата"
}

View file

@ -2035,6 +2035,34 @@
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
"radioStats_settingsTile": "Senderinformationen",
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "Übersetzung",
"translation_composerTitle": "Übersetzen Sie vor dem Versenden",
"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_targetLanguage": "Zielsprache",
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
"translation_presetModelLabel": "Vordefinierter Hugging Face-Modell",
"translation_manualUrlLabel": "URL für das manuelle Modell",
"translation_downloadModel": "Modell herunterladen",
"translation_downloading": "Herunterladen...",
"translation_working": "Arbeiten...",
"translation_stop": "Stopp",
"translation_mergingChunks": "Zusammenführen der heruntergeladenen Teile in die finale Datei...",
"translation_downloadedModels": "Heruntergeladene Modelle",
"translation_deleteModel": "Modell löschen",
"translation_modelDownloaded": "Übersetzungsmotor heruntergeladen.",
"translation_downloadStopped": "Herunterladen abgebrochen.",
"translation_downloadFailed": "Download fehlgeschlagen: {error}",
"translation_enterUrlFirst": "Geben Sie zunächst die URL eines Modells ein.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2042,8 +2070,22 @@
}
}
},
"scanner_linuxPairingPinPrompt": "Geben Sie den PIN-Code für {deviceName} ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).",
"scanner_linuxPairingShowPin": "PIN anzeigen",
"scanner_linuxPairingHidePin": "PIN ausblenden",
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine)."
"scanner_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung",
"scanner_linuxPairingHidePin": "PIN verbergen",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Nachricht übersetzen",
"translation_composerEnabledHint": "Die Nachrichten werden vor dem Versenden übersetzt.",
"translation_translateBeforeSending": "Übersetzen Sie vor dem Versenden",
"translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.",
"translation_translateTo": "Übersetzen Sie auf {language}",
"translation_translationOptions": "Übersetzungsmöglichkeiten",
"translation_systemLanguage": "Sprache des Systems"
}

View file

@ -2042,6 +2042,35 @@
"radioStats_stripWaiting": "Fetching radio stats…",
"radioStats_settingsTile": "Radio stats",
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
"translation_title": "Translation",
"translation_enableTitle": "Enable translation",
"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_targetLanguage": "Target language",
"translation_useAppLanguage": "Use app language",
"translation_downloadedModelLabel": "Downloaded model",
"translation_presetModelLabel": "Preset Hugging Face model",
"translation_manualUrlLabel": "Manual model URL",
"translation_downloadModel": "Download model",
"translation_downloading": "Downloading...",
"translation_working": "Working...",
"translation_stop": "Stop",
"translation_mergingChunks": "Merging downloaded chunks into final file...",
"translation_downloadedModels": "Downloaded models",
"translation_deleteModel": "Delete model",
"translation_modelDownloaded": "Translation model downloaded.",
"translation_downloadStopped": "Download stopped.",
"translation_downloadFailed": "Download failed: {error}",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enterUrlFirst": "Enter a model URL first.",
"scanner_linuxPairingShowPin": "Show PIN",
"scanner_linuxPairingHidePin": "Hide PIN",
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
@ -2052,5 +2081,19 @@
"type": "String"
}
}
}
}
},
"translation_messageTranslation": "Message translation",
"translation_translateBeforeSending": "Translate before sending",
"translation_composerEnabledHint": "Messages will be translated before send.",
"translation_composerDisabledHint": "Send messages in the original typed language.",
"translation_translateTo": "Translate to {language}",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translationOptions": "Translation options",
"translation_systemLanguage": "System language"
}

View file

@ -2035,6 +2035,34 @@
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
"radioStats_settingsTile": "Estadísticas de radio",
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "Traducción",
"translation_enableSubtitle": "Traducir los mensajes entrantes y permitir la traducción previa al envío.",
"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_targetLanguage": "Idioma de destino",
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
"translation_downloadedModelLabel": "Modelo descargado",
"translation_presetModelLabel": "Modelo predefinido de Hugging Face",
"translation_manualUrlLabel": "URL del modelo manual",
"translation_downloadModel": "Descargar el modelo",
"translation_downloading": "Descargando...",
"translation_working": "Trabajando...",
"translation_stop": "¡Detente!",
"translation_mergingChunks": "Combinando los fragmentos descargados en el archivo final...",
"translation_downloadedModels": "Modelos descargados",
"translation_deleteModel": "Eliminar modelo",
"translation_modelDownloaded": "Modelo de traducción descargado.",
"translation_downloadStopped": "La descarga se ha detenido.",
"translation_downloadFailed": "No se pudo descargar: {error}",
"translation_enterUrlFirst": "Primero, introduzca la URL del modelo.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2042,8 +2070,22 @@
}
}
},
"scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth",
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
"scanner_linuxPairingShowPin": "Mostrar código PIN",
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)."
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Envía mensajes utilizando el lenguaje escrito original.",
"translation_composerEnabledHint": "Los mensajes serán traducidos antes de ser enviados.",
"translation_messageTranslation": "Traducción del mensaje",
"translation_translateBeforeSending": "Traducir antes de enviar",
"translation_translateTo": "Traducir a {language}",
"translation_translationOptions": "Opciones de traducción",
"translation_systemLanguage": "Idioma del sistema"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Récupération des statistiques de la radio…",
"radioStats_settingsTile": "Statistiques de radio",
"radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Traduire avant d'envoyer",
"translation_enableTitle": "Activer la traduction",
"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_targetLanguage": "Langue cible",
"translation_useAppLanguage": "Utiliser la langue de l'application",
"translation_downloadedModelLabel": "Modèle téléchargé",
"translation_presetModelLabel": "Modèle Hugging Face préconfiguré",
"translation_manualUrlLabel": "URL du modèle manuel",
"translation_downloadModel": "Télécharger le modèle",
"translation_downloading": "Téléchargement...",
"translation_working": "Au travail...",
"translation_stop": "Arrêtez",
"translation_mergingChunks": "Fusion des fragments téléchargés dans le fichier final...",
"translation_downloadedModels": "Modèles téléchargés",
"translation_deleteModel": "Supprimer le modèle",
"translation_modelDownloaded": "Modèle de traduction téléchargé.",
"translation_downloadStopped": "Le téléchargement a été interrompu.",
"translation_downloadFailed": "Échec du téléchargement : {error}",
"translation_enterUrlFirst": "Entrez d'abord l'URL du modèle.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingShowPin": "Afficher le code PIN",
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
"scanner_linuxPairingHidePin": "Masquer le code PIN",
"scanner_linuxPairingPinTitle": "Code PIN dappairage Bluetooth",
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun)."
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
"scanner_linuxPairingShowPin": "Afficher le code PIN",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Les messages seront traduits avant d'être envoyés.",
"translation_translateBeforeSending": "Traduire avant d'envoyer",
"translation_composerDisabledHint": "Envoyez des messages dans la langue originale, telle que vous l'avez tapée.",
"translation_messageTranslation": "Traduction du message",
"translation_translateTo": "Traduire en {language}",
"translation_translationOptions": "Options de traduction",
"translation_systemLanguage": "Langue du système"
}

View file

@ -2045,6 +2045,34 @@
"contact_teleEnvSubtitle": "Engedje meg az érzékelő adatok megosztását",
"map_showOverlaps": "Az ismétlő kulcsok ütköznek",
"map_runTraceWithReturnPath": "Visszaforduljon az eredeti úton.",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "Fordítás",
"translation_enableTitle": "Engedje meg a fordítást",
"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_targetLanguage": "Célnyelv",
"translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.",
"translation_downloadedModelLabel": "Letöltött modell",
"translation_presetModelLabel": "Előre definiált Hugging Face-modell",
"translation_manualUrlLabel": "Manuális modell URL",
"translation_downloadModel": "Letöltés",
"translation_downloading": "Letöltés...",
"translation_working": "Munkában vagyok...",
"translation_stop": "Halt",
"translation_mergingChunks": "A letöltött részek összeállítása a végleges fájlba...",
"translation_downloadedModels": "Letöltött modelok",
"translation_deleteModel": "Törölje a modellt",
"translation_modelDownloaded": "Fordítási modell letöltve.",
"translation_downloadStopped": "A letöltés leállt.",
"translation_downloadFailed": "Letöltés sikertelen: {error}",
"translation_enterUrlFirst": "Addon először egy modell URL-t.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2052,8 +2080,22 @@
}
}
},
"scanner_linuxPairingHidePin": "PIN elrejtése",
"scanner_linuxPairingShowPin": "PIN megjelenítése",
"scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot",
"scanner_linuxPairingPinPrompt": "Adja meg a PIN kódot a {deviceName} számára (hagyja üresen, ha nincs).",
"scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot",
"scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)."
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "Fordítsa el, mielőtt elküldi",
"translation_composerEnabledHint": "A üzenetek fordítását a küldés előtt elvégezzük.",
"translation_messageTranslation": "Üzenet fordítása",
"translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.",
"translation_translateTo": "Fordítás {language}-ra",
"translation_translationOptions": "Fordítási lehetőségek",
"translation_systemLanguage": "Rendszer nyelvé"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
"radioStats_settingsTile": "Statistiche radio",
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Tradurre prima di inviare",
"translation_enableSubtitle": "Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell'invio.",
"translation_enableTitle": "Abilitare la traduzione",
"translation_title": "Traduzione",
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
"translation_targetLanguage": "Lingua di destinazione",
"translation_useAppLanguage": "Utilizza la lingua dell'app",
"translation_downloadedModelLabel": "Modello scaricato",
"translation_presetModelLabel": "Modello predefinito di Hugging Face",
"translation_manualUrlLabel": "URL del modello manuale",
"translation_downloadModel": "Scarica il modello",
"translation_downloading": "Inizio download...",
"translation_working": "Lavoro...",
"translation_stop": "Smetta",
"translation_downloadedModels": "Modelli scaricati",
"translation_mergingChunks": "Unione dei frammenti scaricati in un unico file...",
"translation_deleteModel": "Elimina modello",
"translation_modelDownloaded": "Modello di traduzione scaricato.",
"translation_downloadStopped": "Il download è stato interrotto.",
"translation_downloadFailed": "Download fallito: {error}",
"translation_enterUrlFirst": "Inserite innanzitutto l'URL del modello.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
"scanner_linuxPairingShowPin": "Mostra PIN",
"scanner_linuxPairingHidePin": "Nascondi PIN",
"scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth",
"scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è)."
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
"scanner_linuxPairingHidePin": "Nascondi il PIN",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Traduzione del messaggio",
"translation_translateBeforeSending": "Tradurre prima di inviare",
"translation_composerDisabledHint": "Invia messaggi utilizzando la lingua originale, scritta.",
"translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.",
"translation_translateTo": "Tradurre in {language}",
"translation_translationOptions": "Opzioni di traduzione",
"translation_systemLanguage": "Lingua del sistema"
}

View file

@ -2045,6 +2045,34 @@
"contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する",
"map_showOverlaps": "リピーターキーの重複",
"map_runTraceWithReturnPath": "元の経路に戻る。",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "受信メッセージを翻訳し、送信前に翻訳を適用できるようにする。",
"translation_title": "翻訳",
"translation_composerTitle": "送信する前に翻訳する",
"translation_enableTitle": "翻訳機能を有効にする",
"translation_composerSubtitle": "作曲家翻訳アイコンのデフォルト状態を制御する。",
"translation_targetLanguage": "翻訳対象言語",
"translation_useAppLanguage": "アプリの言語設定",
"translation_downloadedModelLabel": "ダウンロードしたモデル",
"translation_presetModelLabel": "あらかじめ設定されたHugging Faceモデル",
"translation_manualUrlLabel": "マニュアルモデルのURL",
"translation_downloadModel": "モデルのダウンロード",
"translation_downloading": "ダウンロード中...",
"translation_working": "業務中…",
"translation_stop": "停止",
"translation_mergingChunks": "ダウンロードしたファイルを最終ファイルに結合中...",
"translation_downloadedModels": "ダウンロードされたモデル",
"translation_deleteModel": "モデルを削除",
"translation_modelDownloaded": "翻訳モデルのダウンロードが完了しました。",
"translation_downloadStopped": "ダウンロードが中断されました。",
"translation_downloadFailed": "ダウンロードに失敗しました:{error}",
"translation_enterUrlFirst": "まず、モデルのURLを入力してください。",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2052,8 +2080,22 @@
}
}
},
"scanner_linuxPairingShowPin": "PINを表示",
"scanner_linuxPairingHidePin": "PINを非表示",
"scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してくださいなしの場合は空欄のまま。"
"scanner_linuxPairingShowPin": "PINを表示する",
"scanner_linuxPairingHidePin": "PINを非表示にする",
"scanner_linuxPairingPinPrompt": "{deviceName} の PIN を入力してください(該当しない場合は空白で入力)。",
"scanner_linuxPairingPinTitle": "Bluetooth 接続のためのPIN",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "送信する前に翻訳する",
"translation_composerEnabledHint": "メッセージは送信前に翻訳されます。",
"translation_messageTranslation": "メッセージの翻訳",
"translation_composerDisabledHint": "元のタイプされた言語でメッセージを送信してください。",
"translation_translateTo": "{language} への翻訳",
"translation_translationOptions": "翻訳の選択肢",
"translation_systemLanguage": "システム言語"
}

View file

@ -2045,6 +2045,34 @@
"contact_teleEnvSubtitle": "환경 센서 데이터를 공유하도록 허용",
"map_showOverlaps": "반복 키 중복",
"map_runTraceWithReturnPath": "원래 경로로 돌아가세요.",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "입력 메시지를 번역하고, 미리 번역 기능을 제공합니다.",
"translation_title": "번역",
"translation_enableTitle": "번역 기능 활성화",
"translation_composerTitle": "보내기 전에 번역",
"translation_composerSubtitle": "컴포저 번역 아이콘의 기본 상태를 제어합니다.",
"translation_targetLanguage": "목표 언어",
"translation_useAppLanguage": "앱 언어 사용",
"translation_downloadedModelLabel": "다운로드한 모델",
"translation_presetModelLabel": "사전에 설정된 Hugging Face 모델",
"translation_manualUrlLabel": "수동 모델 URL",
"translation_downloadModel": "모델 다운로드",
"translation_downloading": "다운로드 중...",
"translation_working": "업무 중...",
"translation_stop": "멈춰",
"translation_mergingChunks": "다운로드한 파일 조각들을 최종 파일로 병합 중...",
"translation_downloadedModels": "다운로드한 모델",
"translation_deleteModel": "모델 삭제",
"translation_modelDownloaded": "번역 모델이 다운로드되었습니다.",
"translation_downloadStopped": "다운로드 중단됨.",
"translation_downloadFailed": "다운로드 실패: {error}",
"translation_enterUrlFirst": "먼저 모델 URL을 입력하세요.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2052,8 +2080,22 @@
}
}
},
"scanner_linuxPairingShowPin": "PIN 표시",
"scanner_linuxPairingPinTitle": "블루투스 페어링 PIN",
"scanner_linuxPairingHidePin": "PIN 숨기기",
"scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)."
"scanner_linuxPairingShowPin": "PIN 보기",
"scanner_linuxPairingPinPrompt": "{deviceName}의 PIN을 입력하세요 (해당하는 경우에만 입력).",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "메시지는 전송하기 전에 번역될 것입니다.",
"translation_translateBeforeSending": "보내기 전에 번역",
"translation_messageTranslation": "메시지 번역",
"translation_composerDisabledHint": "원래 작성된 언어로 메시지를 보내세요.",
"translation_translateTo": "{language} 번역",
"translation_translationOptions": "번역 옵션",
"translation_systemLanguage": "시스템 언어"
}

View file

@ -6148,30 +6148,6 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Noise floor, RSSI, SNR, and airtime'**
String get radioStats_settingsSubtitle;
/// No description provided for @scanner_linuxPairingShowPin.
///
/// In en, this message translates to:
/// **'Show PIN'**
String get scanner_linuxPairingShowPin;
/// No description provided for @scanner_linuxPairingHidePin.
///
/// In en, this message translates to:
/// **'Hide PIN'**
String get scanner_linuxPairingHidePin;
/// No description provided for @scanner_linuxPairingPinTitle.
///
/// In en, this message translates to:
/// **'Bluetooth Pairing PIN'**
String get scanner_linuxPairingPinTitle;
/// No description provided for @scanner_linuxPairingPinPrompt.
///
/// In en, this message translates to:
/// **'Enter PIN for {deviceName} (leave blank if none).'**
String scanner_linuxPairingPinPrompt(String deviceName);
}
class _AppLocalizationsDelegate

View file

@ -3568,18 +3568,112 @@ class AppLocalizationsBg extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Ниво на шума, RSSI, SNR и време на пренос';
@override
String get translation_title => 'Превод';
@override
String get translation_enableTitle => 'Активирайте превода';
@override
String get translation_enableSubtitle =>
'Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.';
@override
String get translation_composerTitle => 'Преведете преди да изпратите';
@override
String get translation_composerSubtitle =>
'Контролира началния статус на иконата за превод, създадена от композитора.';
@override
String get translation_targetLanguage => 'Целеви език';
@override
String get translation_useAppLanguage => 'Използвайте езика на приложението';
@override
String get translation_downloadedModelLabel => 'Изтегнат модел';
@override
String get translation_presetModelLabel =>
'Предварително конфигуриран модел от Hugging Face';
@override
String get translation_manualUrlLabel => 'URL на ръководството';
@override
String get translation_downloadModel => 'Изтеглете модела';
@override
String get translation_downloading => 'Изтегляне...';
@override
String get translation_working => 'Работа...';
@override
String get translation_stop => 'Спрете';
@override
String get translation_mergingChunks =>
'Съединяване на изтеглените части в един файл...';
@override
String get translation_downloadedModels => 'Изтеглени модели';
@override
String get translation_deleteModel => 'Изтриване на модела';
@override
String get translation_modelDownloaded => 'Моделът за превод е изтеглен.';
@override
String get translation_downloadStopped => 'Изтеглянето беше прекъснато.';
@override
String translation_downloadFailed(String error) {
return 'Не успях да изтегля: $error';
}
@override
String get translation_enterUrlFirst => 'Въведете първо URL адрес на модела.';
@override
String get scanner_linuxPairingShowPin => 'Покажи PIN';
@override
String get scanner_linuxPairingHidePin => 'Скрий ПИН';
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
@override
String get scanner_linuxPairingPinTitle =>
'PIN код за сдвояване на Bluetooth';
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Въведете ПИН за $deviceName (оставете празно, ако няма).';
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
}
@override
String get translation_messageTranslation => 'Превод на съобщението';
@override
String get translation_translateBeforeSending =>
'Преведете преди да изпратите';
@override
String get translation_composerEnabledHint =>
'Съобщенията ще бъдат преведени, преди да бъдат изпратени.';
@override
String get translation_composerDisabledHint =>
'Изпращайте съобщения на оригиналния въведен език.';
@override
String translation_translateTo(String language) {
return 'Превеждане на $language';
}
@override
String get translation_translationOptions => 'Опции за превод';
@override
String get translation_systemLanguage => 'Език на системата';
}

View file

@ -3577,17 +3577,114 @@ class AppLocalizationsDe extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit';
@override
String get translation_title => 'Übersetzung';
@override
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
@override
String get translation_enableSubtitle =>
'Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.';
@override
String get translation_composerTitle => 'Übersetzen Sie vor dem Versenden';
@override
String get translation_composerSubtitle =>
'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.';
@override
String get translation_targetLanguage => 'Zielsprache';
@override
String get translation_useAppLanguage => 'Verwenden Sie die App-Sprache';
@override
String get translation_downloadedModelLabel => 'Heruntergeladenes Modell';
@override
String get translation_presetModelLabel =>
'Vordefinierter Hugging Face-Modell';
@override
String get translation_manualUrlLabel => 'URL für das manuelle Modell';
@override
String get translation_downloadModel => 'Modell herunterladen';
@override
String get translation_downloading => 'Herunterladen...';
@override
String get translation_working => 'Arbeiten...';
@override
String get translation_stop => 'Stopp';
@override
String get translation_mergingChunks =>
'Zusammenführen der heruntergeladenen Teile in die finale Datei...';
@override
String get translation_downloadedModels => 'Heruntergeladene Modelle';
@override
String get translation_deleteModel => 'Modell löschen';
@override
String get translation_modelDownloaded =>
'Übersetzungsmotor heruntergeladen.';
@override
String get translation_downloadStopped => 'Herunterladen abgebrochen.';
@override
String translation_downloadFailed(String error) {
return 'Download fehlgeschlagen: $error';
}
@override
String get translation_enterUrlFirst =>
'Geben Sie zunächst die URL eines Modells ein.';
@override
String get scanner_linuxPairingShowPin => 'PIN anzeigen';
@override
String get scanner_linuxPairingHidePin => 'PIN ausblenden';
String get scanner_linuxPairingHidePin => 'PIN verbergen';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN';
String get scanner_linuxPairingPinTitle => 'PIN für die Bluetooth-Verbindung';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).';
return 'Geben Sie den PIN-Code für $deviceName ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).';
}
@override
String get translation_messageTranslation => 'Nachricht übersetzen';
@override
String get translation_translateBeforeSending =>
'Übersetzen Sie vor dem Versenden';
@override
String get translation_composerEnabledHint =>
'Die Nachrichten werden vor dem Versenden übersetzt.';
@override
String get translation_composerDisabledHint =>
'Nachrichten in der ursprünglichen, getippten Sprache senden.';
@override
String translation_translateTo(String language) {
return 'Übersetzen Sie auf $language';
}
@override
String get translation_translationOptions => 'Übersetzungsmöglichkeiten';
@override
String get translation_systemLanguage => 'Sprache des Systems';
}

View file

@ -3502,6 +3502,74 @@ class AppLocalizationsEn extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Noise floor, RSSI, SNR, and airtime';
@override
String get translation_title => 'Translation';
@override
String get translation_enableTitle => 'Enable translation';
@override
String get translation_enableSubtitle =>
'Translate incoming messages and allow pre-send translation.';
@override
String get translation_composerTitle => 'Translate before sending';
@override
String get translation_composerSubtitle =>
'Controls the default state of the composer translation icon.';
@override
String get translation_targetLanguage => 'Target language';
@override
String get translation_useAppLanguage => 'Use app language';
@override
String get translation_downloadedModelLabel => 'Downloaded model';
@override
String get translation_presetModelLabel => 'Preset Hugging Face model';
@override
String get translation_manualUrlLabel => 'Manual model URL';
@override
String get translation_downloadModel => 'Download model';
@override
String get translation_downloading => 'Downloading...';
@override
String get translation_working => 'Working...';
@override
String get translation_stop => 'Stop';
@override
String get translation_mergingChunks =>
'Merging downloaded chunks into final file...';
@override
String get translation_downloadedModels => 'Downloaded models';
@override
String get translation_deleteModel => 'Delete model';
@override
String get translation_modelDownloaded => 'Translation model downloaded.';
@override
String get translation_downloadStopped => 'Download stopped.';
@override
String translation_downloadFailed(String error) {
return 'Download failed: $error';
}
@override
String get translation_enterUrlFirst => 'Enter a model URL first.';
@override
String get scanner_linuxPairingShowPin => 'Show PIN';
@ -3515,4 +3583,29 @@ class AppLocalizationsEn extends AppLocalizations {
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Enter PIN for $deviceName (leave blank if none).';
}
@override
String get translation_messageTranslation => 'Message translation';
@override
String get translation_translateBeforeSending => 'Translate before sending';
@override
String get translation_composerEnabledHint =>
'Messages will be translated before send.';
@override
String get translation_composerDisabledHint =>
'Send messages in the original typed language.';
@override
String translation_translateTo(String language) {
return 'Translate to $language';
}
@override
String get translation_translationOptions => 'Translation options';
@override
String get translation_systemLanguage => 'System language';
}

View file

@ -3571,16 +3571,113 @@ class AppLocalizationsEs extends AppLocalizations {
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
@override
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
String get translation_title => 'Traducción';
@override
String get translation_enableTitle => 'Habilitar la traducción';
@override
String get translation_enableSubtitle =>
'Traducir los mensajes entrantes y permitir la traducción previa al envío.';
@override
String get translation_composerTitle => 'Traducir antes de enviar';
@override
String get translation_composerSubtitle =>
'Controla el estado predeterminado del icono de traducción del compositor.';
@override
String get translation_targetLanguage => 'Idioma de destino';
@override
String get translation_useAppLanguage =>
'Utilizar el idioma de la aplicación';
@override
String get translation_downloadedModelLabel => 'Modelo descargado';
@override
String get translation_presetModelLabel =>
'Modelo predefinido de Hugging Face';
@override
String get translation_manualUrlLabel => 'URL del modelo manual';
@override
String get translation_downloadModel => 'Descargar el modelo';
@override
String get translation_downloading => 'Descargando...';
@override
String get translation_working => 'Trabajando...';
@override
String get translation_stop => '¡Detente!';
@override
String get translation_mergingChunks =>
'Combinando los fragmentos descargados en el archivo final...';
@override
String get translation_downloadedModels => 'Modelos descargados';
@override
String get translation_deleteModel => 'Eliminar modelo';
@override
String get translation_modelDownloaded => 'Modelo de traducción descargado.';
@override
String get translation_downloadStopped => 'La descarga se ha detenido.';
@override
String translation_downloadFailed(String error) {
return 'No se pudo descargar: $error';
}
@override
String get translation_enterUrlFirst =>
'Primero, introduzca la URL del modelo.';
@override
String get scanner_linuxPairingShowPin => 'Mostrar código PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN de emparejamiento Bluetooth';
String get scanner_linuxPairingPinTitle =>
'PIN para emparejar dispositivos Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Introduzca el PIN para $deviceName (déjelo en blanco si no hay ninguno).';
return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).';
}
@override
String get translation_messageTranslation => 'Traducción del mensaje';
@override
String get translation_translateBeforeSending => 'Traducir antes de enviar';
@override
String get translation_composerEnabledHint =>
'Los mensajes serán traducidos antes de ser enviados.';
@override
String get translation_composerDisabledHint =>
'Envía mensajes utilizando el lenguaje escrito original.';
@override
String translation_translateTo(String language) {
return 'Traducir a $language';
}
@override
String get translation_translationOptions => 'Opciones de traducción';
@override
String get translation_systemLanguage => 'Idioma del sistema';
}

View file

@ -3595,6 +3595,76 @@ class AppLocalizationsFr extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne';
@override
String get translation_title => 'Traduction';
@override
String get translation_enableTitle => 'Activer la traduction';
@override
String get translation_enableSubtitle =>
'Traduire les messages entrants et permettre la traduction avant l\'envoi.';
@override
String get translation_composerTitle => 'Traduire avant d\'envoyer';
@override
String get translation_composerSubtitle =>
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
@override
String get translation_targetLanguage => 'Langue cible';
@override
String get translation_useAppLanguage =>
'Utiliser la langue de l\'application';
@override
String get translation_downloadedModelLabel => 'Modèle téléchargé';
@override
String get translation_presetModelLabel => 'Modèle Hugging Face préconfiguré';
@override
String get translation_manualUrlLabel => 'URL du modèle manuel';
@override
String get translation_downloadModel => 'Télécharger le modèle';
@override
String get translation_downloading => 'Téléchargement...';
@override
String get translation_working => 'Au travail...';
@override
String get translation_stop => 'Arrêtez';
@override
String get translation_mergingChunks =>
'Fusion des fragments téléchargés dans le fichier final...';
@override
String get translation_downloadedModels => 'Modèles téléchargés';
@override
String get translation_deleteModel => 'Supprimer le modèle';
@override
String get translation_modelDownloaded => 'Modèle de traduction téléchargé.';
@override
String get translation_downloadStopped =>
'Le téléchargement a été interrompu.';
@override
String translation_downloadFailed(String error) {
return 'Échec du téléchargement : $error';
}
@override
String get translation_enterUrlFirst => 'Entrez d\'abord l\'URL du modèle.';
@override
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
@ -3602,10 +3672,36 @@ class AppLocalizationsFr extends AppLocalizations {
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
@override
String get scanner_linuxPairingPinTitle => 'Code PIN dappairage Bluetooth';
String get scanner_linuxPairingPinTitle =>
'Code PIN pour la connexion Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Entrez le code PIN pour $deviceName (laissez vide si aucun).';
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
}
@override
String get translation_messageTranslation => 'Traduction du message';
@override
String get translation_translateBeforeSending => 'Traduire avant d\'envoyer';
@override
String get translation_composerEnabledHint =>
'Les messages seront traduits avant d\'être envoyés.';
@override
String get translation_composerDisabledHint =>
'Envoyez des messages dans la langue originale, telle que vous l\'avez tapée.';
@override
String translation_translateTo(String language) {
return 'Traduire en $language';
}
@override
String get translation_translationOptions => 'Options de traduction';
@override
String get translation_systemLanguage => 'Langue du système';
}

View file

@ -3588,16 +3588,112 @@ class AppLocalizationsHu extends AppLocalizations {
'Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama';
@override
String get scanner_linuxPairingShowPin => 'PIN megjelenítése';
String get translation_title => 'Fordítás';
@override
String get scanner_linuxPairingHidePin => 'PIN elrejtése';
String get translation_enableTitle => 'Engedje meg a fordítást';
@override
String get translation_enableSubtitle =>
'Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.';
@override
String get translation_composerTitle => 'Fordítsa el, mielőtt elküldi';
@override
String get translation_composerSubtitle =>
'Ellenőrzi a zeneszerző fordítási ikon alapértékét.';
@override
String get translation_targetLanguage => 'Célnyelv';
@override
String get translation_useAppLanguage =>
'Használja az alkalmazás nyelvének beállítását.';
@override
String get translation_downloadedModelLabel => 'Letöltött modell';
@override
String get translation_presetModelLabel =>
'Előre definiált Hugging Face-modell';
@override
String get translation_manualUrlLabel => 'Manuális modell URL';
@override
String get translation_downloadModel => 'Letöltés';
@override
String get translation_downloading => 'Letöltés...';
@override
String get translation_working => 'Munkában vagyok...';
@override
String get translation_stop => 'Halt';
@override
String get translation_mergingChunks =>
'A letöltött részek összeállítása a végleges fájlba...';
@override
String get translation_downloadedModels => 'Letöltött modelok';
@override
String get translation_deleteModel => 'Törölje a modellt';
@override
String get translation_modelDownloaded => 'Fordítási modell letöltve.';
@override
String get translation_downloadStopped => 'A letöltés leállt.';
@override
String translation_downloadFailed(String error) {
return 'Letöltés sikertelen: $error';
}
@override
String get translation_enterUrlFirst => 'Addon először egy modell URL-t.';
@override
String get scanner_linuxPairingShowPin => 'Megjelenítse a PIN-kódot';
@override
String get scanner_linuxPairingHidePin => 'Rejtse el a PIN-kódot';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth párosítási PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).';
}
@override
String get translation_messageTranslation => 'Üzenet fordítása';
@override
String get translation_translateBeforeSending =>
'Fordítsa el, mielőtt elküldi';
@override
String get translation_composerEnabledHint =>
'A üzenetek fordítását a küldés előtt elvégezzük.';
@override
String get translation_composerDisabledHint =>
'Küldj üzeneteket az eredeti, nyomtatott nyelven.';
@override
String translation_translateTo(String language) {
return 'Fordítás $language-ra';
}
@override
String get translation_translationOptions => 'Fordítási lehetőségek';
@override
String get translation_systemLanguage => 'Rendszer nyelvé';
}

View file

@ -3574,17 +3574,113 @@ class AppLocalizationsIt extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
@override
String get translation_title => 'Traduzione';
@override
String get translation_enableTitle => 'Abilitare la traduzione';
@override
String get translation_enableSubtitle =>
'Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell\'invio.';
@override
String get translation_composerTitle => 'Tradurre prima di inviare';
@override
String get translation_composerSubtitle =>
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
@override
String get translation_targetLanguage => 'Lingua di destinazione';
@override
String get translation_useAppLanguage => 'Utilizza la lingua dell\'app';
@override
String get translation_downloadedModelLabel => 'Modello scaricato';
@override
String get translation_presetModelLabel =>
'Modello predefinito di Hugging Face';
@override
String get translation_manualUrlLabel => 'URL del modello manuale';
@override
String get translation_downloadModel => 'Scarica il modello';
@override
String get translation_downloading => 'Inizio download...';
@override
String get translation_working => 'Lavoro...';
@override
String get translation_stop => 'Smetta';
@override
String get translation_mergingChunks =>
'Unione dei frammenti scaricati in un unico file...';
@override
String get translation_downloadedModels => 'Modelli scaricati';
@override
String get translation_deleteModel => 'Elimina modello';
@override
String get translation_modelDownloaded => 'Modello di traduzione scaricato.';
@override
String get translation_downloadStopped => 'Il download è stato interrotto.';
@override
String translation_downloadFailed(String error) {
return 'Download fallito: $error';
}
@override
String get translation_enterUrlFirst =>
'Inserite innanzitutto l\'URL del modello.';
@override
String get scanner_linuxPairingShowPin => 'Mostra PIN';
@override
String get scanner_linuxPairingHidePin => 'Nascondi PIN';
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN di associazione Bluetooth';
String get scanner_linuxPairingPinTitle =>
'PIN per l\'accoppiamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Inserisci il PIN per $deviceName (lascia vuoto se non ce n\'è).';
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
}
@override
String get translation_messageTranslation => 'Traduzione del messaggio';
@override
String get translation_translateBeforeSending => 'Tradurre prima di inviare';
@override
String get translation_composerEnabledHint =>
'I messaggi verranno tradotti prima di essere inviati.';
@override
String get translation_composerDisabledHint =>
'Invia messaggi utilizzando la lingua originale, scritta.';
@override
String translation_translateTo(String language) {
return 'Tradurre in $language';
}
@override
String get translation_translationOptions => 'Opzioni di traduzione';
@override
String get translation_systemLanguage => 'Lingua del sistema';
}

View file

@ -3403,16 +3403,104 @@ class AppLocalizationsJa extends AppLocalizations {
String get radioStats_settingsSubtitle => 'イズレベル、RSSI、SNR、および通信時間';
@override
String get scanner_linuxPairingShowPin => 'PINを表示';
String get translation_title => '翻訳';
@override
String get scanner_linuxPairingHidePin => 'PINを非表示';
String get translation_enableTitle => '翻訳機能を有効にする';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth ペアリング PIN';
String get translation_enableSubtitle => '受信メッセージを翻訳し、送信前に翻訳を適用できるようにする。';
@override
String get translation_composerTitle => '送信する前に翻訳する';
@override
String get translation_composerSubtitle => '作曲家翻訳アイコンのデフォルト状態を制御する。';
@override
String get translation_targetLanguage => '翻訳対象言語';
@override
String get translation_useAppLanguage => 'アプリの言語設定';
@override
String get translation_downloadedModelLabel => 'ダウンロードしたモデル';
@override
String get translation_presetModelLabel => 'あらかじめ設定されたHugging Faceモデル';
@override
String get translation_manualUrlLabel => 'マニュアルモデルのURL';
@override
String get translation_downloadModel => 'モデルのダウンロード';
@override
String get translation_downloading => 'ダウンロード中...';
@override
String get translation_working => '業務中…';
@override
String get translation_stop => '停止';
@override
String get translation_mergingChunks => 'ダウンロードしたファイルを最終ファイルに結合中...';
@override
String get translation_downloadedModels => 'ダウンロードされたモデル';
@override
String get translation_deleteModel => 'モデルを削除';
@override
String get translation_modelDownloaded => '翻訳モデルのダウンロードが完了しました。';
@override
String get translation_downloadStopped => 'ダウンロードが中断されました。';
@override
String translation_downloadFailed(String error) {
return 'ダウンロードに失敗しました:$error';
}
@override
String get translation_enterUrlFirst => 'まず、モデルのURLを入力してください。';
@override
String get scanner_linuxPairingShowPin => 'PINを表示する';
@override
String get scanner_linuxPairingHidePin => 'PINを非表示にする';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth 接続のためのPIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '$deviceNameのPINを入力してください(なしの場合は空欄のまま)。';
return '$deviceName の PIN を入力してください(該当しない場合は空白で入力)。';
}
@override
String get translation_messageTranslation => 'メッセージの翻訳';
@override
String get translation_translateBeforeSending => '送信する前に翻訳する';
@override
String get translation_composerEnabledHint => 'メッセージは送信前に翻訳されます。';
@override
String get translation_composerDisabledHint => '元のタイプされた言語でメッセージを送信してください。';
@override
String translation_translateTo(String language) {
return '$language への翻訳';
}
@override
String get translation_translationOptions => '翻訳の選択肢';
@override
String get translation_systemLanguage => 'システム言語';
}

View file

@ -3402,7 +3402,72 @@ class AppLocalizationsKo extends AppLocalizations {
String get radioStats_settingsSubtitle => '잡음 수준, RSSI, 신호 대 잡음비, 통신 시간';
@override
String get scanner_linuxPairingShowPin => 'PIN 표시';
String get translation_title => '번역';
@override
String get translation_enableTitle => '번역 기능 활성화';
@override
String get translation_enableSubtitle => '입력 메시지를 번역하고, 미리 번역 기능을 제공합니다.';
@override
String get translation_composerTitle => '보내기 전에 번역';
@override
String get translation_composerSubtitle => '컴포저 번역 아이콘의 기본 상태를 제어합니다.';
@override
String get translation_targetLanguage => '목표 언어';
@override
String get translation_useAppLanguage => '앱 언어 사용';
@override
String get translation_downloadedModelLabel => '다운로드한 모델';
@override
String get translation_presetModelLabel => '사전에 설정된 Hugging Face 모델';
@override
String get translation_manualUrlLabel => '수동 모델 URL';
@override
String get translation_downloadModel => '모델 다운로드';
@override
String get translation_downloading => '다운로드 중...';
@override
String get translation_working => '업무 중...';
@override
String get translation_stop => '멈춰';
@override
String get translation_mergingChunks => '다운로드한 파일 조각들을 최종 파일로 병합 중...';
@override
String get translation_downloadedModels => '다운로드한 모델';
@override
String get translation_deleteModel => '모델 삭제';
@override
String get translation_modelDownloaded => '번역 모델이 다운로드되었습니다.';
@override
String get translation_downloadStopped => '다운로드 중단됨.';
@override
String translation_downloadFailed(String error) {
return '다운로드 실패: $error';
}
@override
String get translation_enterUrlFirst => '먼저 모델 URL을 입력하세요.';
@override
String get scanner_linuxPairingShowPin => 'PIN 보기';
@override
String get scanner_linuxPairingHidePin => 'PIN 숨기기';
@ -3412,6 +3477,29 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '$deviceName 대한 PIN을 입력하세요 (없으면 비워두세요).';
return '$deviceName PIN을 입력하세요 (해당하는 경우에만 입력).';
}
@override
String get translation_messageTranslation => '메시지 번역';
@override
String get translation_translateBeforeSending => '보내기 전에 번역';
@override
String get translation_composerEnabledHint => '메시지는 전송하기 전에 번역될 것입니다.';
@override
String get translation_composerDisabledHint => '원래 작성된 언어로 메시지를 보내세요.';
@override
String translation_translateTo(String language) {
return '$language 번역';
}
@override
String get translation_translationOptions => '번역 옵션';
@override
String get translation_systemLanguage => '시스템 언어';
}

View file

@ -3552,17 +3552,112 @@ class AppLocalizationsNl extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd';
@override
String get translation_title => 'Vertaling';
@override
String get translation_enableTitle => 'Activeer vertaling';
@override
String get translation_enableSubtitle =>
'Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.';
@override
String get translation_composerTitle => 'Vertaal voor verzending';
@override
String get translation_composerSubtitle =>
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
@override
String get translation_targetLanguage => 'Doeltaal';
@override
String get translation_useAppLanguage => 'Gebruik de taal van de app';
@override
String get translation_downloadedModelLabel => 'Gedownloade model';
@override
String get translation_presetModelLabel =>
'Voorgeprogrammeerd Hugging Face-model';
@override
String get translation_manualUrlLabel => 'URL van de handleiding';
@override
String get translation_downloadModel => 'Download het model';
@override
String get translation_downloading => 'Downloaden...';
@override
String get translation_working => 'Werken...';
@override
String get translation_stop => 'Stoppen';
@override
String get translation_mergingChunks =>
'Het samenvoegen van de gedownloade stukken tot één eindbestand...';
@override
String get translation_downloadedModels => 'Gedownloade modellen';
@override
String get translation_deleteModel => 'Model verwijderen';
@override
String get translation_modelDownloaded => 'Vertalingmodel gedownload.';
@override
String get translation_downloadStopped => 'Download is afgebroken.';
@override
String translation_downloadFailed(String error) {
return 'Download mislukt: $error';
}
@override
String get translation_enterUrlFirst =>
'Voer eerst een URL van een model in.';
@override
String get scanner_linuxPairingShowPin => 'Toon PIN';
@override
String get scanner_linuxPairingHidePin => 'PIN verbergen';
String get scanner_linuxPairingHidePin => 'Verberg PIN';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothkoppelingsPIN';
String get scanner_linuxPairingPinTitle => 'PIN voor Bluetooth-koppeling';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
return 'Voer het pincode-in voor $deviceName in (laat dit leeg als er geen is).';
}
@override
String get translation_messageTranslation => 'Berichtvertaling';
@override
String get translation_translateBeforeSending => 'Vertaal voor verzending';
@override
String get translation_composerEnabledHint =>
'De berichten worden vertaald voordat ze verzonden worden.';
@override
String get translation_composerDisabledHint =>
'Stuur berichten in de oorspronkelijke, getypte taal.';
@override
String translation_translateTo(String language) {
return 'Vertalen naar $language';
}
@override
String get translation_translationOptions => 'Opties voor vertaling';
@override
String get translation_systemLanguage => 'Taal van het systeem';
}

View file

@ -3586,16 +3586,110 @@ class AppLocalizationsPl extends AppLocalizations {
'Szum tła, RSSI, SNR oraz czas dostępny';
@override
String get scanner_linuxPairingShowPin => 'Pokaż PIN';
String get translation_title => 'Tłumaczenie';
@override
String get scanner_linuxPairingHidePin => 'Ukryj PIN';
String get translation_enableTitle => 'Włącz tłumaczenie';
@override
String get scanner_linuxPairingPinTitle => 'Kod PIN parowania Bluetooth';
String get translation_enableSubtitle =>
'Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.';
@override
String get translation_composerTitle => 'Przekład przed wysłaniem';
@override
String get translation_composerSubtitle =>
'Kontroluje domyślny stan ikony tłumaczenia w edytorze.';
@override
String get translation_targetLanguage => 'Język docelowy';
@override
String get translation_useAppLanguage => 'Użyj języka aplikacji';
@override
String get translation_downloadedModelLabel => 'Pobudowany model';
@override
String get translation_presetModelLabel => 'Wspólny model Hugging Face';
@override
String get translation_manualUrlLabel => 'Adres URL do wersji manualnej';
@override
String get translation_downloadModel => 'Pobierz model';
@override
String get translation_downloading => 'Pobieranie...';
@override
String get translation_working => 'Praca...';
@override
String get translation_stop => 'Zatrzymaj się';
@override
String get translation_mergingChunks =>
'Scalanie pobranych fragmentów w jeden plik końcowy...';
@override
String get translation_downloadedModels => 'Pobrane modele';
@override
String get translation_deleteModel => 'Usuń model';
@override
String get translation_modelDownloaded => 'Model tłumaczenia został pobrany.';
@override
String get translation_downloadStopped => 'Pobieranie zakończone.';
@override
String translation_downloadFailed(String error) {
return 'Nie udało się pobrać: $error';
}
@override
String get translation_enterUrlFirst => 'Najpierw wprowadź adres URL modelu.';
@override
String get scanner_linuxPairingShowPin => 'Wyświetl kod PIN';
@override
String get scanner_linuxPairingHidePin => 'Ukryj kod PIN';
@override
String get scanner_linuxPairingPinTitle =>
'PIN do sparowania przez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).';
return 'Wprowadź kod PIN dla $deviceName (pust, jeśli nie jest wymagany).';
}
@override
String get translation_messageTranslation => 'Tłumaczenie wiadomości';
@override
String get translation_translateBeforeSending => 'Przekład przed wysłaniem';
@override
String get translation_composerEnabledHint =>
'Komunikaty zostaną przetłumaczone przed wysłaniem.';
@override
String get translation_composerDisabledHint =>
'Wysyłaj wiadomości w oryginalnym, wpisanym formacie.';
@override
String translation_translateTo(String language) {
return 'Tłumacz na $language';
}
@override
String get translation_translationOptions => 'Opcje tłumaczenia';
@override
String get translation_systemLanguage => 'Język systemu';
}

View file

@ -3567,17 +3567,111 @@ class AppLocalizationsPt extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Nível de ruído, RSSI, SNR e tempo de transmissão';
@override
String get translation_title => 'Tradução';
@override
String get translation_enableTitle => 'Ativar a tradução';
@override
String get translation_enableSubtitle =>
'Traduzir mensagens recebidas e permitir a tradução antes do envio.';
@override
String get translation_composerTitle => 'Traduza antes de enviar';
@override
String get translation_composerSubtitle =>
'Controla o estado padrão do ícone de tradução do compositor.';
@override
String get translation_targetLanguage => 'Língua-alvo';
@override
String get translation_useAppLanguage => 'Utilize o idioma da aplicação';
@override
String get translation_downloadedModelLabel => 'Modelo baixado';
@override
String get translation_presetModelLabel =>
'Modelo pré-definido da Hugging Face';
@override
String get translation_manualUrlLabel => 'URL do modelo manual';
@override
String get translation_downloadModel => 'Baixar modelo';
@override
String get translation_downloading => 'Baixando...';
@override
String get translation_working => 'Trabalhando...';
@override
String get translation_stop => 'Pare';
@override
String get translation_mergingChunks =>
'Combinando os fragmentos baixados em um único arquivo...';
@override
String get translation_downloadedModels => 'Modelos baixados';
@override
String get translation_deleteModel => 'Excluir modelo';
@override
String get translation_modelDownloaded => 'Modelo de tradução baixado.';
@override
String get translation_downloadStopped => 'Download interrompido.';
@override
String translation_downloadFailed(String error) {
return 'Falha na descarga: $error';
}
@override
String get translation_enterUrlFirst => 'Insira primeiro a URL do modelo.';
@override
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
String get scanner_linuxPairingHidePin => 'Esconder o PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth';
String get scanner_linuxPairingPinTitle => 'PIN de pareamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Insira o PIN para $deviceName (deixe em branco se não houver).';
return 'Insira o código PIN para $deviceName (deixe em branco se não houver).';
}
@override
String get translation_messageTranslation => 'Tradução da mensagem';
@override
String get translation_translateBeforeSending => 'Traduzir antes de enviar';
@override
String get translation_composerEnabledHint =>
'As mensagens serão traduzidas antes de serem enviadas.';
@override
String get translation_composerDisabledHint =>
'Envie mensagens no idioma original, conforme digitado.';
@override
String translation_translateTo(String language) {
return 'Traduzir para $language';
}
@override
String get translation_translationOptions => 'Opções de tradução';
@override
String get translation_systemLanguage => 'Idioma do sistema';
}

View file

@ -3582,16 +3582,111 @@ class AppLocalizationsRu extends AppLocalizations {
'Уровень шума, RSSI, SNR и время передачи';
@override
String get scanner_linuxPairingShowPin => 'Показать PIN';
String get translation_title => 'Перевод';
@override
String get scanner_linuxPairingHidePin => 'Скрыть PIN';
String get translation_enableTitle => 'Включить перевод';
@override
String get scanner_linuxPairingPinTitle => 'PINкод сопряжения Bluetooth';
String get translation_enableSubtitle =>
'Переводить входящие сообщения и позволять предварительный перевод перед отправкой.';
@override
String get translation_composerTitle => 'Переводить перед отправкой';
@override
String get translation_composerSubtitle =>
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
@override
String get translation_targetLanguage => 'Целевой язык';
@override
String get translation_useAppLanguage => 'Используйте язык приложения';
@override
String get translation_downloadedModelLabel => 'Загруженная модель';
@override
String get translation_presetModelLabel =>
'Предопределенная модель от Hugging Face';
@override
String get translation_manualUrlLabel => 'Ссылка на руководство';
@override
String get translation_downloadModel => 'Скачать модель';
@override
String get translation_downloading => 'Загрузка...';
@override
String get translation_working => 'Работа...';
@override
String get translation_stop => 'Прекратите';
@override
String get translation_mergingChunks =>
'Объединение скачанных фрагментов в один финальный файл...';
@override
String get translation_downloadedModels => 'Загруженные модели';
@override
String get translation_deleteModel => 'Удалить модель';
@override
String get translation_modelDownloaded => 'Модель перевода загружена.';
@override
String get translation_downloadStopped => 'Процесс загрузки был прерван.';
@override
String translation_downloadFailed(String error) {
return 'Не удалось скачать: $error';
}
@override
String get translation_enterUrlFirst => 'Сначала введите URL модели.';
@override
String get scanner_linuxPairingShowPin => 'Показать PIN-код';
@override
String get scanner_linuxPairingHidePin => 'Скрыть PIN-код';
@override
String get scanner_linuxPairingPinTitle =>
'PIN для сопряжения устройств по Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введите PINкод для $deviceName (оставьте пустым, если нет).';
return 'Введите PIN-код для $deviceName (оставьте поле пустым, если PIN-код отсутствует).';
}
@override
String get translation_messageTranslation => 'Перевод сообщения';
@override
String get translation_translateBeforeSending => 'Перевести перед отправкой';
@override
String get translation_composerEnabledHint =>
'Сообщения будут переведены перед отправкой.';
@override
String get translation_composerDisabledHint =>
'Отправляйте сообщения на языке, в котором они были изначально набраны.';
@override
String translation_translateTo(String language) {
return 'Перевести на $language';
}
@override
String get translation_translationOptions => 'Варианты перевода';
@override
String get translation_systemLanguage => 'Язык системы';
}

View file

@ -3545,6 +3545,77 @@ class AppLocalizationsSk extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Úroveň hluku, RSSI, SNR a časové rozloženie';
@override
String get translation_title => 'Preklad';
@override
String get translation_enableTitle => 'Aktivovať preklad';
@override
String get translation_enableSubtitle =>
'Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.';
@override
String get translation_composerTitle => 'Preložte pred odeslaním';
@override
String get translation_composerSubtitle =>
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
@override
String get translation_targetLanguage => 'Cieľový jazyk';
@override
String get translation_useAppLanguage => 'Použite jazyk aplikácie';
@override
String get translation_downloadedModelLabel => 'Stiahnutý model';
@override
String get translation_presetModelLabel =>
'Prednastavený model od Hugging Face';
@override
String get translation_manualUrlLabel =>
'Odkaz na manuál (v elektronickej forme)';
@override
String get translation_downloadModel => 'Stiahnuť model';
@override
String get translation_downloading => 'Stiahnutie...';
@override
String get translation_working => 'Práca...';
@override
String get translation_stop => 'Zastavte';
@override
String get translation_mergingChunks =>
'Sliečenie stiahnutých častí do konečného súboru...';
@override
String get translation_downloadedModels => 'Stiahnuté modely';
@override
String get translation_deleteModel => 'Odstrániť model';
@override
String get translation_modelDownloaded => 'Model pre preklad bol stiahnutý.';
@override
String get translation_downloadStopped => 'Stiahnutie bolo prerušené.';
@override
String translation_downloadFailed(String error) {
return 'Neúspešné stiahnutie: $error';
}
@override
String get translation_enterUrlFirst =>
'Najprv zadajte URL pre konkrétny model.';
@override
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
@ -3552,10 +3623,35 @@ class AppLocalizationsSk extends AppLocalizations {
String get scanner_linuxPairingHidePin => 'Skryť PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth párovací PIN';
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Zadajte PIN pre $deviceName (ak nie je, nechajte prázdne).';
return 'Zadajte PIN pre $deviceName (ak neexistuje, nechajte prázdne).';
}
@override
String get translation_messageTranslation => 'Preklad textu';
@override
String get translation_translateBeforeSending => 'Preložte pred odeslaním';
@override
String get translation_composerEnabledHint =>
'Správy budú preložené, než budú odoslané.';
@override
String get translation_composerDisabledHint =>
'Posielajte správy v pôvodnej písanom jazyku.';
@override
String translation_translateTo(String language) {
return 'Preložte do $language';
}
@override
String get translation_translationOptions => 'Možnosti prekladania';
@override
String get translation_systemLanguage => 'Jazyk systému';
}

View file

@ -3550,17 +3550,114 @@ class AppLocalizationsSl extends AppLocalizations {
String get radioStats_settingsSubtitle =>
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
@override
String get translation_title => 'Prevod';
@override
String get translation_enableTitle => 'Omogočite prevod';
@override
String get translation_enableSubtitle =>
'Prevedite vstopne sporočila in omogočite predhodno prevajanje.';
@override
String get translation_composerTitle => 'Preprištejte, preden pošljete';
@override
String get translation_composerSubtitle =>
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
@override
String get translation_targetLanguage => 'Ciljna jezika';
@override
String get translation_useAppLanguage => 'Uporabite jezik aplikacije';
@override
String get translation_downloadedModelLabel => 'Naložen model';
@override
String get translation_presetModelLabel =>
'Prednastavljeni model Hugging Face';
@override
String get translation_manualUrlLabel => 'URL za ročni model';
@override
String get translation_downloadModel => 'Prenesite model';
@override
String get translation_downloading => 'Izvajanje...';
@override
String get translation_working => 'Delo...';
@override
String get translation_stop => 'Prekliji';
@override
String get translation_mergingChunks =>
'Sklapljanje prenesenih delov v končni datoteko...';
@override
String get translation_downloadedModels => 'Naloženi modeli';
@override
String get translation_deleteModel => 'Izbrisati model';
@override
String get translation_modelDownloaded =>
'Model za prevajanje je bil naložen.';
@override
String get translation_downloadStopped => 'Prenos je bil prekinjen.';
@override
String translation_downloadFailed(String error) {
return 'Izgovoritev ni bila uspešna: $error';
}
@override
String get translation_enterUrlFirst => 'Najprej vnesite URL model.';
@override
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
@override
String get scanner_linuxPairingHidePin => 'Skrij PIN';
String get scanner_linuxPairingHidePin => 'Skrijte PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje';
String get scanner_linuxPairingPinTitle =>
'PIN za združevanje preko Bluetootha';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).';
return 'Vnesite PIN kodo za $deviceName (ostavite prazno, če nimate kode).';
}
@override
String get translation_messageTranslation => 'Prevod sporočila';
@override
String get translation_translateBeforeSending =>
'Preprištejte, preden pošljete';
@override
String get translation_composerEnabledHint =>
'Vsebina sporočil bo prevedena, preden jih pošljemo.';
@override
String get translation_composerDisabledHint =>
'Pošljite sporočila v originalnem tipkanem jeziku.';
@override
String translation_translateTo(String language) {
return 'Prevesti v $language';
}
@override
String get translation_translationOptions => 'Možnosti prevoda';
@override
String get translation_systemLanguage => 'Jezik sistema';
}

View file

@ -3528,16 +3528,112 @@ class AppLocalizationsSv extends AppLocalizations {
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
@override
String get scanner_linuxPairingShowPin => 'Visa PIN';
String get translation_title => 'Översättning';
@override
String get scanner_linuxPairingHidePin => 'Dölj PIN';
String get translation_enableTitle => 'Aktivera översättning';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothparningsPIN';
String get translation_enableSubtitle =>
'Översätt inkommande meddelanden och möjliggör översättning före avsändning.';
@override
String get translation_composerTitle => 'Översätt innan du skickar';
@override
String get translation_composerSubtitle =>
'Styr standardtillståndet för kompositorns översättningsikon.';
@override
String get translation_targetLanguage => 'Målmedvetet språk';
@override
String get translation_useAppLanguage => 'Använd appens språk';
@override
String get translation_downloadedModelLabel => 'Nedladdad modell';
@override
String get translation_presetModelLabel =>
'Fördefinierat Hugging Face-modell';
@override
String get translation_manualUrlLabel => 'Manualens URL';
@override
String get translation_downloadModel => 'Ladda ner modellen';
@override
String get translation_downloading => 'Nedladdning...';
@override
String get translation_working => 'Arbeta...';
@override
String get translation_stop => 'Stopp';
@override
String get translation_mergingChunks =>
'Slå samman de nedladdade delarna till en slutlig fil...';
@override
String get translation_downloadedModels => 'Nedladdade modeller';
@override
String get translation_deleteModel => 'Ta bort modell';
@override
String get translation_modelDownloaded =>
'Översättningsmodellen har laddats ner.';
@override
String get translation_downloadStopped => 'Nedladdningen avbruten.';
@override
String translation_downloadFailed(String error) {
return 'Nedladdning misslyckades: $error';
}
@override
String get translation_enterUrlFirst =>
'Ange först en URL för en specifik modell.';
@override
String get scanner_linuxPairingShowPin => 'Visa PIN-kod';
@override
String get scanner_linuxPairingHidePin => 'Dölj PIN-kod';
@override
String get scanner_linuxPairingPinTitle => 'PIN-kod för Bluetooth-anslutning';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Ange PIN för $deviceName (lämna tomt om ingen).';
return 'Ange PIN-kod för $deviceName (lämna tomt om ingen finns).';
}
@override
String get translation_messageTranslation => 'Meddelandets översättning';
@override
String get translation_translateBeforeSending => 'Översätt innan du skickar';
@override
String get translation_composerEnabledHint =>
'Meddelandena kommer att översättas innan de skickas.';
@override
String get translation_composerDisabledHint =>
'Skicka meddelanden på det ursprungliga, stavade språket.';
@override
String translation_translateTo(String language) {
return 'Översätt till $language';
}
@override
String get translation_translationOptions => 'Översättningsalternativ';
@override
String get translation_systemLanguage => 'Språk för systemet';
}

View file

@ -3585,16 +3585,113 @@ class AppLocalizationsUk extends AppLocalizations {
'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.';
@override
String get scanner_linuxPairingShowPin => 'Показати PIN';
String get translation_title => 'Переклад';
@override
String get scanner_linuxPairingHidePin => 'Приховати PIN';
String get translation_enableTitle => 'Увімкнути переклад';
@override
String get scanner_linuxPairingPinTitle => 'PINкод спарювання Bluetooth';
String get translation_enableSubtitle =>
'Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.';
@override
String get translation_composerTitle => 'Перекладіть перед відправкою';
@override
String get translation_composerSubtitle =>
'Контролює стан ікон перекладу, який використовується за замовчуванням.';
@override
String get translation_targetLanguage => 'Цільова мова';
@override
String get translation_useAppLanguage => 'Використовуйте мову додатку';
@override
String get translation_downloadedModelLabel => 'Завантажений шаблон';
@override
String get translation_presetModelLabel =>
'Заздалегідь налаштований модель від Hugging Face';
@override
String get translation_manualUrlLabel =>
'Посилання на веб-сторінку з інструкцією';
@override
String get translation_downloadModel => 'Завантажити модель';
@override
String get translation_downloading => 'Завантаження...';
@override
String get translation_working => 'Працюю...';
@override
String get translation_stop => 'Припинити';
@override
String get translation_mergingChunks =>
'Об\'єднання завантажених фрагментів у кінцевий файл...';
@override
String get translation_downloadedModels => 'Завантажені моделі';
@override
String get translation_deleteModel => 'Видалити модель';
@override
String get translation_modelDownloaded => 'Модель перекладу завантажена.';
@override
String get translation_downloadStopped => 'Завантаження призупинено.';
@override
String translation_downloadFailed(String error) {
return 'Не вдалося завантажити: $error';
}
@override
String get translation_enterUrlFirst => 'Спочатку введіть URL моделі.';
@override
String get scanner_linuxPairingShowPin => 'Показати PIN-код';
@override
String get scanner_linuxPairingHidePin => 'Приховати PIN-код';
@override
String get scanner_linuxPairingPinTitle =>
'PIN для з\'єднання через Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введіть PIN для $deviceName (залиште порожнім, якщо його немає).';
return 'Введіть PIN-код для $deviceName (залиште поле порожнім, якщо немає).';
}
@override
String get translation_messageTranslation => 'Переклад повідомлення';
@override
String get translation_translateBeforeSending =>
'Перекладіть перед відправкою';
@override
String get translation_composerEnabledHint =>
'Повідомлення будуть перекладені перед відправленням.';
@override
String get translation_composerDisabledHint =>
'Надсилайте повідомлення, використовуючи оригінальний текстовий формат.';
@override
String translation_translateTo(String language) {
return 'Перекласти на $language';
}
@override
String get translation_translationOptions => 'Варіанти перекладу';
@override
String get translation_systemLanguage => 'Мова системи';
}

View file

@ -3300,7 +3300,72 @@ class AppLocalizationsZh extends AppLocalizations {
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
@override
String get scanner_linuxPairingShowPin => '显示 PIN码';
String get translation_title => '翻译';
@override
String get translation_enableTitle => '启用翻译功能';
@override
String get translation_enableSubtitle => '翻译收到的消息,并允许在发送前进行翻译。';
@override
String get translation_composerTitle => '在发送之前进行翻译';
@override
String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。';
@override
String get translation_targetLanguage => '目标语言';
@override
String get translation_useAppLanguage => '使用应用程序语言';
@override
String get translation_downloadedModelLabel => '下载的模型';
@override
String get translation_presetModelLabel => '预设的 Hugging Face 模型';
@override
String get translation_manualUrlLabel => '手动模型网址';
@override
String get translation_downloadModel => '下载模型';
@override
String get translation_downloading => '正在下载...';
@override
String get translation_working => '工作中...';
@override
String get translation_stop => '停止';
@override
String get translation_mergingChunks => '将下载的片段合并成最终文件...';
@override
String get translation_downloadedModels => '下载的模型';
@override
String get translation_deleteModel => '删除模型';
@override
String get translation_modelDownloaded => '翻译模型已下载。';
@override
String get translation_downloadStopped => '下载已停止。';
@override
String translation_downloadFailed(String error) {
return '下载失败:$error';
}
@override
String get translation_enterUrlFirst => '首先,请输入模型的 URL。';
@override
String get scanner_linuxPairingShowPin => '显示PIN码';
@override
String get scanner_linuxPairingHidePin => '隐藏 PIN';
@ -3310,6 +3375,29 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '输入 $deviceName 的 PIN(如果没有,请留空)。';
return '输入 $deviceName 的 PIN 码(如果为空,则留空)。';
}
@override
String get translation_messageTranslation => '消息翻译';
@override
String get translation_translateBeforeSending => '在发送前进行翻译';
@override
String get translation_composerEnabledHint => '消息将在发送前进行翻译。';
@override
String get translation_composerDisabledHint => '使用原始的打字方式发送消息。';
@override
String translation_translateTo(String language) {
return '翻译成 $language';
}
@override
String get translation_translationOptions => '翻译选项';
@override
String get translation_systemLanguage => '系统语言';
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
"radioStats_settingsTile": "Statistieken over radio",
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.",
"translation_enableTitle": "Activeer vertaling",
"translation_title": "Vertaling",
"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_targetLanguage": "Doeltaal",
"translation_downloadedModelLabel": "Gedownloade model",
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
"translation_manualUrlLabel": "URL van de handleiding",
"translation_downloadModel": "Download het model",
"translation_downloading": "Downloaden...",
"translation_working": "Werken...",
"translation_mergingChunks": "Het samenvoegen van de gedownloade stukken tot één eindbestand...",
"translation_stop": "Stoppen",
"translation_downloadedModels": "Gedownloade modellen",
"translation_deleteModel": "Model verwijderen",
"translation_modelDownloaded": "Vertalingmodel gedownload.",
"translation_downloadStopped": "Download is afgebroken.",
"translation_downloadFailed": "Download mislukt: {error}",
"translation_enterUrlFirst": "Voer eerst een URL van een model in.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingPinTitle": "PIN voor Bluetooth-koppeling",
"scanner_linuxPairingHidePin": "Verberg PIN",
"scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is).",
"scanner_linuxPairingShowPin": "Toon PIN",
"scanner_linuxPairingHidePin": "PIN verbergen",
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
"scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN"
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Stuur berichten in de oorspronkelijke, getypte taal.",
"translation_translateBeforeSending": "Vertaal voor verzending",
"translation_composerEnabledHint": "De berichten worden vertaald voordat ze verzonden worden.",
"translation_messageTranslation": "Berichtvertaling",
"translation_translationOptions": "Opties voor vertaling",
"translation_systemLanguage": "Taal van het systeem",
"translation_translateTo": "Vertalen naar {language}"
}

View file

@ -2045,6 +2045,34 @@
"radioStats_stripWaiting": "Pobieranie danych dotyczących radia…",
"radioStats_settingsTile": "Statystyki radiowe",
"radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Przekład przed wysłaniem",
"translation_title": "Tłumaczenie",
"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_targetLanguage": "Język docelowy",
"translation_useAppLanguage": "Użyj języka aplikacji",
"translation_downloadedModelLabel": "Pobudowany model",
"translation_presetModelLabel": "Wspólny model Hugging Face",
"translation_manualUrlLabel": "Adres URL do wersji manualnej",
"translation_downloadModel": "Pobierz model",
"translation_downloading": "Pobieranie...",
"translation_working": "Praca...",
"translation_stop": "Zatrzymaj się",
"translation_mergingChunks": "Scalanie pobranych fragmentów w jeden plik końcowy...",
"translation_downloadedModels": "Pobrane modele",
"translation_deleteModel": "Usuń model",
"translation_modelDownloaded": "Model tłumaczenia został pobrany.",
"translation_downloadStopped": "Pobieranie zakończone.",
"translation_downloadFailed": "Nie udało się pobrać: {error}",
"translation_enterUrlFirst": "Najpierw wprowadź adres URL modelu.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2052,8 +2080,22 @@
}
}
},
"scanner_linuxPairingShowPin": "Pokaż PIN",
"scanner_linuxPairingHidePin": "Ukryj PIN",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth"
"scanner_linuxPairingShowPin": "Wyświetl kod PIN",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pust, jeśli nie jest wymagany).",
"scanner_linuxPairingHidePin": "Ukryj kod PIN",
"scanner_linuxPairingPinTitle": "PIN do sparowania przez Bluetooth",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Komunikaty zostaną przetłumaczone przed wysłaniem.",
"translation_translateBeforeSending": "Przekład przed wysłaniem",
"translation_composerDisabledHint": "Wysyłaj wiadomości w oryginalnym, wpisanym formacie.",
"translation_messageTranslation": "Tłumaczenie wiadomości",
"translation_translationOptions": "Opcje tłumaczenia",
"translation_systemLanguage": "Język systemu",
"translation_translateTo": "Tłumacz na {language}"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
"radioStats_settingsTile": "Estatísticas de rádio",
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Traduza antes de enviar",
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
"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_targetLanguage": "Língua-alvo",
"translation_useAppLanguage": "Utilize o idioma da aplicação",
"translation_downloadedModelLabel": "Modelo baixado",
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
"translation_manualUrlLabel": "URL do modelo manual",
"translation_downloading": "Baixando...",
"translation_downloadModel": "Baixar modelo",
"translation_working": "Trabalhando...",
"translation_stop": "Pare",
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
"translation_downloadedModels": "Modelos baixados",
"translation_deleteModel": "Excluir modelo",
"translation_modelDownloaded": "Modelo de tradução baixado.",
"translation_downloadStopped": "Download interrompido.",
"translation_downloadFailed": "Falha na descarga: {error}",
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingHidePin": "Esconder o PIN",
"scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth"
"scanner_linuxPairingPinTitle": "PIN de pareamento Bluetooth",
"scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Tradução da mensagem",
"translation_translateBeforeSending": "Traduzir antes de enviar",
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
"translation_translateTo": "Traduzir para {language}",
"translation_translationOptions": "Opções de tradução",
"translation_systemLanguage": "Idioma do sistema"
}

View file

@ -1247,6 +1247,34 @@
"radioStats_stripWaiting": "Получение данных о радио…",
"radioStats_settingsTile": "Статистика радиовещания",
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
"translation_composerTitle": "Переводить перед отправкой",
"translation_title": "Перевод",
"translation_enableTitle": "Включить перевод",
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
"translation_targetLanguage": "Целевой язык",
"translation_useAppLanguage": "Используйте язык приложения",
"translation_downloadedModelLabel": "Загруженная модель",
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
"translation_manualUrlLabel": "Ссылка на руководство",
"translation_downloadModel": "Скачать модель",
"translation_downloading": "Загрузка...",
"translation_stop": "Прекратите",
"translation_working": "Работа...",
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
"translation_downloadedModels": "Загруженные модели",
"translation_deleteModel": "Удалить модель",
"translation_modelDownloaded": "Модель перевода загружена.",
"translation_downloadStopped": "Процесс загрузки был прерван.",
"translation_downloadFailed": "Не удалось скачать: {error}",
"translation_enterUrlFirst": "Сначала введите URL модели.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -1254,8 +1282,22 @@
}
}
},
"scanner_linuxPairingShowPin": "Показать PIN",
"scanner_linuxPairingPinPrompt": "Введите PINкод для {deviceName} (оставьте пустым, если нет).",
"scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PINкод сопряжения Bluetooth"
"scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).",
"scanner_linuxPairingHidePin": "Скрыть PIN-код",
"scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth",
"scanner_linuxPairingShowPin": "Показать PIN-код",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "Перевести перед отправкой",
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
"translation_messageTranslation": "Перевод сообщения",
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
"translation_translateTo": "Перевести на {language}",
"translation_translationOptions": "Варианты перевода",
"translation_systemLanguage": "Язык системы"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
"translation_enableTitle": "Aktivovať preklad",
"translation_composerTitle": "Preložte pred odeslaním",
"translation_title": "Preklad",
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
"translation_targetLanguage": "Cieľový jazyk",
"translation_useAppLanguage": "Použite jazyk aplikácie",
"translation_downloadedModelLabel": "Stiahnutý model",
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
"translation_downloadModel": "Stiahnuť model",
"translation_downloading": "Stiahnutie...",
"translation_working": "Práca...",
"translation_stop": "Zastavte",
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
"translation_downloadedModels": "Stiahnuté modely",
"translation_deleteModel": "Odstrániť model",
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).",
"scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingHidePin": "Skryť PIN",
"scanner_linuxPairingPinTitle": "Bluetooth párovací PIN"
"scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
"translation_translateBeforeSending": "Preložte pred odeslaním",
"translation_messageTranslation": "Preklad textu",
"translation_translateTo": "Preložte do {language}",
"translation_translationOptions": "Možnosti prekladania",
"translation_systemLanguage": "Jazyk systému"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
"radioStats_settingsTile": "Radijske statistike",
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Preprištejte, preden pošljete",
"translation_title": "Prevod",
"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_targetLanguage": "Ciljna jezika",
"translation_useAppLanguage": "Uporabite jezik aplikacije",
"translation_downloadedModelLabel": "Naložen model",
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
"translation_manualUrlLabel": "URL za ročni model",
"translation_downloadModel": "Prenesite model",
"translation_downloading": "Izvajanje...",
"translation_working": "Delo...",
"translation_stop": "Prekliji",
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
"translation_downloadedModels": "Naloženi modeli",
"translation_deleteModel": "Izbrisati model",
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
"translation_downloadStopped": "Prenos je bil prekinjen.",
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
"translation_enterUrlFirst": "Najprej vnesite URL model.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingHidePin": "Skrijte PIN",
"scanner_linuxPairingShowPin": "Prikaži PIN",
"scanner_linuxPairingHidePin": "Skrij PIN",
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje"
"scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).",
"scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
"translation_messageTranslation": "Prevod sporočila",
"translation_translateTo": "Prevesti v {language}",
"translation_translationOptions": "Možnosti prevoda",
"translation_systemLanguage": "Jezik sistema"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Hämtar radiostatistik…",
"radioStats_settingsTile": "Radiostation",
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
"translation_enableTitle": "Aktivera översättning",
"translation_title": "Översättning",
"translation_composerTitle": "Översätt innan du skickar",
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
"translation_targetLanguage": "Målmedvetet språk",
"translation_useAppLanguage": "Använd appens språk",
"translation_downloadedModelLabel": "Nedladdad modell",
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
"translation_manualUrlLabel": "Manualens URL",
"translation_downloadModel": "Ladda ner modellen",
"translation_downloading": "Nedladdning...",
"translation_working": "Arbeta...",
"translation_stop": "Stopp",
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
"translation_downloadedModels": "Nedladdade modeller",
"translation_deleteModel": "Ta bort modell",
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
"translation_downloadStopped": "Nedladdningen avbruten.",
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingShowPin": "Visa PIN",
"scanner_linuxPairingPinTitle": "BluetoothparningsPIN",
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"scanner_linuxPairingHidePin": "Dölj PIN"
"scanner_linuxPairingPinPrompt": "Ange PIN-kod för {deviceName} (lämna tomt om ingen finns).",
"scanner_linuxPairingPinTitle": "PIN-kod för Bluetooth-anslutning",
"scanner_linuxPairingShowPin": "Visa PIN-kod",
"scanner_linuxPairingHidePin": "Dölj PIN-kod",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
"translation_translateBeforeSending": "Översätt innan du skickar",
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
"translation_messageTranslation": "Meddelandets översättning",
"translation_translateTo": "Översätt till {language}",
"translation_translationOptions": "Översättningsalternativ",
"translation_systemLanguage": "Språk för systemet"
}

View file

@ -2007,6 +2007,34 @@
"radioStats_stripWaiting": "Отримано статистику радіо…",
"radioStats_settingsTile": "Дані про радіостанції",
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Перекладіть перед відправкою",
"translation_title": "Переклад",
"translation_enableTitle": "Увімкнути переклад",
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
"translation_targetLanguage": "Цільова мова",
"translation_useAppLanguage": "Використовуйте мову додатку",
"translation_downloadedModelLabel": "Завантажений шаблон",
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
"translation_downloadModel": "Завантажити модель",
"translation_downloading": "Завантаження...",
"translation_working": "Працюю...",
"translation_stop": "Припинити",
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
"translation_downloadedModels": "Завантажені моделі",
"translation_deleteModel": "Видалити модель",
"translation_modelDownloaded": "Модель перекладу завантажена.",
"translation_downloadStopped": "Завантаження призупинено.",
"translation_downloadFailed": "Не вдалося завантажити: {error}",
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2014,8 +2042,22 @@
}
}
},
"scanner_linuxPairingPinTitle": "PINкод спарювання Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN",
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN"
"scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN-код",
"scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).",
"scanner_linuxPairingHidePin": "Приховати PIN-код",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
"translation_messageTranslation": "Переклад повідомлення",
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
"translation_translateBeforeSending": "Перекладіть перед відправкою",
"translation_translateTo": "Перекласти на {language}",
"translation_translationOptions": "Варіанти перекладу",
"translation_systemLanguage": "Мова системи"
}

View file

@ -2012,6 +2012,34 @@
"radioStats_stripWaiting": "正在获取收音机数据…",
"radioStats_settingsTile": "广播统计数据",
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "翻译",
"translation_enableSubtitle": "翻译收到的消息,并允许在发送前进行翻译。",
"translation_composerTitle": "在发送之前进行翻译",
"translation_enableTitle": "启用翻译功能",
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
"translation_targetLanguage": "目标语言",
"translation_useAppLanguage": "使用应用程序语言",
"translation_downloadedModelLabel": "下载的模型",
"translation_presetModelLabel": "预设的 Hugging Face 模型",
"translation_downloadModel": "下载模型",
"translation_manualUrlLabel": "手动模型网址",
"translation_downloading": "正在下载...",
"translation_working": "工作中...",
"translation_stop": "停止",
"translation_mergingChunks": "将下载的片段合并成最终文件...",
"translation_downloadedModels": "下载的模型",
"translation_deleteModel": "删除模型",
"translation_modelDownloaded": "翻译模型已下载。",
"translation_downloadStopped": "下载已停止。",
"translation_downloadFailed": "下载失败:{error}",
"translation_enterUrlFirst": "首先,请输入模型的 URL。",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
@ -2019,8 +2047,22 @@
}
}
},
"scanner_linuxPairingShowPin": "显示 PIN码",
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN如果没有请留空。",
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
"scanner_linuxPairingHidePin": "隐藏 PIN"
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
"scanner_linuxPairingHidePin": "隐藏 PIN",
"scanner_linuxPairingShowPin": "显示PIN码",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "使用原始的打字方式发送消息。",
"translation_messageTranslation": "消息翻译",
"translation_composerEnabledHint": "消息将在发送前进行翻译。",
"translation_translateBeforeSending": "在发送前进行翻译",
"translation_translateTo": "翻译成 {language}",
"translation_translationOptions": "翻译选项",
"translation_systemLanguage": "系统语言"
}

View file

@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart';
import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
@ -41,6 +42,7 @@ void main() async {
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage);
@ -60,6 +62,7 @@ void main() async {
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
await translationService.refreshDownloadedModels();
await uiViewStateService.initialize();
await timeoutPredictionService.initialize();
@ -68,6 +71,7 @@ void main() async {
retryService: retryService,
pathHistoryService: pathHistoryService,
appSettingsService: appSettingsService,
translationService: translationService,
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
@ -93,6 +97,7 @@ void main() async {
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
translationService: translationService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService,
),
@ -130,6 +135,7 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final TranslationService translationService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService;
@ -144,6 +150,7 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.translationService,
required this.uiViewStateService,
required this.timeoutPredictionService,
});
@ -159,6 +166,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: translationService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),

View file

@ -1,3 +1,5 @@
import 'translation_support.dart';
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
@ -49,6 +51,12 @@ class AppSettings {
final String tcpServerAddress;
final int tcpServerPort;
final bool jumpToOldestUnread;
final bool translationEnabled;
final String? translationTargetLanguageCode;
final bool composerTranslationEnabled;
final String? translationModelSourceUrl;
final String? translationSelectedModelId;
final List<TranslationModelRecord> translationDownloadedModels;
AppSettings({
this.clearPathOnMaxRetry = false,
@ -86,9 +94,16 @@ class AppSettings {
this.tcpServerAddress = '',
this.tcpServerPort = 0,
this.jumpToOldestUnread = false,
this.translationEnabled = false,
this.translationTargetLanguageCode,
this.composerTranslationEnabled = false,
this.translationModelSourceUrl,
this.translationSelectedModelId,
List<TranslationModelRecord>? translationDownloadedModels,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
mutedChannels = mutedChannels ?? {},
translationDownloadedModels = translationDownloadedModels ?? const [];
Map<String, dynamic> toJson() {
return {
@ -127,6 +142,14 @@ class AppSettings {
'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort,
'jump_to_oldest_unread': jumpToOldestUnread,
'translation_enabled': translationEnabled,
'translation_target_language_code': translationTargetLanguageCode,
'composer_translation_enabled': composerTranslationEnabled,
'translation_model_source_url': translationModelSourceUrl,
'translation_selected_model_id': translationSelectedModelId,
'translation_downloaded_models': translationDownloadedModels
.map((model) => model.toJson())
.toList(),
};
}
@ -196,6 +219,24 @@ class AppSettings {
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
translationEnabled: json['translation_enabled'] as bool? ?? false,
translationTargetLanguageCode:
json['translation_target_language_code'] as String?,
composerTranslationEnabled:
json['composer_translation_enabled'] as bool? ?? false,
translationModelSourceUrl:
json['translation_model_source_url'] as String?,
translationSelectedModelId:
json['translation_selected_model_id'] as String?,
translationDownloadedModels:
(json['translation_downloaded_models'] as List<dynamic>?)
?.map(
(entry) => TranslationModelRecord.fromJson(
Map<String, dynamic>.from(entry as Map),
),
)
.toList() ??
const [],
);
}
@ -235,6 +276,12 @@ class AppSettings {
String? tcpServerAddress,
int? tcpServerPort,
bool? jumpToOldestUnread,
bool? translationEnabled,
Object? translationTargetLanguageCode = _unset,
bool? composerTranslationEnabled,
Object? translationModelSourceUrl = _unset,
Object? translationSelectedModelId = _unset,
List<TranslationModelRecord>? translationDownloadedModels,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@ -284,6 +331,20 @@ class AppSettings {
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
translationEnabled: translationEnabled ?? this.translationEnabled,
translationTargetLanguageCode: translationTargetLanguageCode == _unset
? this.translationTargetLanguageCode
: translationTargetLanguageCode as String?,
composerTranslationEnabled:
composerTranslationEnabled ?? this.composerTranslationEnabled,
translationModelSourceUrl: translationModelSourceUrl == _unset
? this.translationModelSourceUrl
: translationModelSourceUrl as String?,
translationSelectedModelId: translationSelectedModelId == _unset
? this.translationSelectedModelId
: translationSelectedModelId as String?,
translationDownloadedModels:
translationDownloadedModels ?? this.translationDownloadedModels,
);
}
}

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import 'translation_support.dart';
import '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed }
@ -24,9 +25,16 @@ class Repeat {
}
class ChannelMessage {
static const Object _unset = Object();
final Uint8List? senderKey;
final String senderName;
final String text;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
final DateTime timestamp;
final bool isOutgoing;
final ChannelMessageStatus status;
@ -47,6 +55,11 @@ class ChannelMessage {
this.senderKey,
required this.senderName,
required this.text,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
required this.timestamp,
required this.isOutgoing,
this.status = ChannelMessageStatus.pending,
@ -86,12 +99,30 @@ class ChannelMessage {
String? replyToMessageId,
String? replyToSenderName,
String? replyToText,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
}) {
return ChannelMessage(
senderKey: senderKey,
senderName: senderName,
text: text,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
timestamp: timestamp,
isOutgoing: isOutgoing,
status: status ?? this.status,
@ -191,12 +222,18 @@ class ChannelMessage {
static ChannelMessage outgoing(
String text,
String senderName,
int channelIndex,
) {
int channelIndex, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
}) {
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: ChannelMessageStatus.pending,

View file

@ -1,19 +1,27 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import 'translation_support.dart';
enum MessageStatus { pending, sent, delivered, failed }
class Message {
static const Object _unset = Object();
final Uint8List senderKey;
final String text;
final DateTime timestamp;
final bool isOutgoing;
final bool isCli;
final MessageStatus status;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
// NEW: Retry logic fields
final String? messageId;
final String messageId;
final int retryCount;
final int? estimatedTimeoutMs;
final int? expectedAckHash;
@ -33,7 +41,12 @@ class Message {
required this.isOutgoing,
this.isCli = false,
this.status = MessageStatus.pending,
this.messageId,
String? messageId,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
this.retryCount = 0,
this.estimatedTimeoutMs,
this.expectedAckHash,
@ -45,7 +58,10 @@ class Message {
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
}) : pathBytes = pathBytes ?? Uint8List(0),
}) : messageId =
messageId ??
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {},
reactionStatuses = reactionStatuses ?? {};
@ -63,6 +79,11 @@ class Message {
int? pathLength,
Uint8List? pathBytes,
bool? isCli,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
Uint8List? fourByteRoomContactKey,
@ -75,6 +96,19 @@ class Message {
isCli: isCli ?? this.isCli,
status: status ?? this.status,
messageId: messageId,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
retryCount: retryCount ?? this.retryCount,
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
@ -124,12 +158,18 @@ class Message {
static Message outgoing(
Uint8List recipientKey,
String text, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
int? pathLength,
Uint8List? pathBytes,
}) {
return Message(
senderKey: recipientKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
isCli: false,

View file

@ -0,0 +1,136 @@
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
extension MessageTranslationStatusValue on MessageTranslationStatus {
String get value {
switch (this) {
case MessageTranslationStatus.pending:
return 'pending';
case MessageTranslationStatus.completed:
return 'completed';
case MessageTranslationStatus.failed:
return 'failed';
case MessageTranslationStatus.skipped:
return 'skipped';
case MessageTranslationStatus.none:
return 'none';
}
}
}
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
if (value is! String) {
return MessageTranslationStatus.none;
}
for (final status in MessageTranslationStatus.values) {
if (status.value == value) {
return status;
}
}
return MessageTranslationStatus.none;
}
class TranslationModelRecord {
final String id;
final String name;
final String sourceUrl;
final String localPath;
final DateTime downloadedAt;
final int fileSizeBytes;
const TranslationModelRecord({
required this.id,
required this.name,
required this.sourceUrl,
required this.localPath,
required this.downloadedAt,
required this.fileSizeBytes,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'source_url': sourceUrl,
'local_path': localPath,
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
'file_size_bytes': fileSizeBytes,
};
}
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
return TranslationModelRecord(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
sourceUrl: json['source_url'] as String? ?? '',
localPath: json['local_path'] as String? ?? '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
json['downloaded_at'] as int? ?? 0,
),
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
);
}
}
String translationModelFriendlyName(TranslationModelRecord model) {
switch (model.id) {
case 'hy-mt1.5-1.8b-q4_k_m':
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
case 'hy-mt1.5-1.8b-q6_k':
return 'Tencent HY-MT 1.5 1.8B Q6_K';
default:
final trimmed = model.name.trim();
if (trimmed.endsWith('.gguf')) {
return trimmed.substring(0, trimmed.length - 5);
}
return trimmed.isEmpty ? model.id : trimmed;
}
}
class TranslationLanguageOption {
final String code;
final String label;
const TranslationLanguageOption({required this.code, required this.label});
}
const List<TranslationLanguageOption> supportedTranslationLanguages = [
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
TranslationLanguageOption(code: 'de', label: 'German'),
TranslationLanguageOption(code: 'en', label: 'English'),
TranslationLanguageOption(code: 'es', label: 'Spanish'),
TranslationLanguageOption(code: 'fr', label: 'French'),
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
TranslationLanguageOption(code: 'it', label: 'Italian'),
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
TranslationLanguageOption(code: 'ko', label: 'Korean'),
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
TranslationLanguageOption(code: 'pl', label: 'Polish'),
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
TranslationLanguageOption(code: 'ru', label: 'Russian'),
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
];
final List<TranslationModelRecord> translationPresetModels = [
TranslationModelRecord(
id: 'hy-mt1.5-1.8b-q4_k_m',
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
sourceUrl:
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
localPath: '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
fileSizeBytes: 0,
),
TranslationModelRecord(
id: 'hy-mt1.5-1.8b-q6_k',
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
sourceUrl:
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
localPath: '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
fileSizeBytes: 0,
),
];

View file

@ -1,11 +1,14 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/notification_service.dart';
import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'map_cache_screen.dart';
@ -21,26 +24,46 @@ class AppSettingsScreen extends StatelessWidget {
),
body: SafeArea(
top: false,
child: Consumer2<AppSettingsService, MeshCoreConnector>(
builder: (context, settingsService, connector, child) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAppearanceCard(context, settingsService),
const SizedBox(height: 16),
_buildNotificationsCard(context, settingsService),
const SizedBox(height: 16),
_buildMessagingCard(context, settingsService),
const SizedBox(height: 16),
_buildBatteryCard(context, settingsService, connector),
const SizedBox(height: 16),
_buildMapSettingsCard(context, settingsService),
const SizedBox(height: 16),
_buildDebugCard(context, settingsService),
],
);
},
),
child:
Consumer3<
AppSettingsService,
MeshCoreConnector,
TranslationService
>(
builder:
(
context,
settingsService,
connector,
translationService,
child,
) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAppearanceCard(context, settingsService),
const SizedBox(height: 16),
_buildNotificationsCard(context, settingsService),
const SizedBox(height: 16),
_buildMessagingCard(context, settingsService),
const SizedBox(height: 16),
if (!kIsWeb) ...[
_buildTranslationCard(
context,
settingsService,
translationService,
),
const SizedBox(height: 16),
],
_buildBatteryCard(context, settingsService, connector),
const SizedBox(height: 16),
_buildMapSettingsCard(context, settingsService),
const SizedBox(height: 16),
_buildDebugCard(context, settingsService),
],
);
},
),
),
);
}
@ -530,6 +553,211 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildTranslationCard(
BuildContext context,
AppSettingsService settingsService,
TranslationService translationService,
) {
final settings = settingsService.settings;
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
context.l10n.translation_title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
SwitchListTile(
secondary: const Icon(Icons.translate),
title: Text(context.l10n.translation_enableTitle),
subtitle: Text(context.l10n.translation_enableSubtitle),
value: settings.translationEnabled,
onChanged: settingsService.setTranslationEnabled,
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.outgoing_mail),
title: Text(context.l10n.translation_composerTitle),
subtitle: Text(context.l10n.translation_composerSubtitle),
value: settings.composerTranslationEnabled,
onChanged: settingsService.setComposerTranslationEnabled,
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.language),
title: Text(context.l10n.translation_targetLanguage),
subtitle: Text(
_translationLanguageLabel(
context,
settings.translationTargetLanguageCode,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: () =>
_showTranslationLanguageDialog(context, settingsService),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: DropdownButtonFormField<String>(
initialValue: settings.translationSelectedModelId,
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.translation_downloadedModelLabel,
border: const OutlineInputBorder(),
),
items: [
for (final model in settings.translationDownloadedModels)
DropdownMenuItem(
value: model.id,
child: Text(translationModelFriendlyName(model)),
),
],
onChanged: settings.translationDownloadedModels.isEmpty
? null
: (value) {
settingsService.setTranslationSelectedModelId(value);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: DropdownButtonFormField<String>(
initialValue: null,
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.translation_presetModelLabel,
border: const OutlineInputBorder(),
),
items: [
for (final preset in translationPresetModels)
DropdownMenuItem(
value: preset.sourceUrl,
child: Text(translationModelFriendlyName(preset)),
),
],
onChanged: translationService.isBusy
? null
: (value) async {
if (value == null) return;
final preset = translationPresetModels.firstWhere(
(entry) => entry.sourceUrl == value,
);
await _downloadTranslationModel(
context,
translationService,
settingsService,
sourceUrl: preset.sourceUrl,
fileName: preset.name,
id: preset.id,
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
children: [
_TranslationUrlField(
initialValue: settings.translationModelSourceUrl ?? '',
onChanged: settingsService.setTranslationModelSourceUrl,
onDownload: translationService.isBusy
? null
: (url) => _downloadTranslationModel(
context,
translationService,
settingsService,
sourceUrl: url,
),
downloadLabel: translationService.isDownloading
? context.l10n.translation_downloading
: translationService.isBusy
? context.l10n.translation_working
: context.l10n.translation_downloadModel,
isDownloading: translationService.isDownloading,
onCancel: translationService.cancelDownload,
labelText: context.l10n.translation_manualUrlLabel,
stopLabel: context.l10n.translation_stop,
),
if (translationService.isDownloading) ...[
const SizedBox(height: 12),
LinearProgressIndicator(
value:
translationService.downloadFileName ==
'Merging chunks...'
? null
: translationService.downloadProgress,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: Text(
_downloadProgressLabel(context, translationService),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
if (settings.translationDownloadedModels.isNotEmpty) ...[
const SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.translation_downloadedModels,
style: Theme.of(context).textTheme.titleSmall,
),
),
const SizedBox(height: 8),
for (final model in settings.translationDownloadedModels)
Card.outlined(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
leading: Icon(
model.id == settings.translationSelectedModelId
? Icons.check_circle
: Icons.memory_outlined,
),
title: Text(translationModelFriendlyName(model)),
subtitle: Text(_downloadedModelLabel(model)),
trailing: IconButton(
tooltip: context.l10n.translation_deleteModel,
onPressed: translationService.isBusy
? null
: () => _deleteTranslationModel(
context,
translationService,
model,
),
icon: const Icon(Icons.delete_outline),
),
onTap: () => settingsService
.setTranslationSelectedModelId(model.id),
),
),
],
if (translationService.lastError != null) ...[
const SizedBox(height: 8),
Text(
translationService.lastError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
],
],
),
),
],
),
);
}
// Fixed rendering issues
Widget _buildBatteryCard(
BuildContext context,
@ -910,6 +1138,124 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showTranslationLanguageDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => _TranslationLanguageDialogContent(
currentLanguageCode:
settingsService.settings.translationTargetLanguageCode,
onLanguageSelected: (value) {
settingsService.setTranslationTargetLanguageCode(value);
Navigator.pop(context);
},
),
);
}
Future<void> _downloadTranslationModel(
BuildContext context,
TranslationService translationService,
AppSettingsService settingsService, {
required String sourceUrl,
String? fileName,
String? id,
}) async {
if (sourceUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_enterUrlFirst)),
);
return;
}
try {
await translationService.downloadModel(
sourceUrl: sourceUrl,
fileName: fileName,
id: id,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_modelDownloaded)),
);
await settingsService.setTranslationEnabled(true);
} on TranslationDownloadCancelled {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_downloadStopped)),
);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.translation_downloadFailed(error.toString()),
),
),
);
}
}
String _translationLanguageLabel(BuildContext context, String? languageCode) {
if (languageCode == null || languageCode.isEmpty) {
return context.l10n.translation_useAppLanguage;
}
for (final option in supportedTranslationLanguages) {
if (option.code == languageCode) {
return option.label;
}
}
return languageCode.toUpperCase();
}
String _downloadProgressLabel(
BuildContext context,
TranslationService translationService,
) {
final fileName = translationService.downloadFileName ?? 'Model';
if (fileName == 'Merging chunks...') {
return context.l10n.translation_mergingChunks;
}
final currentMb = translationService.downloadedBytes / (1024 * 1024);
final totalBytes = translationService.downloadTotalBytes;
if (totalBytes == null || totalBytes <= 0) {
return '$fileName: ${currentMb.toStringAsFixed(1)} MB';
}
final totalMb = totalBytes / (1024 * 1024);
final percent = ((translationService.downloadProgress ?? 0) * 100)
.toStringAsFixed(0);
return '$fileName: ${currentMb.toStringAsFixed(1)} / ${totalMb.toStringAsFixed(1)} MB ($percent%)';
}
Future<void> _deleteTranslationModel(
BuildContext context,
TranslationService translationService,
TranslationModelRecord model,
) async {
try {
await translationService.removeModel(model);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
// TODO: l10n
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
),
);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Delete failed: $error')),
); // TODO: l10n
}
}
String _downloadedModelLabel(TranslationModelRecord model) {
final sizeMb = model.fileSizeBytes / (1024 * 1024);
final source = model.sourceUrl.isEmpty ? model.name : model.sourceUrl;
return '${sizeMb.toStringAsFixed(1)} MB • $source';
}
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
@ -950,3 +1296,176 @@ class AppSettingsScreen extends StatelessWidget {
);
}
}
/// Owns the [TextEditingController] for the manual model URL field so it
/// survives rebuilds of the parent [Consumer3].
class _TranslationUrlField extends StatefulWidget {
const _TranslationUrlField({
required this.initialValue,
required this.onChanged,
required this.onDownload,
required this.downloadLabel,
required this.isDownloading,
required this.onCancel,
required this.labelText,
required this.stopLabel,
});
final String initialValue;
final ValueChanged<String> onChanged;
final void Function(String url)? onDownload;
final String downloadLabel;
final bool isDownloading;
final VoidCallback onCancel;
final String labelText;
final String stopLabel;
@override
State<_TranslationUrlField> createState() => _TranslationUrlFieldState();
}
class _TranslationUrlFieldState extends State<_TranslationUrlField> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialValue);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: widget.labelText,
border: const OutlineInputBorder(),
),
onChanged: widget.onChanged,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: widget.onDownload == null
? null
: () => widget.onDownload!(_controller.text.trim()),
icon: const Icon(Icons.download),
label: Text(widget.downloadLabel),
),
),
if (widget.isDownloading) ...[
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: widget.onCancel,
icon: const Icon(Icons.stop_circle_outlined),
label: Text(widget.stopLabel),
),
],
],
),
],
);
}
}
/// Dialog content for choosing the translation target language.
/// Owns the search [TextEditingController] so it is properly disposed.
class _TranslationLanguageDialogContent extends StatefulWidget {
const _TranslationLanguageDialogContent({
required this.currentLanguageCode,
required this.onLanguageSelected,
});
final String? currentLanguageCode;
final ValueChanged<String> onLanguageSelected;
@override
State<_TranslationLanguageDialogContent> createState() =>
_TranslationLanguageDialogContentState();
}
class _TranslationLanguageDialogContentState
extends State<_TranslationLanguageDialogContent> {
late final TextEditingController _searchController;
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
@override
void initState() {
super.initState();
_searchController = TextEditingController();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(context.l10n.translation_targetLanguage),
content: SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _searchController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
final normalized = value.trim().toLowerCase();
setState(() {
_filtered = supportedTranslationLanguages.where((option) {
return option.label.toLowerCase().contains(normalized) ||
option.code.toLowerCase().contains(normalized);
}).toList();
});
},
),
const SizedBox(height: 12),
Flexible(
child: RadioGroup<String>(
groupValue: widget.currentLanguageCode,
onChanged: (value) {
if (value == null) return;
widget.onLanguageSelected(value);
},
child: ListView(
shrinkWrap: true,
children: [
for (final option in _filtered)
RadioListTile<String>(
value: option.code,
title: Text(option.label),
subtitle: Text(option.code.toUpperCase()),
),
],
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
);
}
}

View file

@ -11,22 +11,25 @@ import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/link_handler.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart';
import '../utils/emoji_utils.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart';
import '../widgets/message_status_icon.dart';
import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@ -354,6 +357,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text);
final poi = _parsePoiMessage(message.text);
final translatedDisplayText =
message.translatedText != null &&
message.translatedText!.trim().isNotEmpty
? message.translatedText!.trim()
: message.text;
final originalDisplayText = message.isOutgoing
? message.originalText
: (translatedDisplayText != message.text ? message.text : null);
final displayPath = message.pathBytes.isNotEmpty
? message.pathBytes
: (message.pathVariants.isNotEmpty
@ -504,12 +515,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: message.text,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
originalStyle: TextStyle(
fontSize: bodyFontSize * textScale,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.72),
),
),
),
if (!enableTracing && isOutgoing) ...[
@ -994,6 +1011,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildMessageComposer() {
final connector = context.watch<MeshCoreConnector>();
final maxBytes = maxChannelMessageBytes(connector.selfName);
final settings = context.watch<AppSettingsService>().settings;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -1025,6 +1043,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
if (settings.translationEnabled)
MessageTranslationButton(
enabled: settings.composerTranslationEnabled,
languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
@ -1112,7 +1136,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
void _sendMessage() {
Future<void> _showTranslationOptions() async {
final settingsService = context.read<AppSettingsService>();
final settings = settingsService.settings;
await showMessageTranslationSheet(
context: context,
enabled: settings.composerTranslationEnabled,
selectedLanguageCode: settings.translationTargetLanguageCode,
onEnabledChanged: settingsService.setComposerTranslationEnabled,
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
);
}
Future<void> _sendMessage() async {
final text = _textController.text.trim();
if (text.isEmpty) return;
@ -1126,11 +1162,46 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
_lastChannelSendAt = now;
// Capture reply state before clearing, then clear input synchronously
// to prevent double-send during async translation.
final replyingTo = _replyingToMessage;
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
String messageText = text;
if (_replyingToMessage != null) {
messageText = '@[${_replyingToMessage!.senderName}] $text';
String? originalText;
String? translatedLanguageCode;
String? translationModelId;
if (settings.translationEnabled) {
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
settings.languageOverride,
);
if (translationService.shouldTranslateOutgoing(
text: text,
targetLanguageCode: targetLanguageCode,
)) {
final result = await translationService.translateOutgoingText(
text: text,
targetLanguageCode: targetLanguageCode,
);
if (!mounted) return;
if (result != null &&
result.status == MessageTranslationStatus.completed &&
result.translatedText.isNotEmpty) {
messageText = result.translatedText;
originalText = text;
translatedLanguageCode = result.targetLanguageCode;
translationModelId = result.modelId;
}
}
}
if (replyingTo != null) {
messageText = '@[${replyingTo.senderName}] $messageText';
}
final maxBytes = maxChannelMessageBytes(connector.selfName);
@ -1141,10 +1212,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return;
}
connector.sendChannelMessage(widget.channel, messageText);
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
connector.sendChannelMessage(
widget.channel,
messageText,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
}
String _formatTime(DateTime time) {

View file

@ -16,16 +16,17 @@ import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_history.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart';
import '../services/translation_service.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart';
import 'channel_message_path_screen.dart';
@ -35,8 +36,10 @@ import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart';
import '../widgets/path_selection_dialog.dart';
import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart';
import '../l10n/l10n.dart';
import 'telemetry_screen.dart';
@ -495,6 +498,7 @@ class _ChatScreenState extends State<ChatScreen> {
Widget _buildInputBar(MeshCoreConnector connector) {
final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme;
final settings = context.watch<AppSettingsService>().settings;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@ -509,6 +513,12 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
if (settings.translationEnabled)
MessageTranslationButton(
enabled: settings.composerTranslationEnabled,
languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
@ -606,7 +616,19 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
void _sendMessage(MeshCoreConnector connector) {
Future<void> _showTranslationOptions() async {
final settingsService = context.read<AppSettingsService>();
final settings = settingsService.settings;
await showMessageTranslationSheet(
context: context,
enabled: settings.composerTranslationEnabled,
selectedLanguageCode: settings.translationTargetLanguageCode,
onEnabledChanged: settingsService.setComposerTranslationEnabled,
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
);
}
Future<void> _sendMessage(MeshCoreConnector connector) async {
final text = _textController.text.trim();
if (text.isEmpty) return;
@ -620,17 +642,54 @@ class _ChatScreenState extends State<ChatScreen> {
}
_lastTextSendAt = now;
// Clear input synchronously to prevent double-send
_textController.clear();
_textFieldFocusNode.requestFocus();
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
var outgoingText = text;
String? originalText;
String? translatedLanguageCode;
String? translationModelId;
if (settings.translationEnabled) {
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
settings.languageOverride,
);
if (translationService.shouldTranslateOutgoing(
text: text,
targetLanguageCode: targetLanguageCode,
)) {
final result = await translationService.translateOutgoingText(
text: text,
targetLanguageCode: targetLanguageCode,
);
if (!mounted) return;
if (result != null &&
result.status == MessageTranslationStatus.completed &&
result.translatedText.isNotEmpty) {
outgoingText = result.translatedText;
originalText = text;
translatedLanguageCode = result.targetLanguageCode;
translationModelId = result.modelId;
}
}
}
final maxBytes = maxContactMessageBytes();
if (utf8.encode(text).length > maxBytes) {
if (utf8.encode(outgoingText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
);
return;
}
connector.sendMessage(_resolveContact(connector), text);
_textController.clear();
_textFieldFocusNode.requestFocus();
connector.sendMessage(
_resolveContact(connector),
outgoingText,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
}
void _showPathHistory(BuildContext context) {
@ -1533,6 +1592,14 @@ class _MessageBubble extends StatelessWidget {
if (isRoomServer && !isOutgoing) {
messageText = message.text.substring(4.clamp(0, message.text.length));
}
final translatedDisplayText =
message.translatedText != null &&
message.translatedText!.trim().isNotEmpty
? message.translatedText!.trim()
: messageText;
final originalDisplayText = isOutgoing
? message.originalText
: (translatedDisplayText != messageText ? messageText : null);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
@ -1662,13 +1729,17 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: messageText,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
color: textColor,
fontSize: bodyFontSize * textScale,
),
originalStyle: TextStyle(
color: textColor.withValues(alpha: 0.78),
fontSize: bodyFontSize * textScale,
),
),
),
if (!enableTracing && isOutgoing) ...[

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../storage/prefs_manager.dart';
import '../utils/app_logger.dart';
@ -222,4 +223,34 @@ class AppSettingsService extends ChangeNotifier {
Future<void> setJumpToOldestUnread(bool value) async {
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
}
Future<void> setTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(translationEnabled: value));
}
Future<void> setTranslationTargetLanguageCode(String? value) async {
await updateSettings(
_settings.copyWith(translationTargetLanguageCode: value),
);
}
Future<void> setComposerTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(composerTranslationEnabled: value));
}
Future<void> setTranslationModelSourceUrl(String? value) async {
await updateSettings(_settings.copyWith(translationModelSourceUrl: value));
}
Future<void> setTranslationSelectedModelId(String? value) async {
await updateSettings(_settings.copyWith(translationSelectedModelId: value));
}
Future<void> setTranslationDownloadedModels(
List<TranslationModelRecord> value,
) async {
await updateSettings(
_settings.copyWith(translationDownloadedModels: value),
);
}
}

View file

@ -138,6 +138,9 @@ class MessageRetryService extends ChangeNotifier {
Future<void> sendMessageWithRetry({
required Contact contact,
required String text,
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
Uint8List? pathBytes,
int? pathLength,
}) async {
@ -150,6 +153,9 @@ class MessageRetryService extends ChangeNotifier {
final message = Message(
senderKey: contact.publicKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: MessageStatus.pending,

View file

@ -0,0 +1,2 @@
export 'translation_file_store_stub.dart'
if (dart.library.io) 'translation_file_store_io.dart';

View file

@ -0,0 +1,131 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
final baseDir = await getApplicationDocumentsDirectory();
final dir = Directory('${baseDir.path}/translation_models');
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
return dir.path;
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
final dir = Directory(await modelDirectoryPath());
if (!dir.existsSync()) {
return const [];
}
final models = <TranslationModelRecord>[];
for (final entity in dir.listSync().whereType<File>()) {
final name = entity.uri.pathSegments.last;
// Skip hidden chunk files from interrupted parallel downloads.
if (name.startsWith('.')) {
await entity.delete();
continue;
}
final stat = entity.statSync();
models.add(
TranslationModelRecord(
id: name,
name: name,
sourceUrl: '',
localPath: entity.path,
downloadedAt: stat.modified,
fileSizeBytes: stat.size,
),
);
}
return models;
}
Future<void> deleteModel(TranslationModelRecord model) async {
await deleteFile(model.localPath);
}
Future<void> deleteFile(String path) async {
final file = File(path);
if (file.existsSync()) {
await file.delete();
}
}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
final directoryPath = await modelDirectoryPath();
final file = File('$directoryPath/$fileName');
final sink = file.openWrite();
var fileSizeBytes = 0;
var completed = false;
try {
await for (final chunk in chunks) {
sink.add(chunk);
fileSizeBytes += chunk.length;
}
completed = true;
} finally {
await sink.close();
if (!completed && file.existsSync()) {
await file.delete();
}
}
return DownloadedModelFile(
localPath: file.path,
fileSizeBytes: fileSizeBytes,
);
}
Future<String> chunkFilePath(String fileName, int index) async {
final dir = await modelDirectoryPath();
return '$dir/.${fileName}_chunk_$index';
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
final dir = await modelDirectoryPath();
final finalPath = '$dir/$fileName';
final sink = File(finalPath).openWrite();
var totalSize = 0;
var completed = false;
try {
for (final chunkPath in chunkPaths) {
final chunkFile = File(chunkPath);
await sink.addStream(chunkFile.openRead());
totalSize += await chunkFile.length();
}
completed = true;
} finally {
await sink.close();
for (final chunkPath in chunkPaths) {
final file = File(chunkPath);
if (file.existsSync()) {
await file.delete();
}
}
if (!completed) {
final finalFile = File(finalPath);
if (finalFile.existsSync()) {
await finalFile.delete();
}
}
}
return DownloadedModelFile(localPath: finalPath, fileSizeBytes: totalSize);
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}

View file

@ -0,0 +1,43 @@
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
throw UnsupportedError('Local model storage is not supported on web.');
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
return const [];
}
Future<void> deleteModel(TranslationModelRecord model) async {}
Future<void> deleteFile(String path) async {}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<String> chunkFilePath(String fileName, int index) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}

View file

@ -0,0 +1,653 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:llamadart/llamadart.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../utils/app_logger.dart';
import 'app_settings_service.dart';
import 'translation_file_store.dart';
class TranslationResult {
final String translatedText;
final String targetLanguageCode;
final String? detectedLanguageCode;
final String? modelId;
final MessageTranslationStatus status;
const TranslationResult({
required this.translatedText,
required this.targetLanguageCode,
required this.status,
this.detectedLanguageCode,
this.modelId,
});
}
class TranslationDownloadCancelled implements Exception {
const TranslationDownloadCancelled();
@override
String toString() => 'Download canceled.';
}
class TranslationService extends ChangeNotifier {
final AppSettingsService _appSettingsService;
final TranslationFileStore _fileStore;
TranslationService(
this._appSettingsService, {
TranslationFileStore? fileStore,
}) : _fileStore = fileStore ?? TranslationFileStore();
bool _isBusy = false;
bool _isDownloading = false;
bool _cancelDownloadRequested = false;
String? _lastError;
Future<void> _queue = Future<void>.value();
LlamaEngine? _engine;
String? _loadedModelPath;
String? _failedModelPath;
int _downloadedBytes = 0;
int? _downloadTotalBytes;
String? _downloadFileName;
bool get isBusy => _isBusy;
bool get isDownloading => _isDownloading;
String? get lastError => _lastError;
int get downloadedBytes => _downloadedBytes;
int? get downloadTotalBytes => _downloadTotalBytes;
String? get downloadFileName => _downloadFileName;
double? get downloadProgress {
final total = _downloadTotalBytes;
if (!_isDownloading || total == null || total <= 0) {
return null;
}
return (_downloadedBytes / total).clamp(0.0, 1.0);
}
AppSettings get _settings => _appSettingsService.settings;
String? resolvedTargetLanguageCode(String? fallbackLanguageCode) {
return _settings.translationTargetLanguageCode ??
_settings.languageOverride ??
fallbackLanguageCode;
}
String? resolvedIncomingLanguageCode(String? fallbackLanguageCode) {
return _settings.languageOverride ?? fallbackLanguageCode ?? 'en';
}
bool shouldTranslateIncoming({
required String text,
required bool isCli,
required bool isOutgoing,
}) {
if (!_settings.translationEnabled || isCli || isOutgoing) {
return false;
}
return _isPlainTextEligible(text);
}
bool shouldTranslateOutgoing({
required String text,
required String? targetLanguageCode,
}) {
return _settings.composerTranslationEnabled &&
targetLanguageCode != null &&
targetLanguageCode.isNotEmpty &&
_isPlainTextEligible(text);
}
List<TranslationModelRecord> get availableModels =>
_settings.translationDownloadedModels;
TranslationModelRecord? get selectedModel {
final selectedId = _settings.translationSelectedModelId;
if (selectedId == null) {
return availableModels.isNotEmpty ? availableModels.first : null;
}
for (final model in availableModels) {
if (model.id == selectedId) {
return model;
}
}
return availableModels.isNotEmpty ? availableModels.first : null;
}
Future<void> refreshDownloadedModels() async {
if (_isDownloading) return;
final scanned = await _fileStore.scanDownloadedModels();
if (scanned.isEmpty) {
return;
}
final existingByPath = {
for (final model in _settings.translationDownloadedModels)
model.localPath: model,
};
final merged = scanned.map((model) {
final existing = existingByPath[model.localPath];
if (existing == null) {
return model;
}
return TranslationModelRecord(
id: existing.id,
name: existing.name,
sourceUrl: existing.sourceUrl,
localPath: existing.localPath,
downloadedAt: existing.downloadedAt,
fileSizeBytes: model.fileSizeBytes,
);
}).toList();
await _appSettingsService.setTranslationDownloadedModels(merged);
_failedModelPath = null;
if (_settings.translationSelectedModelId == null && merged.isNotEmpty) {
await _appSettingsService.setTranslationSelectedModelId(merged.first.id);
}
}
static const int _parallelChunks = 8;
static const int _parallelMinBytes = 10 * 1024 * 1024; // 10 MB
Future<TranslationModelRecord> downloadModel({
required String sourceUrl,
String? fileName,
String? id,
}) async {
final uri = Uri.tryParse(sourceUrl);
if (uri == null || !uri.hasScheme) {
throw ArgumentError('Invalid model URL.');
}
return _runExclusive(() async {
_setBusy(true);
_setDownloading(true);
_lastError = null;
try {
final resolvedFileName =
fileName ??
_sanitizeFileName(
uri.pathSegments.isNotEmpty
? uri.pathSegments.last
: 'translation-model.gguf',
);
_downloadFileName = resolvedFileName;
_downloadedBytes = 0;
_cancelDownloadRequested = false;
// HEAD request to check size and range support.
final headClient = http.Client();
int? totalSize;
bool supportsRange = false;
try {
final headResponse = await headClient.send(http.Request('HEAD', uri));
totalSize = headResponse.contentLength;
supportsRange =
headResponse.headers['accept-ranges']?.contains('bytes') == true;
await headResponse.stream.drain<void>();
} finally {
headClient.close();
}
_downloadTotalBytes = totalSize;
notifyListeners();
DownloadedModelFile downloaded;
if (supportsRange &&
totalSize != null &&
totalSize > _parallelMinBytes) {
downloaded = await _downloadParallel(
uri: uri,
fileName: resolvedFileName,
totalSize: totalSize,
);
} else {
downloaded = await _downloadSingle(
uri: uri,
fileName: resolvedFileName,
);
}
final record = TranslationModelRecord(
id: id ?? resolvedFileName,
name: resolvedFileName,
sourceUrl: sourceUrl,
localPath: downloaded.localPath,
downloadedAt: DateTime.now(),
fileSizeBytes: downloaded.fileSizeBytes,
);
final updated = [
for (final existing in _settings.translationDownloadedModels)
if (existing.id != record.id) existing,
record,
];
await _appSettingsService.setTranslationDownloadedModels(updated);
await _appSettingsService.setTranslationSelectedModelId(record.id);
await _appSettingsService.setTranslationModelSourceUrl(sourceUrl);
_failedModelPath = null;
return record;
} finally {
_setDownloading(false);
}
});
}
Future<DownloadedModelFile> _downloadSingle({
required Uri uri,
required String fileName,
}) async {
final client = http.Client();
try {
final response = await client.send(http.Request('GET', uri));
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StateError('Model download failed: HTTP ${response.statusCode}');
}
_downloadTotalBytes ??= response.contentLength;
notifyListeners();
final trackedStream = _trackDownloadProgress(response.stream);
return await _fileStore.writeModelBytes(
fileName: fileName,
chunks: trackedStream,
);
} finally {
client.close();
}
}
Future<DownloadedModelFile> _downloadParallel({
required Uri uri,
required String fileName,
required int totalSize,
}) async {
final chunkSize = (totalSize / _parallelChunks).ceil();
final chunkPaths = <String>[];
final clients = <http.Client>[];
var combineReached = false;
try {
final futures = <Future<void>>[];
for (var i = 0; i < _parallelChunks; i++) {
final start = i * chunkSize;
final end = (start + chunkSize - 1).clamp(0, totalSize - 1);
if (start >= totalSize) break;
final chunkPath = await _fileStore.chunkFilePath(fileName, i);
chunkPaths.add(chunkPath);
final client = http.Client();
clients.add(client);
futures.add(
_downloadRange(
client: client,
uri: uri,
chunkPath: chunkPath,
start: start,
end: end,
),
);
}
await Future.wait(futures);
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadFileName = 'Merging chunks...';
notifyListeners();
combineReached = true;
return await _fileStore.combineChunks(
fileName: fileName,
chunkPaths: chunkPaths,
);
} finally {
for (final client in clients) {
client.close();
}
if (!combineReached) {
for (final chunkPath in chunkPaths) {
await _fileStore.deleteFile(chunkPath);
}
}
}
}
Future<void> _downloadRange({
required http.Client client,
required Uri uri,
required String chunkPath,
required int start,
required int end,
}) async {
final request = http.Request('GET', uri);
request.headers['Range'] = 'bytes=$start-$end';
final response = await client.send(request);
if (response.statusCode != 206 && response.statusCode != 200) {
throw StateError('Range download failed: HTTP ${response.statusCode}');
}
final trackedStream = _trackDownloadProgress(response.stream);
await _fileStore.writeModelBytes(
fileName: chunkPath.split('/').last,
chunks: trackedStream,
);
}
void cancelDownload() {
if (!_isDownloading) {
return;
}
_cancelDownloadRequested = true;
_lastError = 'Download stopped.';
notifyListeners();
}
Future<void> removeModel(TranslationModelRecord model) async {
await _runExclusive(() async {
_setBusy(true);
_lastError = null;
await _fileStore.deleteModel(model);
final updated = _settings.translationDownloadedModels
.where((entry) => entry.id != model.id)
.toList();
await _appSettingsService.setTranslationDownloadedModels(updated);
if (_settings.translationSelectedModelId == model.id) {
await _appSettingsService.setTranslationSelectedModelId(
updated.isNotEmpty ? updated.first.id : null,
);
}
});
}
Future<TranslationResult?> translateIncomingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
// If translation is nearly identical, text was already in target language.
if (translatedText.trim().toLowerCase() == text.trim().toLowerCase()) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<TranslationResult?> translateOutgoingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<String?> detectLanguage(String text) async {
return _heuristicLanguageCode(text);
}
Future<String?> _translateText({
required String text,
required String targetLanguageCode,
String? sourceLanguageCode,
}) async {
if (!_hasUsableModel) {
return null;
}
final model = selectedModel;
if (model == null || model.localPath.isEmpty) {
return null;
}
final targetLabel = _languageLabel(targetLanguageCode);
final instruction = targetLanguageCode == 'zh'
? '将以下文本翻译为中文,注意只需要输出翻译后的结果,不要额外解释:\n\n$text'
: 'Translate the following segment into $targetLabel, without additional explanation.\n\n$text';
try {
return await _runExclusive(() async {
final engine = await _ensureContext(model.localPath);
if (engine == null) {
return null;
}
final messages = [
LlamaChatMessage.fromText(
role: LlamaChatRole.user,
text: instruction,
),
];
final output = StringBuffer();
await for (final chunk in engine.create(
messages,
params: const GenerationParams(
maxTokens: 256,
temp: 0.7,
topK: 20,
topP: 0.6,
penalty: 1.05,
reusePromptPrefix: false,
),
enableThinking: false,
sourceLangCode: sourceLanguageCode,
targetLangCode: targetLanguageCode,
)) {
final content = chunk.choices.firstOrNull?.delta.content;
if (content != null) {
output.write(content);
}
if (output.length >= text.length * 4 + 100) {
break;
}
}
return _sanitizeOutput(output.toString());
});
} catch (error) {
_lastError = error.toString();
appLogger.warn('Translation request failed: $error');
notifyListeners();
return null;
}
}
bool get _hasUsableModel {
final model = selectedModel;
return !kIsWeb && model != null && model.localPath.isNotEmpty;
}
bool _isPlainTextEligible(String text) {
final trimmed = text.trim();
if (trimmed.isEmpty) {
return false;
}
return !(trimmed.startsWith('g:') ||
trimmed.startsWith('m:') ||
trimmed.startsWith('V1|') ||
trimmed.startsWith('r:'));
}
String? _heuristicLanguageCode(String text) {
if (RegExp(r'[іїєґІЇЄҐ]').hasMatch(text)) {
return 'uk';
}
if (RegExp(r'[а-яёА-ЯЁ]').hasMatch(text)) {
return 'ru';
}
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
return 'ja';
}
if (RegExp(r'[가-힣]').hasMatch(text)) {
return 'ko';
}
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
return 'zh';
}
// Latin-script languages can't be reliably distinguished by characters
// alone return null so the translator always attempts translation.
return null;
}
String _languageLabel(String code) {
for (final option in supportedTranslationLanguages) {
if (option.code == code) {
return option.label;
}
}
return code.toUpperCase();
}
String _sanitizeOutput(String raw) {
var result = raw.trim();
result = result.replaceAll(RegExp(r'\*\*'), '');
result = result.replaceAll(RegExp(r'<[^>]+>'), '');
return result.trim();
}
String _sanitizeFileName(String fileName) {
final cleaned = fileName.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
return cleaned.isEmpty ? 'translation-model.gguf' : cleaned;
}
Future<LlamaEngine?> _ensureContext(String modelPath) async {
if (_engine != null && _loadedModelPath == modelPath) {
return _engine;
}
if (modelPath == _failedModelPath) {
return null;
}
if (_engine != null) {
await _engine!.dispose();
_engine = null;
_loadedModelPath = null;
}
final engine = LlamaEngine(LlamaBackend());
try {
await engine.loadModel(
modelPath,
modelParams: const ModelParams(
gpuLayers: 0,
preferredBackend: GpuBackend.cpu,
),
);
_engine = engine;
_loadedModelPath = modelPath;
_failedModelPath = null;
return _engine;
} catch (_) {
await engine.dispose();
_failedModelPath = modelPath;
rethrow;
}
}
Future<void> releaseModel() async {
await _runExclusive(() async {
final engine = _engine;
if (engine == null) {
_loadedModelPath = null;
return;
}
_engine = null;
_loadedModelPath = null;
await engine.dispose();
});
}
Future<T> _runExclusive<T>(Future<T> Function() action) {
final completer = Completer<T>();
_setBusy(true);
_queue = _queue.then((_) async {
try {
completer.complete(await action());
} catch (error, stackTrace) {
completer.completeError(error, stackTrace);
} finally {
_setBusy(false);
}
});
return completer.future;
}
Stream<List<int>> _trackDownloadProgress(Stream<List<int>> source) async* {
await for (final chunk in source) {
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadedBytes += chunk.length;
notifyListeners();
yield chunk;
}
}
void _setBusy(bool value) {
if (_isBusy == value) {
return;
}
_isBusy = value;
notifyListeners();
}
void _setDownloading(bool value) {
_isDownloading = value;
if (!value) {
_cancelDownloadRequested = false;
_downloadedBytes = 0;
_downloadTotalBytes = null;
_downloadFileName = null;
}
notifyListeners();
}
@override
void dispose() {
final engine = _engine;
_engine = null;
_loadedModelPath = null;
if (engine != null) {
unawaited(engine.dispose());
}
super.dispose();
}
}

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@ -98,6 +99,11 @@ class ChannelMessageStore {
'senderKey': msg.senderKey != null ? base64Encode(msg.senderKey!) : null,
'senderName': msg.senderName,
'text': msg.text,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'timestamp': msg.timestamp.millisecondsSinceEpoch,
'isOutgoing': msg.isOutgoing,
'status': msg.status.index,
@ -126,6 +132,13 @@ class ChannelMessageStore {
: null,
senderName: json['senderName'] as String,
text: decodedText,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
isOutgoing: json['isOutgoing'] as bool,
status: ChannelMessageStatus.values[json['status'] as int],

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import '../models/message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
@ -83,6 +84,11 @@ class MessageStore {
'isCli': msg.isCli,
'status': msg.status.index,
'messageId': msg.messageId,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'retryCount': msg.retryCount,
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
'expectedAckHash': msg.expectedAckHash,
@ -115,6 +121,13 @@ class MessageStore {
isCli: isCli,
status: MessageStatus.values[json['status'] as int],
messageId: json['messageId'] as String?,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
retryCount: json['retryCount'] as int? ?? 0,
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
expectedAckHash: json['expectedAckHash'] as int? ?? 0,

View file

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
import '../models/translation_support.dart';
class MessageTranslationButton extends StatelessWidget {
final bool enabled;
final String? languageCode;
final VoidCallback onPressed;
const MessageTranslationButton({
super.key,
required this.enabled,
required this.languageCode,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final label = _languageLabel(
languageCode,
context.l10n.translation_systemLanguage,
);
return IconButton(
icon: Icon(enabled ? Icons.translate : Icons.translate_outlined),
onPressed: onPressed,
tooltip: enabled
? context.l10n.translation_translateTo(label)
: context.l10n.translation_translationOptions,
);
}
}
Future<void> showMessageTranslationSheet({
required BuildContext context,
required bool enabled,
required String? selectedLanguageCode,
required ValueChanged<bool> onEnabledChanged,
required ValueChanged<String> onLanguageSelected,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (context) => _MessageTranslationSheet(
enabled: enabled,
selectedLanguageCode: selectedLanguageCode,
onEnabledChanged: onEnabledChanged,
onLanguageSelected: onLanguageSelected,
),
);
}
class _MessageTranslationSheet extends StatefulWidget {
final bool enabled;
final String? selectedLanguageCode;
final ValueChanged<bool> onEnabledChanged;
final ValueChanged<String> onLanguageSelected;
const _MessageTranslationSheet({
required this.enabled,
required this.selectedLanguageCode,
required this.onEnabledChanged,
required this.onLanguageSelected,
});
@override
State<_MessageTranslationSheet> createState() =>
_MessageTranslationSheetState();
}
class _MessageTranslationSheetState extends State<_MessageTranslationSheet> {
late final TextEditingController _searchController;
late bool _localEnabled;
late String? _localSelectedLanguageCode;
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
@override
void initState() {
super.initState();
_searchController = TextEditingController();
_localEnabled = widget.enabled;
_localSelectedLanguageCode = widget.selectedLanguageCode;
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _updateFilter(String query) {
final normalized = query.trim().toLowerCase();
setState(() {
_filtered = supportedTranslationLanguages.where((option) {
return option.label.toLowerCase().contains(normalized) ||
option.code.toLowerCase().contains(normalized);
}).toList();
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.translation_messageTranslation,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.translation_translateBeforeSending),
subtitle: Text(
_localEnabled
? context.l10n.translation_composerEnabledHint
: context.l10n.translation_composerDisabledHint,
),
value: _localEnabled,
onChanged: (value) {
setState(() => _localEnabled = value);
widget.onEnabledChanged(value);
},
),
const SizedBox(height: 8),
TextField(
controller: _searchController,
onChanged: _updateFilter,
decoration: InputDecoration(
labelText: context.l10n.translation_targetLanguage,
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: _filtered.length,
itemBuilder: (context, index) {
final option = _filtered[index];
final selected = option.code == _localSelectedLanguageCode;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
selected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: Text(option.label),
subtitle: Text(option.code.toUpperCase()),
onTap: () {
setState(() => _localSelectedLanguageCode = option.code);
widget.onLanguageSelected(option.code);
Navigator.pop(context);
},
);
},
),
),
],
),
),
);
}
}
String _languageLabel(String? languageCode, String systemLanguageFallback) {
if (languageCode == null) {
return systemLanguageFallback;
}
for (final option in supportedTranslationLanguages) {
if (option.code == languageCode) {
return option.label;
}
}
return languageCode.toUpperCase();
}

View file

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../helpers/link_handler.dart';
class TranslatedMessageContent extends StatelessWidget {
final String displayText;
final String? originalText;
final TextStyle style;
final TextStyle? originalStyle;
final bool showOriginalFirst;
const TranslatedMessageContent({
super.key,
required this.displayText,
required this.style,
this.originalText,
this.originalStyle,
this.showOriginalFirst = true,
});
@override
Widget build(BuildContext context) {
final trimmedDisplay = displayText.trim();
final trimmedOriginal = originalText?.trim();
final shouldShowOriginal =
trimmedOriginal != null &&
trimmedOriginal.isNotEmpty &&
trimmedOriginal != trimmedDisplay;
final originalWidget = shouldShowOriginal
? LinkHandler.buildLinkifyText(
context: context,
text: trimmedOriginal,
style:
originalStyle ??
style.copyWith(
fontStyle: FontStyle.italic,
fontSize: style.fontSize,
),
)
: null;
final translatedWidget = LinkHandler.buildLinkifyText(
context: context,
text: trimmedDisplay,
style: style,
);
if (!shouldShowOriginal) {
return translatedWidget;
}
final children = showOriginalFirst
? [originalWidget!, const SizedBox(height: 6), translatedWidget]
: [translatedWidget, const SizedBox(height: 6), originalWidget!];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
);
}
}

View file

@ -71,6 +71,17 @@ dependencies:
flutter_blue_plus_platform_interface: ^8.2.1
ml_algo: ^16.0.0
ml_dataframe: ^1.0.0
llamadart: '>=0.6.8 <0.7.0'
hooks:
user_defines:
llamadart:
llamadart_native_backends:
platforms:
android-arm64:
backends: [cpu]
android-x64:
backends: [cpu]
dev_dependencies:
flutter_test: