mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
merge main
This commit is contained in:
commit
6165629c6d
58 changed files with 5222 additions and 318 deletions
|
|
@ -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);
|
||||
|
|
@ -4055,6 +4205,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
_addMessage(message.senderKeyHex, message);
|
||||
if (!message.isOutgoing) {
|
||||
unawaited(
|
||||
_translateIncomingContactMessage(message.senderKeyHex, message),
|
||||
);
|
||||
}
|
||||
_maybeIncrementContactUnread(message);
|
||||
notifyListeners();
|
||||
|
||||
|
|
@ -4287,6 +4442,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) {
|
||||
|
|
@ -4366,6 +4526,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) {
|
||||
|
|
@ -5060,6 +5223,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,
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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": "Език на системата"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2039,6 +2039,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": {
|
||||
|
|
@ -2046,8 +2074,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2046,6 +2046,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",
|
||||
|
|
@ -2056,5 +2085,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2039,6 +2039,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": {
|
||||
|
|
@ -2046,8 +2074,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@
|
|||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||
"settings_bandwidth": "Bande passante",
|
||||
"settings_spreadingFactor": "Facteur de répartition",
|
||||
"settings_codingRate": "Taux de codage",
|
||||
"settings_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"settings_codingRate": "Taux de codage (CR)",
|
||||
"settings_txPower": "TX Puissance (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||
|
|
@ -570,7 +570,7 @@
|
|||
"chat_clearPath": "Effacer le chemin",
|
||||
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
|
||||
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
|
||||
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
|
||||
"chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.",
|
||||
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
|
||||
"chat_fullPath": "Chemin complet",
|
||||
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
|
||||
|
|
@ -646,7 +646,7 @@
|
|||
},
|
||||
"map_chat": "Chat",
|
||||
"map_repeater": "Répéteur",
|
||||
"map_room": "Salle",
|
||||
"map_room": "Room Server",
|
||||
"map_sensor": "Capteur",
|
||||
"map_pinDm": "Clé (DM)",
|
||||
"map_pinPrivate": "Verrouiller (Privé)",
|
||||
|
|
@ -686,7 +686,7 @@
|
|||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_sharedAt": "Partagé",
|
||||
"map_joinRoom": "Rejoindre la salle",
|
||||
"map_joinRoom": "Rejoindre le room server",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||
|
|
@ -869,7 +869,7 @@
|
|||
"path_labelHexPrefixes": "Préfixes hexadécimaux",
|
||||
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
|
||||
"path_selectFromContacts": "Sélectionner à partir des contacts :",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
|
||||
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
|
||||
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
|
||||
"@path_invalidHexPrefixes": {
|
||||
|
|
@ -1000,15 +1000,15 @@
|
|||
"repeater_txPower": "TX Puissance",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Bande passante",
|
||||
"repeater_spreadingFactor": "Facteur de répartition",
|
||||
"repeater_codingRate": "Taux de codage",
|
||||
"repeater_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"repeater_codingRate": "Taux de codage (CR)",
|
||||
"repeater_locationSettings": "Paramètres de localisation",
|
||||
"repeater_latitude": "Latitude",
|
||||
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
|
||||
"repeater_longitude": "Longitude",
|
||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||
"repeater_features": "Fonctionnalités",
|
||||
"repeater_packetForwarding": "Transfert de paquets",
|
||||
"repeater_packetForwarding": "Mode répéteur",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||
"repeater_guestAccess": "Accès Invité",
|
||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||
|
|
@ -1381,7 +1381,7 @@
|
|||
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
||||
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
||||
"channels_scanQrCode": "Scanner un code QR",
|
||||
"channels_scanQrCode": "Scanner un QR code",
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #equipe",
|
||||
|
|
@ -1470,8 +1470,8 @@
|
|||
"community_join": "Rejoindre",
|
||||
"community_joinTitle": "Rejoindre la communauté",
|
||||
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
|
||||
"community_scanQr": "Scanner la communauté QR",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
|
||||
"community_scanQr": "Scanner un QR code de communauté",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
|
||||
"community_showQr": "Afficher le QR Code",
|
||||
"community_publicChannel": "Communauté Publique",
|
||||
"community_hashtagChannel": "Hashtag Communauté",
|
||||
|
|
@ -1482,13 +1482,13 @@
|
|||
"community_qrTitle": "Partager Communauté",
|
||||
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
|
||||
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
|
||||
"community_invalidQrCode": "Code QR de communauté non valide",
|
||||
"community_invalidQrCode": "QR code de communauté non valide",
|
||||
"community_alreadyMember": "Déjà membre",
|
||||
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
|
||||
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
|
||||
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
|
||||
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
|
||||
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
|
||||
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
|
||||
"community_manageCommunities": "Gérer les Communautés",
|
||||
"community_delete": "Quitter la communauté",
|
||||
"community_deleteConfirm": "Quitter \"{name}\" ?",
|
||||
|
|
@ -1538,10 +1538,10 @@
|
|||
}
|
||||
},
|
||||
"community_regenerateSecret": "Régénérer le secret",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
|
||||
"community_regenerate": "Régénérer",
|
||||
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_updateSecret": "Mettre à jour le secret",
|
||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
|
|
@ -1558,11 +1558,11 @@
|
|||
"contacts_pathTrace": "Traçage de chemin",
|
||||
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
|
||||
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_roomPing": "Pinguer le serveur de la salle",
|
||||
"contacts_roomPing": "Pinguer le room server",
|
||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||
"appSettings_languageUk": "Ukrainien",
|
||||
"appSettings_languageRu": "Russe",
|
||||
|
|
@ -1587,12 +1587,12 @@
|
|||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
||||
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
|
||||
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
||||
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
||||
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
||||
|
|
@ -1804,15 +1804,15 @@
|
|||
"contacts_unread": "Non lu",
|
||||
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||
"settings_contactSettings": "Paramètres de contact",
|
||||
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers découverts",
|
||||
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||
"contactsSettings_title": "Paramètres des contacts",
|
||||
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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 d’appairage 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2049,6 +2049,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": {
|
||||
|
|
@ -2056,8 +2084,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é"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2049,6 +2049,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": {
|
||||
|
|
@ -2056,8 +2084,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": "システム言語"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2049,6 +2049,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": {
|
||||
|
|
@ -2056,8 +2084,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": "시스템 언어"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6173,6 +6173,132 @@ abstract class AppLocalizations {
|
|||
/// **'Noise floor, RSSI, SNR, and airtime'**
|
||||
String get radioStats_settingsSubtitle;
|
||||
|
||||
/// No description provided for @translation_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation'**
|
||||
String get translation_title;
|
||||
|
||||
/// No description provided for @translation_enableTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable translation'**
|
||||
String get translation_enableTitle;
|
||||
|
||||
/// No description provided for @translation_enableSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate incoming messages and allow pre-send translation.'**
|
||||
String get translation_enableSubtitle;
|
||||
|
||||
/// No description provided for @translation_composerTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate before sending'**
|
||||
String get translation_composerTitle;
|
||||
|
||||
/// No description provided for @translation_composerSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Controls the default state of the composer translation icon.'**
|
||||
String get translation_composerSubtitle;
|
||||
|
||||
/// No description provided for @translation_targetLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Target language'**
|
||||
String get translation_targetLanguage;
|
||||
|
||||
/// No description provided for @translation_useAppLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use app language'**
|
||||
String get translation_useAppLanguage;
|
||||
|
||||
/// No description provided for @translation_downloadedModelLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloaded model'**
|
||||
String get translation_downloadedModelLabel;
|
||||
|
||||
/// No description provided for @translation_presetModelLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preset Hugging Face model'**
|
||||
String get translation_presetModelLabel;
|
||||
|
||||
/// No description provided for @translation_manualUrlLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manual model URL'**
|
||||
String get translation_manualUrlLabel;
|
||||
|
||||
/// No description provided for @translation_downloadModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download model'**
|
||||
String get translation_downloadModel;
|
||||
|
||||
/// No description provided for @translation_downloading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloading...'**
|
||||
String get translation_downloading;
|
||||
|
||||
/// No description provided for @translation_working.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Working...'**
|
||||
String get translation_working;
|
||||
|
||||
/// No description provided for @translation_stop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stop'**
|
||||
String get translation_stop;
|
||||
|
||||
/// No description provided for @translation_mergingChunks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Merging downloaded chunks into final file...'**
|
||||
String get translation_mergingChunks;
|
||||
|
||||
/// No description provided for @translation_downloadedModels.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloaded models'**
|
||||
String get translation_downloadedModels;
|
||||
|
||||
/// No description provided for @translation_deleteModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete model'**
|
||||
String get translation_deleteModel;
|
||||
|
||||
/// No description provided for @translation_modelDownloaded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation model downloaded.'**
|
||||
String get translation_modelDownloaded;
|
||||
|
||||
/// No description provided for @translation_downloadStopped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download stopped.'**
|
||||
String get translation_downloadStopped;
|
||||
|
||||
/// No description provided for @translation_downloadFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download failed: {error}'**
|
||||
String translation_downloadFailed(String error);
|
||||
|
||||
/// No description provided for @translation_enterUrlFirst.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a model URL first.'**
|
||||
String get translation_enterUrlFirst;
|
||||
|
||||
/// No description provided for @scanner_linuxPairingShowPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -6196,6 +6322,48 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Enter PIN for {deviceName} (leave blank if none).'**
|
||||
String scanner_linuxPairingPinPrompt(String deviceName);
|
||||
|
||||
/// No description provided for @translation_messageTranslation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Message translation'**
|
||||
String get translation_messageTranslation;
|
||||
|
||||
/// No description provided for @translation_translateBeforeSending.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate before sending'**
|
||||
String get translation_translateBeforeSending;
|
||||
|
||||
/// No description provided for @translation_composerEnabledHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Messages will be translated before send.'**
|
||||
String get translation_composerEnabledHint;
|
||||
|
||||
/// No description provided for @translation_composerDisabledHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send messages in the original typed language.'**
|
||||
String get translation_composerDisabledHint;
|
||||
|
||||
/// No description provided for @translation_translateTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate to {language}'**
|
||||
String translation_translateTo(String language);
|
||||
|
||||
/// No description provided for @translation_translationOptions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation options'**
|
||||
String get translation_translationOptions;
|
||||
|
||||
/// No description provided for @translation_systemLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'System language'**
|
||||
String get translation_systemLanguage;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -3580,18 +3580,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 => 'Език на системата';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3589,17 +3589,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3514,6 +3514,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';
|
||||
|
||||
|
|
@ -3527,4 +3595,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3583,16 +3583,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -559,10 +559,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get settings_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get settings_spreadingFactor => 'Facteur de répartition';
|
||||
String get settings_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get settings_codingRate => 'Taux de codage';
|
||||
String get settings_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Puissance (dBm)';
|
||||
|
|
@ -946,7 +946,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Rechercher $number$str serveurs de salle...';
|
||||
return 'Rechercher $number$str room server...';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -1229,7 +1229,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
'N\'importe qui peut rejoindre les canaux #hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scanner un code QR';
|
||||
String get channels_scanQrCode => 'Scanner un QR code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
|
||||
|
|
@ -1498,7 +1498,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Utiliser le commutateur de routage dans la barre d\'application';
|
||||
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
|
|
@ -1623,7 +1623,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get map_repeater => 'Répéteur';
|
||||
|
||||
@override
|
||||
String get map_room => 'Salle';
|
||||
String get map_room => 'Room Server';
|
||||
|
||||
@override
|
||||
String get map_sensor => 'Capteur';
|
||||
|
|
@ -1742,7 +1742,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get map_sharedAt => 'Partagé';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre la salle';
|
||||
String get map_joinRoom => 'Rejoindre le room server';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Gérer le répéteur';
|
||||
|
|
@ -2011,7 +2011,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Aucun répéteur ou serveur de salle n\'a été trouvé.';
|
||||
'Aucun répéteur ou room server n\'a été trouvé.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
|
|
@ -2221,10 +2221,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get repeater_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition';
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get repeater_codingRate => 'Taux de codage';
|
||||
String get repeater_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Paramètres de localisation';
|
||||
|
|
@ -2247,7 +2247,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get repeater_features => 'Fonctionnalités';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwarding => 'Transfert de paquets';
|
||||
String get repeater_packetForwarding => 'Mode répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwardingSubtitle =>
|
||||
|
|
@ -2908,11 +2908,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get community_scanQr => 'Scanner la communauté QR';
|
||||
String get community_scanQr => 'Scanner un QR code de communauté';
|
||||
|
||||
@override
|
||||
String get community_scanInstructions =>
|
||||
'Pointez l\'appareil photo vers un code QR communautaire.';
|
||||
'Pointez l\'appareil photo vers un QR code de communauté.';
|
||||
|
||||
@override
|
||||
String get community_showQr => 'Afficher le QR Code';
|
||||
|
|
@ -2952,7 +2952,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
|
||||
|
||||
@override
|
||||
String get community_invalidQrCode => 'Code QR de communauté non valide';
|
||||
String get community_invalidQrCode => 'QR code de communauté non valide';
|
||||
|
||||
@override
|
||||
String get community_alreadyMember => 'Déjà membre';
|
||||
|
|
@ -2976,7 +2976,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get community_scanOrCreate =>
|
||||
'Scanner un code QR ou créer une communauté pour commencer';
|
||||
'Scanner un QR code ou créer une communauté pour commencer';
|
||||
|
||||
@override
|
||||
String get community_manageCommunities => 'Gérer les Communautés';
|
||||
|
|
@ -3004,7 +3004,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -3025,7 +3025,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
|
||||
return 'Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -3273,11 +3273,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get contacts_repeaterPing => 'Pinguer le répéteur';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Traçage du chemin vers le serveur de la salle';
|
||||
String get contacts_roomPathTrace => 'Traçage du chemin vers le room server';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pinguer le serveur de la salle';
|
||||
String get contacts_roomPing => 'Pinguer le room server';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Tracer le chemin';
|
||||
|
|
@ -3383,7 +3382,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exporter les répéteurs / serveur de salle au format GPX';
|
||||
'Exporter les répéteurs / room servers au format GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
|
|
@ -3421,7 +3420,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Emplacements des serveurs de répéteur et de salle';
|
||||
'Emplacements des répéteurs et room servers';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Emplacements des compagnons';
|
||||
|
|
@ -3472,11 +3471,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Ajouter automatiquement les serveurs de salle';
|
||||
'Ajouter automatiquement les room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
|
||||
'Autoriser le compagnon à ajouter automatiquement les room servers découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
|
|
@ -3608,6 +3607,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';
|
||||
|
||||
|
|
@ -3615,10 +3684,36 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Code PIN d’appairage 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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3600,16 +3600,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é';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3586,17 +3586,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3415,16 +3415,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 => 'システム言語';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3414,7 +3414,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 숨기기';
|
||||
|
|
@ -3424,6 +3489,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 => '시스템 언어';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_nodeSettings => 'Node Instellingen';
|
||||
|
||||
@override
|
||||
String get settings_nodeName => 'Node Naam';
|
||||
String get settings_nodeName => 'Nodenaam';
|
||||
|
||||
@override
|
||||
String get settings_nodeNameNotSet => 'Niet ingesteld';
|
||||
|
|
@ -452,7 +452,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_advertisementSent => 'Advertentie verzonden';
|
||||
|
||||
@override
|
||||
String get settings_syncTime => 'Synchronisatie Tijd';
|
||||
String get settings_syncTime => 'Tijd Synchroniseren';
|
||||
|
||||
@override
|
||||
String get settings_syncTimeSubtitle =>
|
||||
|
|
@ -472,7 +472,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_rebootDevice => 'Apparaat opnieuw opstarten';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore apparaat';
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore-apparaat';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceConfirm =>
|
||||
|
|
@ -556,7 +556,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Vermogen (dBm)';
|
||||
String get settings_txPower => 'TX-Vermogen (dBm)';
|
||||
|
||||
@override
|
||||
String get settings_txPowerHelper => '0 - 22';
|
||||
|
|
@ -565,11 +565,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||
String get settings_clientRepeat => 'Off-Grid Herhalen';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||
'Laat dit apparaat de berichten van andere apparaten doorsturen.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
|
|
@ -846,19 +846,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get appSettings_allTime => 'Altijd';
|
||||
|
||||
@override
|
||||
String get appSettings_lastHour => 'Laat uur';
|
||||
String get appSettings_lastHour => 'Afgelopen uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last6Hours => 'laatste 6 uur';
|
||||
String get appSettings_last6Hours => 'Afgelopen 6 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last24Hours => 'De laatste 24 uur';
|
||||
String get appSettings_last24Hours => 'Afgelopen 24 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Laatste week';
|
||||
String get appSettings_lastWeek => 'Afgelopen week';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||
String get appSettings_offlineMapCache => 'Offline Kaartcache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Eenheden';
|
||||
|
|
@ -1185,32 +1185,32 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get channels_sortUnread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
String get channels_createPrivateChannel => 'PrivéKanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Beveiligd met een geheime sleutel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
String get channels_joinPrivateChannel => 'PrivéKanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Handmatig een geheime sleutel invoeren.';
|
||||
'Voer handmatig een geheime sleutel in.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
|
||||
String get channels_joinPublicChannel => 'Publiek Kanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Iedereen kan dit kanaal aanmelden.';
|
||||
'Iedereen kan toetreden tot dit kanaal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
|
||||
String get channels_joinHashtagChannel => 'Hashtag-kanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Iedereen kan lid worden van hashtag-kanalen.';
|
||||
'Iedereen kan toetreden tot hashtag-kanalen.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scan een QR-code';
|
||||
|
|
@ -1721,7 +1721,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get map_sharedAt => 'Gedeeld';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Sluit Kamer';
|
||||
String get map_joinRoom => 'Kamer Toetreden';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Beheer Repeater';
|
||||
|
|
@ -2013,7 +2013,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get room_management => 'Beheer Server Kamer';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerinstrumenten';
|
||||
String get repeater_managementTools => 'Beheerfuncties';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
|
@ -2039,7 +2039,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_neighbors => 'Buren';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul-hopsburen.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Instellingen';
|
||||
|
|
@ -2105,10 +2105,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_noiseFloor => 'Ruisvloer';
|
||||
|
||||
@override
|
||||
String get repeater_txAirtime => 'TX Airtime';
|
||||
String get repeater_txAirtime => 'TX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_rxAirtime => 'RX Airtime';
|
||||
String get repeater_rxAirtime => 'RX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_packetStatistics => 'Pakketstatistieken';
|
||||
|
|
@ -2153,7 +2153,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get repeater_settingsTitle => 'Repeater Instellingen';
|
||||
String get repeater_settingsTitle => 'Repeaterinstellingen';
|
||||
|
||||
@override
|
||||
String get repeater_basicSettings => 'Basisinstellingen';
|
||||
|
|
@ -2162,19 +2162,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_repeaterName => 'Repeaternaam';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
|
||||
String get repeater_repeaterNameHelper => 'Weergavenaam voor deze repeater';
|
||||
|
||||
@override
|
||||
String get repeater_adminPassword => 'Admin wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_adminPasswordHelper => 'Volledige toegangspaswoord';
|
||||
String get repeater_adminPasswordHelper => 'Wachtwoord administratortoegang';
|
||||
|
||||
@override
|
||||
String get repeater_guestPassword => 'Wachtwoord Gast';
|
||||
String get repeater_guestPassword => 'Gast wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_guestPasswordHelper => 'Leesbeheer wachtwoord';
|
||||
String get repeater_guestPasswordHelper => 'Wachtwoord gasttoegen';
|
||||
|
||||
@override
|
||||
String get repeater_radioSettings => 'Radio Instellingen';
|
||||
|
|
@ -2201,7 +2201,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Locatie Instellingen';
|
||||
String get repeater_locationSettings => 'Locatie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_latitude => 'Breedtegraad';
|
||||
|
|
@ -2233,14 +2233,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
'Toegestane leesbeheer toegang voor gasten.';
|
||||
|
||||
@override
|
||||
String get repeater_privacyMode => 'Privacy Modus';
|
||||
String get repeater_privacyMode => 'Privacymodus';
|
||||
|
||||
@override
|
||||
String get repeater_privacyModeSubtitle =>
|
||||
'Naam/locatie verbergen in advertenties';
|
||||
|
||||
@override
|
||||
String get repeater_advertisementSettings => 'Advertentie Instellingen';
|
||||
String get repeater_advertisementSettings => 'Advertentie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_localAdvertInterval => 'Lokale Advertentie Interval';
|
||||
|
|
@ -2345,7 +2345,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_refreshGuestAccess => 'Toegang Gast Vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshPrivacyMode => 'Privacy Mode vernieuwen';
|
||||
String get repeater_refreshPrivacyMode => 'Privacymode vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshAdvertisementSettings =>
|
||||
|
|
@ -2371,10 +2371,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get repeater_commandHelp => 'Help';
|
||||
|
||||
@override
|
||||
String get repeater_clearHistory => 'Verwijder Geschiedenis';
|
||||
String get repeater_clearHistory => 'Geschiedenis Verwijderen';
|
||||
|
||||
@override
|
||||
String get repeater_noCommandsSent => 'Geen commando\'s verzonden nog.';
|
||||
String get repeater_noCommandsSent => 'Nog geen commando\'s verzonden.';
|
||||
|
||||
@override
|
||||
String get repeater_typeCommandOrUseQuick =>
|
||||
|
|
@ -2401,28 +2401,28 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetName => 'Haal Naam op';
|
||||
String get repeater_cliQuickGetName => 'Naam opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetRadio => 'Radio ontvangen';
|
||||
String get repeater_cliQuickGetRadio => 'Radio-instellingen opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetTx => 'Krijg TX';
|
||||
String get repeater_cliQuickGetTx => 'TX opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickNeighbors => 'Buren';
|
||||
String get repeater_cliQuickNeighbors => 'Buren opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickVersion => 'Versie';
|
||||
String get repeater_cliQuickVersion => 'Versie opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties';
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Tijd';
|
||||
String get repeater_cliQuickClock => 'Tijd opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
|
||||
String get repeater_cliHelpAdvert => 'Advertentie uitzenden';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpReboot =>
|
||||
|
|
@ -2694,7 +2694,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get telemetry_voltageLabel => 'Spanning';
|
||||
|
||||
@override
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU Temperatuur';
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU-temperatuur';
|
||||
|
||||
@override
|
||||
String get telemetry_temperatureLabel => 'Temperatuur';
|
||||
|
|
@ -2735,7 +2735,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||
String get neighbors_repeatersNeighbors => 'Repeatbburen';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||
|
|
@ -3034,7 +3034,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get listFilter_latestMessages => 'Recente berichten';
|
||||
|
||||
@override
|
||||
String get listFilter_heardRecently => 'Hoor je onlangs';
|
||||
String get listFilter_heardRecently => 'Recent gezien';
|
||||
|
||||
@override
|
||||
String get listFilter_az => 'A-Z';
|
||||
|
|
@ -3280,7 +3280,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
'Contact uit klembord toevoegen';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||
|
|
@ -3564,17 +3564,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 => 'Bluetooth‑koppelings‑PIN';
|
||||
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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Wiele potwierdzeń: $value';
|
||||
return 'Wielokrotne ACK: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -611,7 +611,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get appSettings_language => 'Język';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSystem => 'Domyślny systemu';
|
||||
String get appSettings_languageSystem => 'Domyślny systemowy';
|
||||
|
||||
@override
|
||||
String get appSettings_languageEn => 'English';
|
||||
|
|
@ -1708,7 +1708,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get map_otherNodes => 'Inne węzły';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
|
||||
String get map_showOverlaps => 'Nakładające się klucze przekaźników';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefiks klucza';
|
||||
|
|
@ -1757,7 +1757,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get map_runTrace => 'Uruchom ślad ścieżki';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
|
||||
String get map_runTraceWithReturnPath => 'Wróć tą samą ścieżką';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Usuń ostatni';
|
||||
|
|
@ -2067,8 +2067,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get repeater_neighbors => 'Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||
String get repeater_neighborsSubtitle => 'Wyświetl sąsiadów zero-hop.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Ustawienia';
|
||||
|
|
@ -3380,11 +3379,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Eksportuj przekaźniki / serwer pokojowy do GPX';
|
||||
'Eksportuj przekaźniki / roomservery do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.';
|
||||
'Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
|
||||
|
|
@ -3416,7 +3415,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Lokalizacje przekaźników i serwerów pokojowych';
|
||||
'Lokalizacje przekaźników i roomserverów';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
|
||||
|
|
@ -3433,7 +3432,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
'Eksport danych mapy GPX meshcore-open';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||
String get snrIndicator_nearByRepeaters => 'Pobliskie przekaźniki';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||
|
|
@ -3466,11 +3465,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatycznie dodaj serwery pokojowe';
|
||||
'Automatycznie dodaj roomservery';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
|
|
@ -3599,16 +3598,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3579,17 +3579,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3594,16 +3594,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 => 'Язык системы';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3557,6 +3557,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';
|
||||
|
||||
|
|
@ -3564,10 +3635,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3562,17 +3562,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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3540,16 +3540,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 => 'Bluetooth‑parnings‑PIN';
|
||||
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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3597,16 +3597,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 => 'Мова системи';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3312,7 +3312,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';
|
||||
|
|
@ -3322,6 +3387,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 => '系统语言';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
"settings_appSettings": "App Instellingen",
|
||||
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
|
||||
"settings_nodeSettings": "Node Instellingen",
|
||||
"settings_nodeName": "Node Naam",
|
||||
"settings_nodeName": "Nodenaam",
|
||||
"settings_nodeNameNotSet": "Niet ingesteld",
|
||||
"settings_nodeNameHint": "Voer nodenaam in",
|
||||
"settings_nodeNameUpdated": "Naam bijgewerkt",
|
||||
|
|
@ -107,13 +107,13 @@
|
|||
"settings_sendAdvertisement": "Verzend Advertentie",
|
||||
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
||||
"settings_advertisementSent": "Advertentie verzonden",
|
||||
"settings_syncTime": "Synchronisatie Tijd",
|
||||
"settings_syncTime": "Tijd Synchroniseren",
|
||||
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
|
||||
"settings_timeSynchronized": "Tijdsynchronisatie",
|
||||
"settings_refreshContacts": "Contacten vernieuwen",
|
||||
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
|
||||
"settings_rebootDevice": "Apparaat opnieuw opstarten",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore apparaat",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore-apparaat",
|
||||
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_bleDebugLog": "BLE Debug Log",
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
"settings_bandwidth": "Bandbreedte",
|
||||
"settings_spreadingFactor": "Spreadsnelheid",
|
||||
"settings_codingRate": "Codeertarief",
|
||||
"settings_txPower": "TX Vermogen (dBm)",
|
||||
"settings_txPower": "TX-Vermogen (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||
"settings_error": "Fout: {message}",
|
||||
|
|
@ -232,11 +232,11 @@
|
|||
"appSettings_mapTimeFilter": "Filter tijd op kaart",
|
||||
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
|
||||
"appSettings_allTime": "Altijd",
|
||||
"appSettings_lastHour": "Laat uur",
|
||||
"appSettings_last6Hours": "laatste 6 uur",
|
||||
"appSettings_last24Hours": "De laatste 24 uur",
|
||||
"appSettings_lastWeek": "Laatste week",
|
||||
"appSettings_offlineMapCache": "Offline Kaarten Cache",
|
||||
"appSettings_lastHour": "Afgelopen uur",
|
||||
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||
"appSettings_lastWeek": "Afgelopen week",
|
||||
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||
"@appSettings_areaSelectedZoom": {
|
||||
|
|
@ -686,7 +686,7 @@
|
|||
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
||||
"map_sharedPin": "Gedeelde pin",
|
||||
"map_sharedAt": "Gedeeld",
|
||||
"map_joinRoom": "Sluit Kamer",
|
||||
"map_joinRoom": "Kamer Toetreden",
|
||||
"map_manageRepeater": "Beheer Repeater",
|
||||
"mapCache_title": "Offline Kaarten Cache",
|
||||
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
|
||||
|
|
@ -882,7 +882,7 @@
|
|||
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
|
||||
"path_setPath": "Stel Pad in",
|
||||
"repeater_management": "Beheer Repeaters",
|
||||
"repeater_managementTools": "Beheerinstrumenten",
|
||||
"repeater_managementTools": "Beheerfuncties",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
|
|
@ -916,8 +916,8 @@
|
|||
"repeater_lastRssi": "Laatste RSSI",
|
||||
"repeater_lastSnr": "Laatste SNR",
|
||||
"repeater_noiseFloor": "Ruisvloer",
|
||||
"repeater_txAirtime": "TX Airtime",
|
||||
"repeater_rxAirtime": "RX Airtime",
|
||||
"repeater_txAirtime": "TX-zendtijd",
|
||||
"repeater_rxAirtime": "RX-zendtijd",
|
||||
"repeater_packetStatistics": "Pakketstatistieken",
|
||||
"repeater_sent": "Verzonden",
|
||||
"repeater_received": "Ontvangen",
|
||||
|
|
@ -986,14 +986,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsTitle": "Repeater Instellingen",
|
||||
"repeater_settingsTitle": "Repeaterinstellingen",
|
||||
"repeater_basicSettings": "Basisinstellingen",
|
||||
"repeater_repeaterName": "Repeaternaam",
|
||||
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
|
||||
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
|
||||
"repeater_adminPassword": "Admin wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
|
||||
"repeater_guestPassword": "Wachtwoord Gast",
|
||||
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
|
||||
"repeater_guestPassword": "Gast wachtwoord",
|
||||
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
|
||||
"repeater_radioSettings": "Radio Instellingen",
|
||||
"repeater_frequencyMhz": "Frequentie (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
|
|
@ -1002,7 +1002,7 @@
|
|||
"repeater_bandwidth": "Bandbreedte",
|
||||
"repeater_spreadingFactor": "Spreidingsfactor",
|
||||
"repeater_codingRate": "Codeertarief",
|
||||
"repeater_locationSettings": "Locatie Instellingen",
|
||||
"repeater_locationSettings": "Locatie-instellingen",
|
||||
"repeater_latitude": "Breedtegraad",
|
||||
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
|
||||
"repeater_longitude": "Lengtegraad",
|
||||
|
|
@ -1012,9 +1012,9 @@
|
|||
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
|
||||
"repeater_guestAccess": "Toegang voor Gasten",
|
||||
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
|
||||
"repeater_privacyMode": "Privacy Modus",
|
||||
"repeater_privacyMode": "Privacymodus",
|
||||
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||
"repeater_advertisementSettings": "Advertentie Instellingen",
|
||||
"repeater_advertisementSettings": "Advertentie-instellingen",
|
||||
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
|
|
@ -1077,7 +1077,7 @@
|
|||
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
|
||||
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
|
||||
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
|
||||
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
|
||||
"repeater_refreshed": "{label} is vernieuwd",
|
||||
"@repeater_refreshed": {
|
||||
|
|
@ -1098,8 +1098,8 @@
|
|||
"repeater_cliTitle": "Repeater CLI",
|
||||
"repeater_debugNextCommand": "Debug Volgende Commando",
|
||||
"repeater_commandHelp": "Help",
|
||||
"repeater_clearHistory": "Verwijder Geschiedenis",
|
||||
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
|
||||
"repeater_clearHistory": "Geschiedenis Verwijderen",
|
||||
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
|
||||
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
|
||||
"repeater_enterCommandHint": "Voer bevel in...",
|
||||
"repeater_previousCommand": "Vorige opdracht",
|
||||
|
|
@ -1114,14 +1114,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliQuickGetName": "Haal Naam op",
|
||||
"repeater_cliQuickGetRadio": "Radio ontvangen",
|
||||
"repeater_cliQuickGetTx": "Krijg TX",
|
||||
"repeater_cliQuickNeighbors": "Buren",
|
||||
"repeater_cliQuickVersion": "Versie",
|
||||
"repeater_cliQuickAdvertise": "Advertenties",
|
||||
"repeater_cliQuickClock": "Tijd",
|
||||
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
|
||||
"repeater_cliQuickGetName": "Naam opvragen",
|
||||
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
|
||||
"repeater_cliQuickGetTx": "TX opvragen",
|
||||
"repeater_cliQuickNeighbors": "Buren opvragen",
|
||||
"repeater_cliQuickVersion": "Versie opvragen",
|
||||
"repeater_cliQuickAdvertise": "Advertenties opvragen",
|
||||
"repeater_cliQuickClock": "Tijd opvragen",
|
||||
"repeater_cliHelpAdvert": "Advertentie uitzenden",
|
||||
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
|
||||
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
|
||||
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
|
||||
|
|
@ -1207,7 +1207,7 @@
|
|||
},
|
||||
"telemetry_batteryLabel": "Batterij",
|
||||
"telemetry_voltageLabel": "Spanning",
|
||||
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
|
||||
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
|
||||
"telemetry_temperatureLabel": "Temperatuur",
|
||||
"telemetry_currentLabel": "Huidig",
|
||||
"telemetry_batteryValue": "{percent}% / {volts}V",
|
||||
|
|
@ -1350,7 +1350,7 @@
|
|||
"listFilter_tooltip": "Filteren en sorteren",
|
||||
"listFilter_sortBy": "Sorteren door",
|
||||
"listFilter_latestMessages": "Recente berichten",
|
||||
"listFilter_heardRecently": "Hoor je onlangs",
|
||||
"listFilter_heardRecently": "Recent gezien",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_filters": "Filters",
|
||||
"listFilter_all": "Alles",
|
||||
|
|
@ -1367,20 +1367,20 @@
|
|||
}
|
||||
},
|
||||
"repeater_neighbors": "Buren",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
|
||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_repeatersNeighbors": "Repeatbburen",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
||||
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
|
||||
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
|
||||
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
|
||||
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
|
||||
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
|
||||
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
|
||||
"channels_scanQrCode": "Scan een QR-code",
|
||||
"channels_scanQrCodeComingSoon": "Komt later",
|
||||
"channels_enterHashtag": "Voer hashtag in",
|
||||
|
|
@ -1578,7 +1578,7 @@
|
|||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_ShareContact": "Contact naar Klembord kopiëren",
|
||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"notification_activityTitle": "MeshCore Activiteit",
|
||||
|
|
@ -1616,8 +1616,8 @@
|
|||
"snrIndicator_lastSeen": "Laatst gezien",
|
||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||
"chat_ShowAllPaths": "Toon alle paden",
|
||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||
"settings_clientRepeat": "Off-Grid Herhalen",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
|
||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Eenheden",
|
||||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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": "Bluetooth‑koppelings‑PIN"
|
||||
"@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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@
|
|||
"appSettings_themeLight": "Jasne",
|
||||
"appSettings_themeDark": "Ciemny",
|
||||
"appSettings_language": "Język",
|
||||
"appSettings_languageSystem": "Domyślny systemu",
|
||||
"appSettings_languageSystem": "Domyślny systemowy",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
|
|
@ -1377,7 +1377,7 @@
|
|||
}
|
||||
},
|
||||
"repeater_neighbors": "Sąsiedzi",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiadów zero-hop.",
|
||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||
|
|
@ -1626,12 +1626,12 @@
|
|||
},
|
||||
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
|
||||
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
|
||||
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / serwer pokojowy do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / roomservery do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
|
||||
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
|
||||
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
|
||||
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i serwerów pokojowych",
|
||||
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i roomserverów",
|
||||
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
|
||||
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
|
||||
|
|
@ -1652,7 +1652,7 @@
|
|||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
|
||||
"snrIndicator_nearByRepeaters": "Pobliskie przekaźniki",
|
||||
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
|
||||
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||
|
|
@ -1850,12 +1850,12 @@
|
|||
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie przekaźników",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie odkrytych przekaźników.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj roomservery",
|
||||
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
|
||||
"settings_contactSettings": "Ustawienia kontaktów",
|
||||
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
|
||||
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
|
||||
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
|
||||
"discoveredContacts_contactAdded": "Kontakt dodany",
|
||||
|
|
@ -1983,9 +1983,9 @@
|
|||
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
|
||||
"settings_multiAck": "Wiele potwierdzeń: {value}",
|
||||
"map_showOverlaps": "Nakładające się klucze powtarzalne",
|
||||
"map_runTraceWithReturnPath": "Wróć z powrotem tą samą ścieżką",
|
||||
"settings_multiAck": "Wielokrotne ACK: {value}",
|
||||
"map_showOverlaps": "Nakładające się klucze przekaźników",
|
||||
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
|
|
@ -2049,6 +2049,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": {
|
||||
|
|
@ -2056,8 +2084,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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1251,6 +1251,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": {
|
||||
|
|
@ -1258,8 +1286,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": "Язык системы"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingShowPin": "Visa PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,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": {
|
||||
|
|
@ -2018,8 +2046,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": "Мова системи"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2016,6 +2016,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": {
|
||||
|
|
@ -2023,8 +2051,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": "系统语言"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
136
lib/models/translation_support.dart
Normal file
136
lib/models/translation_support.dart
Normal 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,
|
||||
),
|
||||
];
|
||||
|
|
@ -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,179 @@ 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) {
|
||||
widget.onLanguageSelected(value);
|
||||
},
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
RadioListTile<String?>(
|
||||
value: null,
|
||||
title: Text(context.l10n.translation_useAppLanguage),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,26 @@ 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 '../helpers/smaz.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';
|
||||
|
||||
|
|
@ -357,6 +360,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
|
||||
|
|
@ -507,12 +518,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) ...[
|
||||
|
|
@ -1129,6 +1146,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
final smazEncoder = connector.isChannelSmazEnabled(widget.channel.index)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -1203,6 +1221,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
|
|
@ -1293,7 +1317,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;
|
||||
|
||||
|
|
@ -1308,10 +1344,38 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
_lastChannelSendAt = now;
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
|
||||
String messageText = text;
|
||||
String? originalText;
|
||||
String? translatedLanguageCode;
|
||||
String? translationModelId;
|
||||
if (settings.translationEnabled) {
|
||||
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
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 (_replyingToMessage != null) {
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $text';
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $messageText';
|
||||
}
|
||||
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
|
|
@ -1326,10 +1390,16 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ 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 '../helpers/smaz.dart';
|
||||
|
|
@ -24,9 +23,11 @@ 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';
|
||||
|
|
@ -36,8 +37,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';
|
||||
|
|
@ -502,6 +505,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
connector.isContactSmazEnabled(widget.contact.publicKeyHex)
|
||||
? Smaz.encodeIfSmaller
|
||||
: null;
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -559,6 +563,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
|
|
@ -754,7 +764,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;
|
||||
|
||||
|
|
@ -768,10 +790,39 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
_lastTextSendAt = now;
|
||||
|
||||
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(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
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();
|
||||
final outboundText = connector.prepareContactOutboundText(
|
||||
widget.contact,
|
||||
text,
|
||||
outgoingText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -780,9 +831,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
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) {
|
||||
|
|
@ -1686,6 +1743,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(
|
||||
|
|
@ -1815,13 +1880,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) ...[
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
lib/services/translation_file_store.dart
Normal file
2
lib/services/translation_file_store.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'translation_file_store_stub.dart'
|
||||
if (dart.library.io) 'translation_file_store_io.dart';
|
||||
131
lib/services/translation_file_store_io.dart
Normal file
131
lib/services/translation_file_store_io.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
43
lib/services/translation_file_store_stub.dart
Normal file
43
lib/services/translation_file_store_stub.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
660
lib/services/translation_service.dart
Normal file
660
lib/services/translation_service.dart
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
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.translationTargetLanguageCode ??
|
||||
_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) {
|
||||
await response.stream.drain<void>();
|
||||
throw StateError(
|
||||
'Range download failed: HTTP ${response.statusCode}'
|
||||
'${response.statusCode == 200 ? ' (server ignored Range header)' : ''}',
|
||||
);
|
||||
}
|
||||
final trackedStream = _trackDownloadProgress(response.stream);
|
||||
await _fileStore.writeModelBytes(
|
||||
fileName: chunkPath.split(RegExp(r'[/\\]')).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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
204
lib/widgets/message_translation_button.dart
Normal file
204
lib/widgets/message_translation_button.dart
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
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 + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
final selected = _localSelectedLanguageCode == null;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
selected
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_unchecked,
|
||||
),
|
||||
title: Text(context.l10n.translation_useAppLanguage),
|
||||
onTap: () {
|
||||
setState(() => _localSelectedLanguageCode = null);
|
||||
widget.onLanguageSelected(null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
final option = _filtered[index - 1];
|
||||
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();
|
||||
}
|
||||
61
lib/widgets/translated_message_content.dart
Normal file
61
lib/widgets/translated_message_content.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import flutter_blue_plus_darwin
|
|||
import flutter_local_notifications
|
||||
import mobile_scanner
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
|
|
@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
|
|
|||
11
pubspec.yaml
11
pubspec.yaml
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue