From 29660d520e6a43bd32636223842a7a1829d459cc Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 16:28:57 -0400 Subject: [PATCH 1/6] feat: Linux BLE pairing support via bluetoothctl Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN entry, with Completer-based flow control, explicit retry loop, and named timeout constants. - LinuxBlePairingService: pair-and-trust with up to 2 retries - LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors - Conditional import stub for web builds (dart.library.io gate) - Scanner screen: PIN dialog integration for Linux pairing flow - MeshCoreConnector: Linux pairing/recovery/reconnect wiring - l10n: 4 new pairing keys across all 14 locales - 12 unit tests (pairing service + error classifier) --- lib/connector/meshcore_connector.dart | 464 +++++++++++++++++- lib/l10n/app_bg.arb | 13 +- lib/l10n/app_de.arb | 13 +- lib/l10n/app_en.arb | 11 + lib/l10n/app_es.arb | 13 +- lib/l10n/app_fr.arb | 13 +- lib/l10n/app_it.arb | 13 +- lib/l10n/app_localizations.dart | 24 + lib/l10n/app_localizations_bg.dart | 14 + lib/l10n/app_localizations_de.dart | 14 + lib/l10n/app_localizations_en.dart | 14 + lib/l10n/app_localizations_es.dart | 15 + lib/l10n/app_localizations_fr.dart | 15 + lib/l10n/app_localizations_it.dart | 15 + lib/l10n/app_localizations_nl.dart | 14 + lib/l10n/app_localizations_pl.dart | 15 + lib/l10n/app_localizations_pt.dart | 14 + lib/l10n/app_localizations_ru.dart | 15 + lib/l10n/app_localizations_sk.dart | 14 + lib/l10n/app_localizations_sl.dart | 15 + lib/l10n/app_localizations_sv.dart | 14 + lib/l10n/app_localizations_uk.dart | 15 + lib/l10n/app_localizations_zh.dart | 14 + lib/l10n/app_nl.arb | 13 +- lib/l10n/app_pl.arb | 13 +- lib/l10n/app_pt.arb | 13 +- lib/l10n/app_ru.arb | 13 +- lib/l10n/app_sk.arb | 13 +- lib/l10n/app_sl.arb | 13 +- lib/l10n/app_sv.arb | 13 +- lib/l10n/app_uk.arb | 13 +- lib/l10n/app_zh.arb | 13 +- lib/screens/scanner_screen.dart | 116 ++++- lib/services/linux_ble_error_classifier.dart | 37 ++ lib/services/linux_ble_pairing_service.dart | 423 ++++++++++++++++ .../linux_ble_pairing_service_stub.dart | 28 ++ pubspec.yaml | 1 + .../linux_ble_error_classifier_test.dart | 150 ++++++ .../linux_ble_pairing_service_test.dart | 418 ++++++++++++++++ 39 files changed, 2031 insertions(+), 40 deletions(-) create mode 100644 lib/services/linux_ble_error_classifier.dart create mode 100644 lib/services/linux_ble_pairing_service.dart create mode 100644 lib/services/linux_ble_pairing_service_stub.dart create mode 100644 test/services/linux_ble_error_classifier_test.dart create mode 100644 test/services/linux_ble_pairing_service_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef2f9b7..f176cf6 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart' as crypto; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -15,6 +16,9 @@ import '../helpers/reaction_helper.dart'; import '../helpers/smaz.dart'; import '../services/app_debug_log_service.dart'; import '../services/ble_debug_log_service.dart'; +import '../services/linux_ble_error_classifier.dart'; +import '../services/linux_ble_pairing_service_stub.dart' + if (dart.library.io) '../services/linux_ble_pairing_service.dart'; import '../services/message_retry_service.dart'; import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; @@ -116,11 +120,14 @@ class MeshCoreConnector extends ChangeNotifier { String? _lastDeviceDisplayName; bool _manualDisconnect = false; final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); + final LinuxBlePairingService _linuxBlePairingService = + LinuxBlePairingService(); StreamSubscription? _usbFrameSubscription; final MeshCoreTcpConnector _tcpConnector = MeshCoreTcpConnector(); MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; final List _scanResults = []; + final List _linuxSystemScanResults = []; final List _contacts = []; final List _discoveredContacts = []; final List _channels = []; @@ -269,6 +276,8 @@ class MeshCoreConnector extends ChangeNotifier { bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; + bool get isAutoReconnectScheduled => + _shouldAutoReconnect && (_reconnectTimer?.isActive ?? false); String? get activeTcpEndpoint => _tcpConnector.activeEndpoint; bool get isTcpTransportConnected => _state == MeshCoreConnectionState.connected && @@ -933,6 +942,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_state == MeshCoreConnectionState.scanning) return; _scanResults.clear(); + _linuxSystemScanResults.clear(); _setState(MeshCoreConnectionState.scanning); // Ensure any previous scan is fully stopped. Guard with isScanningNow to @@ -971,9 +981,15 @@ class MeshCoreConnector extends ChangeNotifier { await Future.delayed(const Duration(milliseconds: 300)); } + if (PlatformInfo.isLinux) { + await _loadLinuxSystemDevicesForScan(); + } + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { - _scanResults.clear(); - _scanResults.addAll(results); + _scanResults + ..clear() + ..addAll(results); + _mergeLinuxSystemScanResults(); notifyListeners(); }); @@ -994,6 +1010,62 @@ class MeshCoreConnector extends ChangeNotifier { await stopScan(); } + Future _loadLinuxSystemDevicesForScan() async { + try { + final systemDevices = await FlutterBluePlus.systemDevices([ + Guid(MeshCoreUuids.service), + ]); + _linuxSystemScanResults + ..clear() + ..addAll( + systemDevices + .where( + (device) => + device.platformName.startsWith('MeshCore-') || + device.platformName.startsWith('Whisper-'), + ) + .map( + (device) => ScanResult( + device: device, + advertisementData: AdvertisementData( + advName: device.platformName, + txPowerLevel: null, + appearance: null, + connectable: true, + manufacturerData: const >{}, + serviceData: const >{}, + serviceUuids: [Guid(MeshCoreUuids.service)], + ), + rssi: 0, + timeStamp: DateTime.now(), + ), + ), + ); + _mergeLinuxSystemScanResults(); + notifyListeners(); + } catch (error) { + _appDebugLogService?.warn( + 'Failed loading Linux paired/system BLE devices: $error', + tag: 'BLE Scan', + ); + } + } + + void _mergeLinuxSystemScanResults() { + if (!PlatformInfo.isLinux || _linuxSystemScanResults.isEmpty) { + return; + } + final existingIds = _scanResults + .map((result) => result.device.remoteId.str) + .toSet(); + for (final result in _linuxSystemScanResults) { + if (existingIds.contains(result.device.remoteId.str)) { + continue; + } + _scanResults.add(result); + } + } + Future stopScan() async { // Only call FlutterBluePlus.stopScan() when a scan is actually running. // Calling it when idle triggers a native BLE completion callback even @@ -1250,7 +1322,11 @@ class MeshCoreConnector extends ChangeNotifier { activeTransport == MeshCoreTransportType.tcp; } - Future connect(BluetoothDevice device, {String? displayName}) async { + Future connect( + BluetoothDevice device, { + String? displayName, + Future Function()? linuxPairingPinProvider, + }) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { return; @@ -1295,22 +1371,149 @@ class MeshCoreConnector extends ChangeNotifier { } }); - try { - await device.connect( - timeout: const Duration(seconds: 15), - mtu: null, - license: License.free, - ); - } catch (error) { - _appDebugLogService?.error( - 'device.connect() failure: $error', + if (PlatformInfo.isLinux) { + final remoteId = device.remoteId.str; + _appDebugLogService?.info( + 'Linux pre-connect BlueZ disconnect for $remoteId', tag: 'BLE Connect', ); - rethrow; + await _linuxBlePairingService.disconnectDevice( + remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + ); } - // Request larger MTU only on native platforms; web does not support it. - if (!PlatformInfo.isWeb) { + final connectTimeout = PlatformInfo.isLinux + ? const Duration(seconds: 6) + : const Duration(seconds: 15); + _appDebugLogService?.info( + 'device.connect timeout set to ${connectTimeout.inSeconds}s', + tag: 'BLE Connect', + ); + if (PlatformInfo.isLinux) { + Future attemptConnect() { + return device + .connect( + timeout: connectTimeout, + mtu: null, + license: License.free, + ) + .timeout( + connectTimeout + const Duration(seconds: 2), + onTimeout: () { + throw TimeoutException( + 'Linux connect hard-timeout after ${connectTimeout.inSeconds + 2}s', + ); + }, + ); + } + + try { + await attemptConnect(); + } catch (error) { + _appDebugLogService?.error( + 'device.connect() failure: $error', + tag: 'BLE Connect', + ); + final remoteId = device.remoteId.str; + _appDebugLogService?.warn( + 'Linux immediate retry: forcing BlueZ disconnect before second connect attempt', + tag: 'BLE Connect', + ); + await _linuxBlePairingService.disconnectDevice( + remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + ); + await Future.delayed(const Duration(milliseconds: 700)); + try { + await attemptConnect(); + _appDebugLogService?.info( + 'Linux immediate retry connect succeeded', + tag: 'BLE Connect', + ); + } catch (retryError, retryStackTrace) { + Object finalConnectError = retryError; + StackTrace finalConnectStackTrace = retryStackTrace; + final retryErrorText = retryError.toString().toLowerCase(); + final isAbortByLocal = retryErrorText.contains( + 'le-connection-abort-by-local', + ); + var recoveredOnThirdAttempt = false; + if (isAbortByLocal) { + _appDebugLogService?.warn( + 'Linux immediate retry aborted by local stack; waiting and retrying once more', + tag: 'BLE Connect', + ); + await Future.delayed(const Duration(milliseconds: 1200)); + try { + await attemptConnect(); + _appDebugLogService?.info( + 'Linux third-attempt connect succeeded after local abort', + tag: 'BLE Connect', + ); + recoveredOnThirdAttempt = true; + } catch (thirdError, thirdStackTrace) { + finalConnectError = thirdError; + finalConnectStackTrace = thirdStackTrace; + _appDebugLogService?.error( + 'device.connect() third-attempt failure: $thirdError', + tag: 'BLE Connect', + ); + } + } + if (!recoveredOnThirdAttempt) { + final recoveredByPairing = await _recoverLinuxConnectFailure( + device, + attemptConnect: attemptConnect, + onRequestPin: linuxPairingPinProvider, + ); + if (recoveredByPairing) { + _appDebugLogService?.info( + 'Linux connect succeeded after pairing/trust recovery', + tag: 'BLE Connect', + ); + } else { + _appDebugLogService?.error( + 'device.connect() retry failure: $finalConnectError', + tag: 'BLE Connect', + ); + Error.throwWithStackTrace( + _wrapLinuxConnectStageError(finalConnectError), + finalConnectStackTrace, + ); + } + } + } + } + } else { + try { + await device.connect( + timeout: connectTimeout, + mtu: null, + license: License.free, + ); + } catch (error) { + _appDebugLogService?.error( + 'device.connect() failure: $error', + tag: 'BLE Connect', + ); + rethrow; + } + } + + if (PlatformInfo.isLinux) { + await _ensureLinuxBleBond( + device, + onRequestPin: linuxPairingPinProvider, + ); + } + + // Request larger MTU only where the platform path supports it. + if (!PlatformInfo.isWeb && !PlatformInfo.isLinux) { try { final mtu = await device.requestMtu(185); _appDebugLogService?.info('MTU set to: $mtu', tag: 'BLE Connect'); @@ -1320,6 +1523,11 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'BLE Connect', ); } + } else if (PlatformInfo.isLinux) { + _appDebugLogService?.info( + 'Skipping MTU request on Linux; flutter_blue_plus only supports requestMtu on Android', + tag: 'BLE Connect', + ); } late final List services; @@ -1433,11 +1641,213 @@ class MeshCoreConnector extends ChangeNotifier { await _startBleInitialSync(); } catch (e) { _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); - await disconnect(manual: false); + final errorText = e.toString(); + final lowerErrorText = errorText.toLowerCase(); + final isLinuxPairingFailure = + PlatformInfo.isLinux && isLinuxBlePairingFailureText(errorText); + final isLikelyPairingTimeout = isLikelyLinuxBlePairingTimeoutText( + errorText, + ); + final isConnectFailure = isLinuxBleConnectFailureText(errorText); + final isConnectTimeoutFailure = + isConnectFailure && lowerErrorText.contains('timed out'); + final isLinuxConnectFailure = PlatformInfo.isLinux && isConnectFailure; + // Linux pairing failures should not enter auto-reconnect loops; user + // needs to retry manually so they can re-enter PIN / resolve pairing. + if (isLinuxPairingFailure) { + _appDebugLogService?.warn( + isLikelyPairingTimeout + ? 'Linux pairing timed out: stopping reconnect until user retries manually' + : 'Linux pairing failure: stopping reconnect until user retries manually', + tag: 'BLE Connect', + ); + await disconnect(manual: true); + } else if (isLinuxConnectFailure) { + _appDebugLogService?.warn( + isConnectTimeoutFailure + ? 'Linux connect timeout: issuing BlueZ disconnect before reconnect' + : 'Linux connect failure: issuing BlueZ disconnect before reconnect', + tag: 'BLE Connect', + ); + final remoteId = _device?.remoteId.str; + if (remoteId != null) { + await _linuxBlePairingService.disconnectDevice( + remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + ); + } + await disconnect(manual: false, skipBleDeviceDisconnect: true); + } else { + await disconnect(manual: false); + } rethrow; } } + Future _recoverLinuxConnectFailure( + BluetoothDevice device, { + required Future Function() attemptConnect, + Future Function()? onRequestPin, + }) async { + if (!PlatformInfo.isLinux || + !await _linuxBlePairingService.isBluetoothctlAvailable()) { + return false; + } + final remoteId = device.remoteId.str; + final pluginBondState = await _getLinuxPluginBondState(device); + final trustedByBluez = await _linuxBlePairingService.isPairedAndTrusted( + remoteId, + ); + final needsBondRecovery = + pluginBondState != BmBondStateEnum.bonded || !trustedByBluez; + if (!needsBondRecovery) { + return false; + } + _appDebugLogService?.warn( + pluginBondState == BmBondStateEnum.bonded + ? 'Linux connect failed with an untrusted bond; attempting trust/pair recovery' + : 'Linux connect failed before bond completed; attempting pairing fallback', + tag: 'BLE Connect', + ); + await _ensureLinuxBleBond(device, onRequestPin: onRequestPin); + _appDebugLogService?.info( + 'Resetting BlueZ connection after Linux pairing/trust recovery', + tag: 'BLE Connect', + ); + await _linuxBlePairingService.disconnectDevice( + remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + ); + await Future.delayed(const Duration(milliseconds: 700)); + try { + await attemptConnect(); + } catch (error, stackTrace) { + Error.throwWithStackTrace(_wrapLinuxConnectStageError(error), stackTrace); + } + return true; + } + + Object _wrapLinuxConnectStageError(Object error) { + final errorText = error.toString(); + if (errorText.toLowerCase().contains(linuxConnectStageFailureMarker)) { + return error; + } + return StateError('Linux connect stage failure: $error'); + } + + Future _getLinuxPluginBondState( + BluetoothDevice device, + ) async { + try { + final response = await FlutterBluePlusPlatform.instance.getBondState( + BmBondStateRequest(remoteId: device.remoteId), + ); + return response.bondState; + } catch (error) { + _appDebugLogService?.warn( + 'Linux getBondState unavailable for ${device.remoteId.str}: $error', + tag: 'BLE Connect', + ); + return null; + } + } + + Future _ensureLinuxBleBond( + BluetoothDevice device, { + Future Function()? onRequestPin, + }) async { + final remoteId = device.remoteId.str; + final bluetoothctlAvailable = await _linuxBlePairingService + .isBluetoothctlAvailable(); + final beforeBondState = await _getLinuxPluginBondState(device); + if (!bluetoothctlAvailable) { + if (beforeBondState == BmBondStateEnum.bonded) { + _appDebugLogService?.warn( + 'bluetoothctl unavailable; continuing with plugin bonded state', + tag: 'BLE Connect', + ); + } else { + _appDebugLogService?.warn( + 'bluetoothctl unavailable and device is not bonded; skipping Linux pairing fallback', + tag: 'BLE Connect', + ); + } + return; + } + + final trustedByBluez = await _linuxBlePairingService.isPairedAndTrusted( + remoteId, + ); + if (trustedByBluez) { + _appDebugLogService?.info( + 'Linux BLE device already paired/trusted, skipping pairing flow', + tag: 'BLE Connect', + ); + return; + } + + if (beforeBondState == BmBondStateEnum.bonded && !trustedByBluez) { + _appDebugLogService?.warn( + 'Linux BLE device is bonded but not trusted in BlueZ; repairing trust', + tag: 'BLE Connect', + ); + final trustRepaired = await _linuxBlePairingService.trustDevice( + remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + ); + if (trustRepaired) { + _appDebugLogService?.info( + 'Linux BLE trust repair succeeded without re-pairing', + tag: 'BLE Connect', + ); + return; + } + _appDebugLogService?.warn( + 'Linux BLE trust repair did not stick; retrying pairing flow', + tag: 'BLE Connect', + ); + } + + _appDebugLogService?.info( + beforeBondState == BmBondStateEnum.bonded + ? 'Linux BLE device still untrusted after repair; requesting pair' + : 'Linux BLE device not bonded, requesting pair', + tag: 'BLE Connect', + ); + final paired = await _linuxBlePairingService.pairAndTrust( + remoteId: remoteId, + onLog: (message) { + _appDebugLogService?.info(message, tag: 'BLE Pair'); + }, + onRequestPin: onRequestPin, + ); + if (!paired) { + throw StateError('Linux pairing fallback failed'); + } + + final afterBondState = await _getLinuxPluginBondState(device); + if (afterBondState != null && afterBondState != BmBondStateEnum.bonded) { + throw StateError('Linux BLE pairing did not complete'); + } else if (afterBondState == null) { + _appDebugLogService?.warn( + 'Linux plugin bond state unavailable after pairing; relying on BlueZ trust verification', + tag: 'BLE Connect', + ); + } + final trustedAfter = await _linuxBlePairingService.isPairedAndTrusted( + remoteId, + ); + if (!trustedAfter) { + throw StateError('Linux BLE trust repair did not complete'); + } + } + Future _waitForSelfInfo({required Duration timeout}) async { if (_selfPublicKey != null) return true; if (!isConnected) return false; @@ -1559,7 +1969,10 @@ class MeshCoreConnector extends ChangeNotifier { }); } - Future disconnect({bool manual = true}) async { + Future disconnect({ + bool manual = true, + bool skipBleDeviceDisconnect = false, + }) async { if (_state == MeshCoreConnectionState.disconnecting) return; final transportAtDisconnect = _activeTransport; final transportLabel = switch (transportAtDisconnect) { @@ -1602,11 +2015,18 @@ class MeshCoreConnector extends ChangeNotifier { _channelSyncTimeout = null; _channelSyncRetries = 0; - try { - // Skip queued BLE operations so disconnect doesn't get stuck behind them. - await _device?.disconnect(queue: false); - } catch (e) { - _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect'); + if (!skipBleDeviceDisconnect) { + try { + // Skip queued BLE operations so disconnect doesn't get stuck behind them. + await _device?.disconnect(queue: false); + } catch (e) { + _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect'); + } + } else { + _appDebugLogService?.info( + 'Skipping plugin BLE disconnect and continuing cleanup', + tag: 'BLE Connect', + ); } _device = null; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b8ea08f..5b41830 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.", "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinTitle": "PIN за свързване чрез Bluetooth", + "scanner_linuxPairingShowPin": "Покажи PIN", + "scanner_linuxPairingHidePin": "Скриване на PIN кода", + "scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв)." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 681cff6..e0a087f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1938,5 +1938,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde", "appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen", "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingHidePin": "PIN verbergen", + "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_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3942afb..ce7e4b7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -127,6 +127,17 @@ } } }, + "scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN", + "scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Show PIN", + "scanner_linuxPairingHidePin": "Hide PIN", "scanner_stop": "Stop", "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4a83680..282ead5 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1938,5 +1938,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.", "appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes", "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).", + "scanner_linuxPairingHidePin": "Ocultar PIN", + "scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth", + "scanner_linuxPairingShowPin": "Mostrar código PIN" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1d684bb..15d55c9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.", "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Afficher le code PIN", + "scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth", + "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", + "scanner_linuxPairingHidePin": "Masquer le code PIN" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 55a1054..d374ac4 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.", "appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio", "appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Mostra PIN", + "scanner_linuxPairingHidePin": "Nascondi il PIN", + "scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).", + "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 84b5432..3be6498 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -592,6 +592,30 @@ abstract class AppLocalizations { /// **'Connection failed: {error}'** String scanner_connectionFailed(String error); + /// No description provided for @scanner_linuxPairingPinTitle. + /// + /// In en, this message translates to: + /// **'Bluetooth Pairing PIN'** + String get scanner_linuxPairingPinTitle; + + /// No description provided for @scanner_linuxPairingPinPrompt. + /// + /// In en, this message translates to: + /// **'Enter PIN for {deviceName} (leave blank if none).'** + String scanner_linuxPairingPinPrompt(String deviceName); + + /// No description provided for @scanner_linuxPairingShowPin. + /// + /// In en, this message translates to: + /// **'Show PIN'** + String get scanner_linuxPairingShowPin; + + /// No description provided for @scanner_linuxPairingHidePin. + /// + /// In en, this message translates to: + /// **'Hide PIN'** + String get scanner_linuxPairingHidePin; + /// No description provided for @scanner_stop. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 2821617..823300b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -268,6 +268,20 @@ class AppLocalizationsBg extends AppLocalizations { return 'Връзката не успя: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN за свързване чрез Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Покажи PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Скриване на PIN кода'; + @override String get scanner_stop => 'Спрете'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 337915e..86b8417 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -271,6 +271,20 @@ class AppLocalizationsDe extends AppLocalizations { return 'Verbindungsfehler: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN für die Bluetooth-Verbindung'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Geben Sie den PIN-Code für $deviceName ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).'; + } + + @override + String get scanner_linuxPairingShowPin => 'PIN anzeigen'; + + @override + String get scanner_linuxPairingHidePin => 'PIN verbergen'; + @override String get scanner_stop => 'Stopp'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1e4e5b0..311dfb7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -265,6 +265,20 @@ class AppLocalizationsEn extends AppLocalizations { return 'Connection failed: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Enter PIN for $deviceName (leave blank if none).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Show PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Hide PIN'; + @override String get scanner_stop => 'Stop'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 657d556..0aa4fc2 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -269,6 +269,21 @@ class AppLocalizationsEs extends AppLocalizations { return 'Error de conexión: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN para emparejar dispositivos Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Mostrar código PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Ocultar PIN'; + @override String get scanner_stop => 'Detener'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7aa7ebe..aab6ad0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -271,6 +271,21 @@ class AppLocalizationsFr extends AppLocalizations { return 'Échec de la connexion : $error'; } + @override + 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 nécessaire).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Afficher le code PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Masquer le code PIN'; + @override String get scanner_stop => 'Arrêter'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02c5937..553fa0e 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -271,6 +271,21 @@ class AppLocalizationsIt extends AppLocalizations { return 'Connessione fallita: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN per l\'accoppiamento Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Mostra PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Nascondi il PIN'; + @override String get scanner_stop => 'Interrompere'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 9e51164..1d0e755 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -267,6 +267,20 @@ class AppLocalizationsNl extends AppLocalizations { return 'Verbinding mislukt: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN voor Bluetooth-koppeling'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Voer het pincode-in voor $deviceName in (laat dit leeg als er geen is).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Toon PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Verberg PIN'; + @override String get scanner_stop => 'Stoppen'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 176c17e..fb07109 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -272,6 +272,21 @@ class AppLocalizationsPl extends AppLocalizations { return 'Połączenie nieudane: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN do sparowania przez Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Wprowadź kod PIN dla $deviceName (pust, jeśli nie jest wymagany).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Wyświetl kod PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Ukryj kod PIN'; + @override String get scanner_stop => 'Zatrzymaj'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a51e1b0..ee6dea1 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -270,6 +270,20 @@ class AppLocalizationsPt extends AppLocalizations { return 'Falha na conexão: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN de pareamento Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Insira o código PIN para $deviceName (deixe em branco se não houver).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Mostrar PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Esconder o PIN'; + @override String get scanner_stop => 'Pare'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a6998f..016c2df 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -270,6 +270,21 @@ class AppLocalizationsRu extends AppLocalizations { return 'Подключение не удалось: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN для сопряжения устройств по Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Введите PIN-код для $deviceName (оставьте поле пустым, если PIN-код отсутствует).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Показать PIN-код'; + + @override + String get scanner_linuxPairingHidePin => 'Скрыть PIN-код'; + @override String get scanner_stop => 'Стоп'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ae6c956..83ca6e2 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -269,6 +269,20 @@ class AppLocalizationsSk extends AppLocalizations { return 'Pripojenie zlyhalo: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Zadajte PIN pre $deviceName (nechajte prázdne, ak neexistuje).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Zobraziť PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Skryť PIN'; + @override String get scanner_stop => 'Zastavte'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 96501cd..b26c79f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -267,6 +267,21 @@ class AppLocalizationsSl extends AppLocalizations { return 'Pošlo je z povezavo: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN za združevanje preko Bluetootha'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Vnesite PIN kodo za $deviceName (ostavite prazno, če nimate kode).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Prikaži PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Skrijte PIN'; + @override String get scanner_stop => 'Prekliči'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a834230..01656eb 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -266,6 +266,20 @@ class AppLocalizationsSv extends AppLocalizations { return 'Anslutning misslyckades: $error'; } + @override + String get scanner_linuxPairingPinTitle => 'PIN för Bluetooth-parning'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Ange PIN-kod för $deviceName (lämna tomt om ingen finns).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Visa PIN-kod'; + + @override + String get scanner_linuxPairingHidePin => 'Dölj PIN-kod'; + @override String get scanner_stop => 'Stoppa'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7db1cc7..ffa37a6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -269,6 +269,21 @@ class AppLocalizationsUk extends AppLocalizations { return 'Помилка підключення: $error'; } + @override + String get scanner_linuxPairingPinTitle => + 'PIN для з\'єднання через Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Введіть PIN-код для $deviceName (залиште поле порожнім, якщо немає).'; + } + + @override + String get scanner_linuxPairingShowPin => 'Показати PIN-код'; + + @override + String get scanner_linuxPairingHidePin => 'Приховати PIN-код'; + @override String get scanner_stop => 'Стоп'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index dc1a17e..9b375e7 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -256,6 +256,20 @@ class AppLocalizationsZh extends AppLocalizations { return '连接失败:$error'; } + @override + String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return '输入 $deviceName 的 PIN 码(如果为空,则留空)。'; + } + + @override + String get scanner_linuxPairingShowPin => '显示PIN码'; + + @override + String get scanner_linuxPairingHidePin => '隐藏PIN码'; + @override String get scanner_stop => '停止'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 3caea31..19bf4b7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering", "appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen", "appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinTitle": "PIN voor Bluetooth-koppeling", + "scanner_linuxPairingShowPin": "Toon PIN", + "scanner_linuxPairingHidePin": "Verberg PIN", + "scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is)." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index c6e3fc4..6453c0b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Waga usunięta z trasy po nieudanej dostawie", "appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości", "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingHidePin": "Ukryj kod PIN", + "scanner_linuxPairingPinTitle": "PIN do sparowania przez Bluetooth", + "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pust, jeśli nie jest wymagany).", + "scanner_linuxPairingShowPin": "Wyświetl kod PIN" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index e7e2ec6..6bf7f75 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.", "appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens", "appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinTitle": "PIN de pareamento Bluetooth", + "scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).", + "scanner_linuxPairingShowPin": "Mostrar PIN", + "scanner_linuxPairingHidePin": "Esconder o PIN" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 92a3800..410a279 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1150,5 +1150,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.", "appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения", "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Показать PIN-код", + "scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).", + "scanner_linuxPairingHidePin": "Скрыть PIN-код", + "scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 75a7c7d..7d23209 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie", "appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ", "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (nechajte prázdne, ak neexistuje).", + "scanner_linuxPairingShowPin": "Zobraziť PIN", + "scanner_linuxPairingHidePin": "Skryť PIN", + "scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 5ab4736..1e5aed5 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.", "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil", "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Prikaži PIN", + "scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).", + "scanner_linuxPairingHidePin": "Skrijte PIN", + "scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 644b43b..e4acaa8 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök", "appSettings_maxMessageRetries": "Maximalt antal försök", "appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinPrompt": "Ange PIN-kod för {deviceName} (lämna tomt om ingen finns).", + "scanner_linuxPairingHidePin": "Dölj PIN-kod", + "scanner_linuxPairingShowPin": "Visa PIN-kod", + "scanner_linuxPairingPinTitle": "PIN för Bluetooth-parning" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 249fd3b..d31f24b 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1910,5 +1910,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки", "appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення", "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingHidePin": "Приховати PIN-код", + "scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth", + "scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).", + "scanner_linuxPairingShowPin": "Показати PIN-код" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1d4ed30..0a3c639 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1915,5 +1915,16 @@ "appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。", "appSettings_maxMessageRetries": "最大消息重试次数", "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", - "path_routeWeight": "{weight}/{max}" + "path_routeWeight": "{weight}/{max}", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinTitle": "蓝牙配对 PIN", + "scanner_linuxPairingHidePin": "隐藏PIN码", + "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。", + "scanner_linuxPairingShowPin": "显示PIN码" } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 986a598..17f26ea 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../services/linux_ble_error_classifier.dart'; import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; @@ -288,12 +289,33 @@ class _ScannerScreenState extends State { MeshCoreConnector connector, ScanResult result, ) async { + final name = result.device.platformName.isNotEmpty + ? result.device.platformName + : result.advertisementData.advName; try { - final name = result.device.platformName.isNotEmpty - ? result.device.platformName - : result.advertisementData.advName; - await connector.connect(result.device, displayName: name); + await connector.connect( + result.device, + displayName: name, + linuxPairingPinProvider: PlatformInfo.isLinux + ? () async { + if (!context.mounted) return null; + return _promptLinuxPairingPin(context, name); + } + : null, + ); } catch (e) { + final errorText = e.toString(); + final suppressTransientLinuxConnectError = + PlatformInfo.isLinux && + connector.isAutoReconnectScheduled && + isLinuxBleConnectFailureText(errorText); + if (suppressTransientLinuxConnectError) { + appLogger.info( + 'Suppressing transient Linux connect error while auto-reconnect is active: $e', + tag: 'ScannerScreen', + ); + return; + } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -305,6 +327,92 @@ class _ScannerScreenState extends State { } } + Future _promptLinuxPairingPin( + BuildContext context, + String deviceName, + ) async { + final l10n = context.l10n; + var pinValue = ''; + var obscure = true; + appLogger.info( + 'Showing Linux BLE pairing PIN prompt for $deviceName', + tag: 'ScannerScreen', + ); + final pin = await showDialog( + context: context, + builder: (dialogContext) { + return StatefulBuilder( + builder: (dialogContext, setDialogState) { + return AlertDialog( + title: Text(l10n.scanner_linuxPairingPinTitle), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.scanner_linuxPairingPinPrompt(deviceName)), + const SizedBox(height: 12), + TextField( + autofocus: true, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + obscureText: obscure, + enableSuggestions: false, + autocorrect: false, + onChanged: (value) { + pinValue = value.trim(); + }, + onSubmitted: (value) { + Navigator.of(dialogContext).pop(value.trim()); + }, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setDialogState(() { + obscure = !obscure; + }); + }, + icon: Icon( + obscure ? Icons.visibility : Icons.visibility_off, + ), + tooltip: obscure + ? l10n.scanner_linuxPairingShowPin + : l10n.scanner_linuxPairingHidePin, + ), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(null), + child: Text(l10n.common_cancel), + ), + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(pinValue), + child: Text(l10n.common_connect), + ), + ], + ); + }, + ); + }, + ); + if (pin == null) { + appLogger.info( + 'Linux BLE pairing PIN prompt cancelled for $deviceName', + tag: 'ScannerScreen', + ); + return null; + } + appLogger.info( + 'Linux BLE pairing PIN prompt completed for $deviceName', + tag: 'ScannerScreen', + ); + return pin; + } + Widget _bluetoothOffWarning(BuildContext context) { final errorColor = Theme.of(context).colorScheme.error; return Container( diff --git a/lib/services/linux_ble_error_classifier.dart b/lib/services/linux_ble_error_classifier.dart new file mode 100644 index 0000000..acef4c2 --- /dev/null +++ b/lib/services/linux_ble_error_classifier.dart @@ -0,0 +1,37 @@ +const String linuxConnectStageFailureMarker = 'linux connect stage failure'; + +bool isLinuxBleConnectFailureText(String errorText) { + final lowerErrorText = errorText.toLowerCase(); + if (isLinuxBlePairingFailureText(errorText)) { + return false; + } + return lowerErrorText.contains(linuxConnectStageFailureMarker) || + lowerErrorText.contains('| connect |') || + lowerErrorText.contains('linux connect hard-timeout') || + lowerErrorText.contains('org.bluez.error.failed') || + lowerErrorText.contains('org.bluez.error.inprogress') || + lowerErrorText.contains('le-connection-abort-by-local'); +} + +bool isLinuxBlePairingFailureText(String errorText) { + final lowerErrorText = errorText.toLowerCase(); + final isPairingSpecificStateError = + lowerErrorText.contains('bad state: no element') && + (lowerErrorText.contains('pair') || + lowerErrorText.contains('bond') || + lowerErrorText.contains('trust')); + return lowerErrorText.contains('authenticationfailed') || + lowerErrorText.contains('authentication failed') || + lowerErrorText.contains('notpermitted: not paired') || + lowerErrorText.contains('pairing fallback failed') || + lowerErrorText.contains('linux ble pairing did not complete') || + lowerErrorText.contains('linux ble trust repair did not complete') || + isPairingSpecificStateError || + isLikelyLinuxBlePairingTimeoutText(errorText); +} + +bool isLikelyLinuxBlePairingTimeoutText(String errorText) { + final lowerErrorText = errorText.toLowerCase(); + return lowerErrorText.contains('timed out') && + (lowerErrorText.contains('pair') || lowerErrorText.contains('bond')); +} diff --git a/lib/services/linux_ble_pairing_service.dart b/lib/services/linux_ble_pairing_service.dart new file mode 100644 index 0000000..b3d4fc4 --- /dev/null +++ b/lib/services/linux_ble_pairing_service.dart @@ -0,0 +1,423 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +typedef ProcessStartFn = + Future Function(String executable, List arguments); +typedef ProcessRunFn = + Future Function(String executable, List arguments); + +/// Best-effort Linux BLE pairing helper using bluetoothctl. +/// +/// This is used only as a fallback when BlueZ pairing via flutter_blue_plus +/// fails to surface agent prompts in-app. +class LinuxBlePairingService { + /// Maximum number of retry attempts for the pairing flow. + /// Covers one remove-and-retry plus one proactive-PIN retry. + static const int _maxRetries = 2; + + static const Duration _processExitTimeout = Duration(seconds: 6); + static const Duration _pairingCleanupTimeout = Duration(seconds: 5); + static const Duration _defaultPairingTimeout = Duration(seconds: 45); + LinuxBlePairingService({ + ProcessStartFn? processStart, + ProcessRunFn? processRun, + }) : _processStart = processStart ?? Process.start, + _processRun = processRun ?? Process.run; + + final ProcessStartFn _processStart; + final ProcessRunFn _processRun; + + Future isBluetoothctlAvailable() async { + try { + final result = await _processRun('bluetoothctl', ['--version']); + return result.exitCode == 0; + } on ProcessException { + return false; + } + } + + Future disconnectDevice( + String remoteId, { + void Function(String message)? onLog, + }) async { + onLog?.call('Requesting BlueZ disconnect for $remoteId'); + Process process; + try { + process = await _processStart('bluetoothctl', []); + } on ProcessException catch (error) { + onLog?.call( + 'bluetoothctl unavailable, skipping BlueZ disconnect: $error', + ); + return; + } + process.stdin.writeln('disconnect $remoteId'); + process.stdin.writeln('quit'); + try { + await process.exitCode.timeout(_processExitTimeout); + } catch (_) { + process.kill(); + } + onLog?.call('Issued bluetoothctl disconnect for $remoteId'); + } + + Future isPairedAndTrusted(String remoteId) async { + ProcessResult result; + try { + result = await _processRun('bluetoothctl', ['info', remoteId]); + } on ProcessException { + return false; + } + if (result.exitCode != 0) { + return false; + } + final output = (result.stdout as String).toLowerCase(); + return output.contains('paired: yes') && output.contains('trusted: yes'); + } + + Future trustDevice( + String remoteId, { + void Function(String message)? onLog, + }) async { + onLog?.call('Requesting BlueZ trust for $remoteId'); + ProcessResult result; + try { + result = await _processRun('bluetoothctl', ['trust', remoteId]); + } on ProcessException catch (error) { + onLog?.call('bluetoothctl unavailable, cannot trust $remoteId: $error'); + return false; + } + if (result.exitCode != 0) { + onLog?.call('bluetoothctl trust failed for $remoteId: ${result.stderr}'); + return false; + } + final trusted = await isPairedAndTrusted(remoteId); + onLog?.call( + trusted + ? 'Verified BlueZ trust for $remoteId' + : 'BlueZ trust verification failed for $remoteId', + ); + return trusted; + } + + Future pairAndTrust({ + required String remoteId, + Duration timeout = _defaultPairingTimeout, + void Function(String message)? onLog, + Future Function()? onRequestPin, + }) async { + var removeRetryUsed = false; + var proactivePinRetryUsed = false; + Future Function()? currentPinProvider = onRequestPin; + + for (var attempt = 0; attempt <= _maxRetries; attempt++) { + final result = await _runPairingAttempt( + remoteId: remoteId, + timeout: timeout, + onLog: onLog, + onRequestPin: currentPinProvider, + ); + + if (result.success) return true; + if (result.userCancelled) { + onLog?.call('Pairing cancelled by user; skipping retry/remove flow'); + return false; + } + + if (result.pairFailed) { + if (!removeRetryUsed) { + removeRetryUsed = true; + onLog?.call( + 'Pairing failed; removing cached bond and retrying ' + '(attempt ${attempt + 1}/$_maxRetries)', + ); + await _removeDevice(remoteId, onLog: onLog); + continue; + } + if (!result.pinSent && + !proactivePinRetryUsed && + currentPinProvider != null) { + proactivePinRetryUsed = true; + onLog?.call( + 'Pairing failed before PIN challenge; requesting PIN for ' + 'proactive retry (attempt ${attempt + 1}/$_maxRetries)', + ); + final pin = await currentPinProvider(); + if (pin == null) { + onLog?.call('PIN entry cancelled for proactive retry'); + return false; + } + final capturedPin = pin.trim(); + currentPinProvider = () async => capturedPin; + continue; + } + return false; + } + + // Timeout path — pairing neither succeeded nor failed. + onLog?.call('Pairing did not complete before timeout'); + if (!result.pinSent && + !proactivePinRetryUsed && + currentPinProvider != null) { + proactivePinRetryUsed = true; + onLog?.call( + 'No PIN challenge observed before timeout; requesting PIN for ' + 'proactive retry (attempt ${attempt + 1}/$_maxRetries)', + ); + final pin = await currentPinProvider(); + if (pin == null) { + onLog?.call('PIN entry cancelled for proactive retry after timeout'); + return false; + } + final capturedPin = pin.trim(); + currentPinProvider = () async => capturedPin; + continue; + } + return false; + } + return false; + } + + /// Runs a single bluetoothctl pairing attempt. + /// + /// Uses a [Completer] to wake as soon as pairing succeeds or fails, + /// instead of polling. + Future<_PairingResult> _runPairingAttempt({ + required String remoteId, + required Duration timeout, + void Function(String message)? onLog, + Future Function()? onRequestPin, + }) async { + onLog?.call('Starting bluetoothctl pairing flow for $remoteId'); + Process process; + try { + process = await _processStart('bluetoothctl', []); + } on ProcessException catch (error) { + onLog?.call('bluetoothctl unavailable, cannot run pairing flow: $error'); + return const _PairingResult(); + } + final output = StringBuffer(); + var pinSent = false; + var sessionClosed = false; + var userCancelledPinEntry = false; + var confirmationHandled = false; + var successHandled = false; + var failureHandled = false; + var detectorBuffer = ''; + final pairingDone = Completer(); + var pairSucceeded = false; + var pairFailed = false; + + void writeCmd(String cmd) { + if (sessionClosed) return; + try { + process.stdin.writeln(cmd); + } on StateError { + sessionClosed = true; + onLog?.call('bluetoothctl stdin already closed; ignoring "$cmd"'); + } + } + + unawaited( + process.exitCode.then((_) { + sessionClosed = true; + if (!pairingDone.isCompleted) pairingDone.complete(); + }), + ); + + void handleChunk(String chunk) { + output.write(chunk); + detectorBuffer += chunk.toLowerCase(); + if (detectorBuffer.length > 4096) { + detectorBuffer = detectorBuffer.substring(detectorBuffer.length - 4096); + } + final lower = detectorBuffer; + + if (!pinSent && + !sessionClosed && + (lower.contains('enter pin code') || + lower.contains('requestpin') || + lower.contains('input pin code') || + lower.contains('request passkey') || + lower.contains('requestpasskey') || + lower.contains('enter passkey'))) { + pinSent = true; + if (onRequestPin == null) { + onLog?.call( + 'PIN/passkey requested but no onRequestPin callback; ' + 'sending empty line to accept default pairing', + ); + writeCmd(''); + } else { + onLog?.call('Pairing agent is ready for PIN/passkey input'); + unawaited( + Future(() async { + String? pin; + try { + pin = await onRequestPin(); + } catch (e) { + onLog?.call('onRequestPin callback threw: $e'); + pairFailed = true; + writeCmd('cancel'); + if (!pairingDone.isCompleted) pairingDone.complete(); + return; + } + if (pin == null) { + if (sessionClosed) { + onLog?.call( + 'PIN prompt resolved after pairing session closed', + ); + return; + } + onLog?.call('PIN entry cancelled by user; cancelling pairing'); + userCancelledPinEntry = true; + pairFailed = true; + writeCmd('cancel'); + if (!pairingDone.isCompleted) pairingDone.complete(); + return; + } + if (sessionClosed) { + onLog?.call( + 'PIN provided after pairing session closed; ignoring', + ); + return; + } + if (pin.trim().isEmpty) { + onLog?.call( + 'Blank PIN submitted; sending empty line to accept default pairing', + ); + writeCmd(''); + } else { + onLog?.call('Submitting PIN/passkey to pairing agent'); + writeCmd(pin.trim()); + } + }), + ); + } + } + + if (!confirmationHandled && + (lower.contains('confirm passkey') || + lower.contains('requestconfirmation') || + lower.contains('[agent] confirm'))) { + confirmationHandled = true; + onLog?.call( + 'Pairing agent requested passkey confirmation; answering yes', + ); + writeCmd('yes'); + } + + if (!successHandled && + (lower.contains('pairing successful') || + lower.contains('already paired'))) { + successHandled = true; + onLog?.call('Pairing reported success'); + pairSucceeded = true; + if (!pairingDone.isCompleted) pairingDone.complete(); + } + + if (!failureHandled && + (lower.contains('failed to pair') || + lower.contains('authenticationfailed') || + lower.contains('authentication failed'))) { + failureHandled = true; + onLog?.call('Pairing reported authentication failure'); + pairFailed = true; + if (!pairingDone.isCompleted) pairingDone.complete(); + } + } + + final stdoutSub = process.stdout + .transform(utf8.decoder) + .listen(handleChunk); + final stderrSub = process.stderr + .transform(utf8.decoder) + .listen(handleChunk); + + writeCmd('power on'); + writeCmd('agent KeyboardDisplay'); + writeCmd('default-agent'); + onLog?.call('Waiting for pairing challenge from bluetoothctl agent'); + writeCmd('pair $remoteId'); + + // Wait for the Completer to fire (success/failure/process exit) or timeout. + await pairingDone.future.timeout(timeout, onTimeout: () {}); + + if (!pairFailed && pairSucceeded) { + onLog?.call('Pair succeeded; trusting and connecting device'); + writeCmd('trust $remoteId'); + writeCmd('connect $remoteId'); + } + writeCmd('quit'); + sessionClosed = true; + + try { + await process.exitCode.timeout(_pairingCleanupTimeout); + } catch (_) { + process.kill(); + } + await stdoutSub.cancel(); + await stderrSub.cancel(); + + if (pairFailed) { + return _PairingResult( + pairFailed: true, + pinSent: pinSent, + userCancelled: userCancelledPinEntry, + ); + } + + final allOutput = output.toString().toLowerCase(); + final reportedSuccess = + pairSucceeded || + allOutput.contains('pairing successful') || + allOutput.contains('already paired'); + if (reportedSuccess) { + final trusted = await trustDevice(remoteId, onLog: onLog); + if (!trusted) { + onLog?.call('Pairing completed but BlueZ trust was not restored'); + } + return _PairingResult(success: trusted, pinSent: pinSent); + } + + return _PairingResult(pinSent: pinSent); + } + + Future _removeDevice( + String remoteId, { + void Function(String message)? onLog, + }) async { + Process process; + try { + process = await _processStart('bluetoothctl', []); + } on ProcessException catch (error) { + onLog?.call( + 'bluetoothctl unavailable, skipping remove for $remoteId: $error', + ); + return; + } + process.stdin.writeln('remove $remoteId'); + process.stdin.writeln('quit'); + try { + await process.exitCode.timeout(_processExitTimeout); + } catch (_) { + process.kill(); + } + onLog?.call('Issued bluetoothctl remove for $remoteId'); + } +} + +/// Outcome of a single bluetoothctl pairing attempt. +class _PairingResult { + final bool success; + final bool pairFailed; + final bool pinSent; + final bool userCancelled; + + const _PairingResult({ + this.success = false, + this.pairFailed = false, + this.pinSent = false, + this.userCancelled = false, + }); +} diff --git a/lib/services/linux_ble_pairing_service_stub.dart b/lib/services/linux_ble_pairing_service_stub.dart new file mode 100644 index 0000000..c00ced8 --- /dev/null +++ b/lib/services/linux_ble_pairing_service_stub.dart @@ -0,0 +1,28 @@ +/// No-op stub for web builds where dart:io is unavailable. +/// +/// The real implementation lives in linux_ble_pairing_service.dart and is +/// selected via conditional import in meshcore_connector.dart. +class LinuxBlePairingService { + LinuxBlePairingService(); + + Future isBluetoothctlAvailable() async => false; + + Future disconnectDevice( + String remoteId, { + void Function(String message)? onLog, + }) async {} + + Future isPairedAndTrusted(String remoteId) async => false; + + Future trustDevice( + String remoteId, { + void Function(String message)? onLog, + }) async => false; + + Future pairAndTrust({ + required String remoteId, + Duration timeout = const Duration(seconds: 45), + void Function(String message)? onLog, + Future Function()? onRequestPin, + }) async => false; +} diff --git a/pubspec.yaml b/pubspec.yaml index 663622b..a20fc00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: material_symbols_icons: ^4.2906.0 web: ^1.1.1 flutter_svg: ^2.0.10+1 + flutter_blue_plus_platform_interface: ^8.2.1 ml_algo: ^16.0.0 ml_dataframe: ^1.0.0 diff --git a/test/services/linux_ble_error_classifier_test.dart b/test/services/linux_ble_error_classifier_test.dart new file mode 100644 index 0000000..b41b17c --- /dev/null +++ b/test/services/linux_ble_error_classifier_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/services/linux_ble_error_classifier.dart'; + +void main() { + group('isLinuxBleConnectFailureText', () { + test('matches flutter_blue_plus connect timeout error', () { + expect( + isLinuxBleConnectFailureText( + 'FlutterBluePlusException | connect | fbp-code: 1 | Timed out after 15s', + ), + isTrue, + ); + }); + + test('matches hard-timeout marker', () { + expect( + isLinuxBleConnectFailureText( + 'TimeoutException: Linux connect hard-timeout after 8s', + ), + isTrue, + ); + }); + + test('matches BlueZ local abort failure', () { + expect( + isLinuxBleConnectFailureText( + 'org.bluez.Error.Failed: le-connection-abort-by-local', + ), + isTrue, + ); + }); + + test('matches BlueZ in-progress failure', () { + expect( + isLinuxBleConnectFailureText( + 'org.bluez.Error.InProgress: Operation already in progress', + ), + isTrue, + ); + }); + + test('matches flutter_blue_plus null-detail connect failure', () { + expect( + isLinuxBleConnectFailureText( + 'FlutterBluePlusException | connect | linux-code: null | null', + ), + isTrue, + ); + }); + + test('matches tagged connect-stage failure marker', () { + expect( + isLinuxBleConnectFailureText( + 'StateError: Linux connect stage failure: Bad state: No element', + ), + isTrue, + ); + }); + + test('does not match connect-shaped pairing auth failure', () { + expect( + isLinuxBleConnectFailureText( + 'FlutterBluePlusException | connect | AuthenticationFailed', + ), + isFalse, + ); + }); + + test('does not match explicit pair auth failure', () { + expect( + isLinuxBleConnectFailureText( + 'FlutterBluePlusException | pair | AuthenticationFailed', + ), + isFalse, + ); + }); + }); + + group('isLikelyLinuxBlePairingTimeoutText', () { + test('matches pair timeout text', () { + expect( + isLikelyLinuxBlePairingTimeoutText('Timed out waiting for pair'), + isTrue, + ); + }); + + test('matches bond timeout text', () { + expect( + isLikelyLinuxBlePairingTimeoutText('Operation timed out during bond'), + isTrue, + ); + }); + + test('does not match generic timeout text', () { + expect( + isLikelyLinuxBlePairingTimeoutText('Timed out after 15s'), + isFalse, + ); + }); + }); + + group('isLinuxBlePairingFailureText', () { + test('matches connect-shaped authentication failure', () { + expect( + isLinuxBlePairingFailureText( + 'FlutterBluePlusException | connect | AuthenticationFailed', + ), + isTrue, + ); + }); + + test('matches app pairing incomplete failure', () { + expect( + isLinuxBlePairingFailureText( + 'StateError: Linux BLE pairing did not complete', + ), + isTrue, + ); + }); + + test('does not match generic bad state error', () { + expect(isLinuxBlePairingFailureText('Bad state: No element'), isFalse); + }); + + test('matches pair-context bad state error', () { + expect( + isLinuxBlePairingFailureText( + 'Pair request failed: Bad state: No element', + ), + isTrue, + ); + }); + + test('matches app trust repair incomplete failure', () { + expect( + isLinuxBlePairingFailureText( + 'StateError: Linux BLE trust repair did not complete', + ), + isTrue, + ); + }); + + test('matches pairing timeout text', () { + expect( + isLinuxBlePairingFailureText('Timed out waiting for pair'), + isTrue, + ); + }); + }); +} diff --git a/test/services/linux_ble_pairing_service_test.dart b/test/services/linux_ble_pairing_service_test.dart new file mode 100644 index 0000000..9d34f52 --- /dev/null +++ b/test/services/linux_ble_pairing_service_test.dart @@ -0,0 +1,418 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/services/linux_ble_pairing_service.dart'; + +class _FakeProcess implements Process { + _FakeProcess({this.stdoutText = '', this.autoFinish = true}) { + _stdin = IOSink(_stdinController.sink); + _stdinController.stream.listen((chunk) { + _stdinBuffer.write(utf8.decode(chunk)); + }); + + // Use Timer.run (event-loop tick) instead of microtask so that broadcast + // listeners in _runPairingAttempt are attached before the event fires. + Timer.run(() { + if (_closed) { + return; + } + if (stdoutText.isNotEmpty) { + _stdoutController.add(utf8.encode(stdoutText)); + } + }); + + if (autoFinish) { + // Scheduled after the Timer.run above (FIFO order), so stdout is + // emitted before the process exits. + Timer(Duration.zero, () async { + await _finish(exitStatus); + }); + } + } + + final String stdoutText; + final bool autoFinish; + final int exitStatus = 0; + final StreamController> _stdinController = + StreamController>(); + final StreamController> _stdoutController = + StreamController>.broadcast(); + final StreamController> _stderrController = + StreamController>.broadcast(); + final Completer _exitCodeCompleter = Completer(); + final StringBuffer _stdinBuffer = StringBuffer(); + late final IOSink _stdin; + bool _closed = false; + + String get stdinText => _stdinBuffer.toString(); + + void emitStdout(String text) { + if (!_closed) { + _stdoutController.add(utf8.encode(text)); + } + } + + void finishProcess([int code = 0]) { + unawaited(_finish(code)); + } + + Future _finish(int code) async { + if (_closed) { + return; + } + _closed = true; + await _stdin.close(); + await _stdoutController.close(); + await _stderrController.close(); + if (!_exitCodeCompleter.isCompleted) { + _exitCodeCompleter.complete(code); + } + } + + @override + Future get exitCode => _exitCodeCompleter.future; + + @override + bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { + unawaited(_finish(exitStatus)); + return true; + } + + @override + int get pid => 1; + + @override + IOSink get stdin => _stdin; + + @override + Stream> get stderr => _stderrController.stream; + + @override + Stream> get stdout => _stdoutController.stream; +} + +void main() { + test( + 'disconnectDevice skips gracefully when bluetoothctl is unavailable', + () async { + final logs = []; + final service = LinuxBlePairingService( + processStart: (executable, arguments) async { + throw const ProcessException( + 'bluetoothctl', + [], + 'not found', + 2, + ); + }, + ); + + await service.disconnectDevice('AA:BB:CC:DD:EE:FF', onLog: logs.add); + + expect( + logs.any((line) => line.contains('bluetoothctl unavailable')), + isTrue, + ); + }, + ); + + test( + 'isPairedAndTrusted returns false when bluetoothctl is unavailable', + () async { + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + throw const ProcessException( + 'bluetoothctl', + [], + 'not found', + 2, + ); + }, + ); + + final trusted = await service.isPairedAndTrusted('AA:BB:CC:DD:EE:FF'); + expect(trusted, isFalse); + }, + ); + + test('isBluetoothctlAvailable returns false when unavailable', () async { + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + throw const ProcessException( + 'bluetoothctl', + [], + 'not found', + 2, + ); + }, + ); + + final available = await service.isBluetoothctlAvailable(); + expect(available, isFalse); + }); + + test( + 'isBluetoothctlAvailable returns true when version command succeeds', + () async { + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + return ProcessResult(1234, 0, '5.72', ''); + }, + ); + + final available = await service.isBluetoothctlAvailable(); + expect(available, isTrue); + }, + ); + + test( + 'isPairedAndTrusted returns true when paired and trusted are yes', + () async { + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: yes +''', ''); + }, + ); + + final trusted = await service.isPairedAndTrusted('AA:BB:CC:DD:EE:FF'); + expect(trusted, isTrue); + }, + ); + + test('pairAndTrust returns false when bluetoothctl is unavailable', () async { + final service = LinuxBlePairingService( + processStart: (executable, arguments) async { + throw const ProcessException( + 'bluetoothctl', + [], + 'not found', + 2, + ); + }, + ); + + final paired = await service.pairAndTrust(remoteId: 'AA:BB:CC:DD:EE:FF'); + expect(paired, isFalse); + }); + + test('trustDevice verifies trust after trust command succeeds', () async { + final logs = []; + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + switch (arguments.first) { + case 'trust': + return ProcessResult(1234, 0, 'trust succeeded', ''); + case 'info': + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: yes +''', ''); + } + fail('Unexpected bluetoothctl arguments: $arguments'); + }, + ); + + final trusted = await service.trustDevice( + 'AA:BB:CC:DD:EE:FF', + onLog: logs.add, + ); + + expect(trusted, isTrue); + expect(logs.any((line) => line.contains('Verified BlueZ trust')), isTrue); + }); + + test( + 'trustDevice returns false when trust verification stays untrusted', + () async { + final logs = []; + final service = LinuxBlePairingService( + processRun: (executable, arguments) async { + switch (arguments.first) { + case 'trust': + return ProcessResult(1234, 0, 'trust succeeded', ''); + case 'info': + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: no +''', ''); + } + fail('Unexpected bluetoothctl arguments: $arguments'); + }, + ); + + final trusted = await service.trustDevice( + 'AA:BB:CC:DD:EE:FF', + onLog: logs.add, + ); + + expect(trusted, isFalse); + expect( + logs.any((line) => line.contains('trust verification failed')), + isTrue, + ); + }, + ); + + test( + 'pairAndTrust fails when pairing reports success but trust is not restored', + () async { + final logs = []; + final service = LinuxBlePairingService( + processStart: (executable, arguments) async => + _FakeProcess(stdoutText: 'Pairing successful\n'), + processRun: (executable, arguments) async { + switch (arguments.first) { + case 'trust': + return ProcessResult(1234, 0, 'trust succeeded', ''); + case 'info': + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: no +''', ''); + } + fail('Unexpected bluetoothctl arguments: $arguments'); + }, + ); + + final paired = await service.pairAndTrust( + remoteId: 'AA:BB:CC:DD:EE:FF', + onLog: logs.add, + ); + + expect(paired, isFalse); + expect( + logs.any((line) => line.contains('trust was not restored')), + isTrue, + ); + }, + ); + + test( + 'pairAndTrust succeeds without requesting proactive PIN after success', + () async { + final logs = []; + var pinRequests = 0; + final service = LinuxBlePairingService( + processStart: (executable, arguments) async => + _FakeProcess(stdoutText: 'Pairing successful\n'), + processRun: (executable, arguments) async { + switch (arguments.first) { + case 'trust': + return ProcessResult(1234, 0, 'trust succeeded', ''); + case 'info': + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: yes +''', ''); + } + fail('Unexpected bluetoothctl arguments: $arguments'); + }, + ); + + final paired = await service.pairAndTrust( + remoteId: 'AA:BB:CC:DD:EE:FF', + onLog: logs.add, + onRequestPin: () async { + pinRequests++; + return '123456'; + }, + ); + + expect(paired, isTrue); + expect(pinRequests, 0); + expect( + logs.any((line) => line.contains('did not complete before timeout')), + isFalse, + ); + }, + ); + + test( + 'pairAndTrust sends empty line when blank PIN is submitted (not cancel)', + () async { + final logs = []; + late final _FakeProcess fakeProc; + final service = LinuxBlePairingService( + processStart: (executable, arguments) async { + fakeProc = _FakeProcess(stdoutText: '', autoFinish: false); + // Emit PIN prompt after an event-loop tick (not microtask) so + // broadcast listeners are attached first. + Timer.run(() { + fakeProc.emitStdout('Enter PIN code:\n'); + Future.delayed(const Duration(milliseconds: 100), () { + fakeProc.emitStdout('Pairing successful\n'); + Future.delayed(const Duration(milliseconds: 50), () { + fakeProc.finishProcess(); + }); + }); + }); + return fakeProc; + }, + processRun: (executable, arguments) async { + switch (arguments.first) { + case 'trust': + return ProcessResult(1234, 0, 'trust succeeded', ''); + case 'info': + return ProcessResult(1234, 0, ''' +Device AA:BB:CC:DD:EE:FF + Paired: yes + Trusted: yes +''', ''); + } + fail('Unexpected bluetoothctl arguments: $arguments'); + }, + ); + + final paired = await service.pairAndTrust( + remoteId: 'AA:BB:CC:DD:EE:FF', + timeout: const Duration(seconds: 5), + onLog: logs.add, + onRequestPin: () async => '', + ); + + expect(paired, isTrue); + expect(logs.any((line) => line.contains('Blank PIN submitted')), isTrue); + expect(logs.any((line) => line.contains('cancelling pairing')), isFalse); + }, + ); + + test('pairAndTrust cancels pairing when PIN dialog returns null', () async { + final logs = []; + final service = LinuxBlePairingService( + processStart: (executable, arguments) async { + final proc = _FakeProcess(stdoutText: '', autoFinish: false); + Timer.run(() { + proc.emitStdout('Enter PIN code:\n'); + // Process will be killed/quit by the pairing service after cancel + Future.delayed(const Duration(milliseconds: 200), () { + proc.finishProcess(); + }); + }); + return proc; + }, + processRun: (executable, arguments) async { + return ProcessResult(1234, 0, '', ''); + }, + ); + + final paired = await service.pairAndTrust( + remoteId: 'AA:BB:CC:DD:EE:FF', + timeout: const Duration(seconds: 3), + onLog: logs.add, + onRequestPin: () async => null, + ); + + expect(paired, isFalse); + expect(logs.any((line) => line.contains('cancelled by user')), isTrue); + }); +} From 990f2bd33d1efdf3510846c82addffeca647dc6b Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 21:06:18 -0400 Subject: [PATCH 2/6] addressed copilot issues still need pr #301 for smoke tests to pass --- lib/connector/meshcore_connector.dart | 11 ++++++++++- lib/services/linux_ble_pairing_service.dart | 12 ++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f176cf6..24d434e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1701,7 +1701,9 @@ class MeshCoreConnector extends ChangeNotifier { remoteId, ); final needsBondRecovery = - pluginBondState != BmBondStateEnum.bonded || !trustedByBluez; + (pluginBondState != null && + pluginBondState != BmBondStateEnum.bonded) || + !trustedByBluez; if (!needsBondRecovery) { return false; } @@ -1770,6 +1772,11 @@ class MeshCoreConnector extends ChangeNotifier { 'bluetoothctl unavailable; continuing with plugin bonded state', tag: 'BLE Connect', ); + } else if (beforeBondState == null) { + _appDebugLogService?.warn( + 'bluetoothctl unavailable and plugin bond state is unknown; skipping Linux pairing fallback', + tag: 'BLE Connect', + ); } else { _appDebugLogService?.warn( 'bluetoothctl unavailable and device is not bonded; skipping Linux pairing fallback', @@ -1817,6 +1824,8 @@ class MeshCoreConnector extends ChangeNotifier { _appDebugLogService?.info( beforeBondState == BmBondStateEnum.bonded ? 'Linux BLE device still untrusted after repair; requesting pair' + : beforeBondState == null + ? 'Linux BLE device bond state unknown; requesting pair' : 'Linux BLE device not bonded, requesting pair', tag: 'BLE Connect', ); diff --git a/lib/services/linux_ble_pairing_service.dart b/lib/services/linux_ble_pairing_service.dart index b3d4fc4..8ed52f6 100644 --- a/lib/services/linux_ble_pairing_service.dart +++ b/lib/services/linux_ble_pairing_service.dart @@ -12,9 +12,9 @@ typedef ProcessRunFn = /// This is used only as a fallback when BlueZ pairing via flutter_blue_plus /// fails to surface agent prompts in-app. class LinuxBlePairingService { - /// Maximum number of retry attempts for the pairing flow. + /// Maximum number of pairing attempts (initial + retries). /// Covers one remove-and-retry plus one proactive-PIN retry. - static const int _maxRetries = 2; + static const int _maxAttempts = 3; static const Duration _processExitTimeout = Duration(seconds: 6); static const Duration _pairingCleanupTimeout = Duration(seconds: 5); @@ -110,7 +110,7 @@ class LinuxBlePairingService { var proactivePinRetryUsed = false; Future Function()? currentPinProvider = onRequestPin; - for (var attempt = 0; attempt <= _maxRetries; attempt++) { + for (var attempt = 0; attempt < _maxAttempts; attempt++) { final result = await _runPairingAttempt( remoteId: remoteId, timeout: timeout, @@ -129,7 +129,7 @@ class LinuxBlePairingService { removeRetryUsed = true; onLog?.call( 'Pairing failed; removing cached bond and retrying ' - '(attempt ${attempt + 1}/$_maxRetries)', + '(attempt ${attempt + 1}/$_maxAttempts)', ); await _removeDevice(remoteId, onLog: onLog); continue; @@ -140,7 +140,7 @@ class LinuxBlePairingService { proactivePinRetryUsed = true; onLog?.call( 'Pairing failed before PIN challenge; requesting PIN for ' - 'proactive retry (attempt ${attempt + 1}/$_maxRetries)', + 'proactive retry (attempt ${attempt + 1}/$_maxAttempts)', ); final pin = await currentPinProvider(); if (pin == null) { @@ -162,7 +162,7 @@ class LinuxBlePairingService { proactivePinRetryUsed = true; onLog?.call( 'No PIN challenge observed before timeout; requesting PIN for ' - 'proactive retry (attempt ${attempt + 1}/$_maxRetries)', + 'proactive retry (attempt ${attempt + 1}/$_maxAttempts)', ); final pin = await currentPinProvider(); if (pin == null) { From d07372c7e09f0ea4c722dee3c5847e6dcf141ec7 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 16 Mar 2026 11:37:45 -0400 Subject: [PATCH 3/6] feat: add MeshCoreUuids class for UUID constants and device name prefixes --- lib/connector/meshcore_connector.dart | 15 +++++---------- lib/connector/meshcore_uuids.dart | 7 +++++++ 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 lib/connector/meshcore_uuids.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 24d434e..b6bd1a4 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -39,14 +39,9 @@ import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; import '../utils/platform_info.dart'; +import 'meshcore_uuids.dart'; import 'meshcore_protocol.dart'; -class MeshCoreUuids { - static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; - static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; - static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; -} - class DirectRepeater { static const int maxAgeMinutes = 30; // Max age for direct repeater info final int pubkeyFirstByte; @@ -995,7 +990,7 @@ class MeshCoreConnector extends ChangeNotifier { try { await FlutterBluePlus.startScan( - withKeywords: ["MeshCore-", "Whisper-"], + withKeywords: MeshCoreUuids.deviceNamePrefixes, webOptionalServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, @@ -1020,9 +1015,9 @@ class MeshCoreConnector extends ChangeNotifier { ..addAll( systemDevices .where( - (device) => - device.platformName.startsWith('MeshCore-') || - device.platformName.startsWith('Whisper-'), + (device) => MeshCoreUuids.deviceNamePrefixes.any( + device.platformName.startsWith, + ), ) .map( (device) => ScanResult( diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart new file mode 100644 index 0000000..52d62e4 --- /dev/null +++ b/lib/connector/meshcore_uuids.dart @@ -0,0 +1,7 @@ +class MeshCoreUuids { + static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; + static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; + static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + + static const List deviceNamePrefixes = ["MeshCore-", "Whisper-", "Wiscore-", "HT-"]; +} From e49e80d3304decb48b58346445e6e7f1d6b3f757 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 16 Mar 2026 19:16:55 -0400 Subject: [PATCH 4/6] style: format deviceNamePrefixes list for better readability --- lib/connector/meshcore_uuids.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index 52d62e4..bb5f9bc 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -3,5 +3,10 @@ class MeshCoreUuids { static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; - static const List deviceNamePrefixes = ["MeshCore-", "Whisper-", "Wiscore-", "HT-"]; + static const List deviceNamePrefixes = [ + "MeshCore-", + "Whisper-", + "Wiscore-", + "HT-", + ]; } From 14f3429eb5be3ae5894c5117a68d871a365a9ef8 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sat, 21 Mar 2026 21:07:56 -0400 Subject: [PATCH 5/6] fix: correct casing of "WisCore-" in deviceNamePrefixes list --- lib/connector/meshcore_uuids.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index bb5f9bc..da7f6b5 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -6,7 +6,7 @@ class MeshCoreUuids { static const List deviceNamePrefixes = [ "MeshCore-", "Whisper-", - "Wiscore-", + "WisCore-", "HT-", ]; } From 38f4de80b622e6068922398242731564b483cabc Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:21:23 +0000 Subject: [PATCH 6/6] Refactor Bluetooth pairing localization strings across multiple languages - Reintroduced Bluetooth pairing PIN title, prompt, show, and hide strings in English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese. - Updated localization files to ensure consistency and clarity in user prompts related to Bluetooth pairing. --- lib/l10n/app_bg.arb | 20 ++++++++----- lib/l10n/app_de.arb | 20 ++++++++----- lib/l10n/app_en.arb | 27 +++++++++-------- lib/l10n/app_es.arb | 20 ++++++++----- lib/l10n/app_fr.arb | 20 ++++++++----- lib/l10n/app_hu.arb | 13 +++++++- lib/l10n/app_it.arb | 20 ++++++++----- lib/l10n/app_ja.arb | 13 +++++++- lib/l10n/app_ko.arb | 13 +++++++- lib/l10n/app_localizations.dart | 48 +++++++++++++++--------------- lib/l10n/app_localizations_bg.dart | 29 +++++++++--------- lib/l10n/app_localizations_de.dart | 28 ++++++++--------- lib/l10n/app_localizations_en.dart | 28 ++++++++--------- lib/l10n/app_localizations_es.dart | 29 +++++++++--------- lib/l10n/app_localizations_fr.dart | 29 +++++++++--------- lib/l10n/app_localizations_hu.dart | 14 +++++++++ lib/l10n/app_localizations_it.dart | 29 +++++++++--------- lib/l10n/app_localizations_ja.dart | 14 +++++++++ lib/l10n/app_localizations_ko.dart | 14 +++++++++ lib/l10n/app_localizations_nl.dart | 28 ++++++++--------- lib/l10n/app_localizations_pl.dart | 29 +++++++++--------- lib/l10n/app_localizations_pt.dart | 28 ++++++++--------- lib/l10n/app_localizations_ru.dart | 29 +++++++++--------- lib/l10n/app_localizations_sk.dart | 28 ++++++++--------- lib/l10n/app_localizations_sl.dart | 29 +++++++++--------- lib/l10n/app_localizations_sv.dart | 28 ++++++++--------- lib/l10n/app_localizations_uk.dart | 29 +++++++++--------- lib/l10n/app_localizations_zh.dart | 28 ++++++++--------- lib/l10n/app_nl.arb | 20 ++++++++----- lib/l10n/app_pl.arb | 20 ++++++++----- lib/l10n/app_pt.arb | 20 ++++++++----- lib/l10n/app_ru.arb | 20 ++++++++----- lib/l10n/app_sk.arb | 20 ++++++++----- lib/l10n/app_sl.arb | 20 ++++++++----- lib/l10n/app_sv.arb | 20 ++++++++----- lib/l10n/app_uk.arb | 20 ++++++++----- lib/l10n/app_zh.arb | 20 ++++++++----- 37 files changed, 495 insertions(+), 369 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index d40535f..0f5145d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_multiAck": "Мулти-потвърди: {value}", "settings_telemetryModeUpdated": "Режим на телеметрията е обновен", "map_showOverlaps": "Покриване на ключа на повтаряча", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN за свързване чрез Bluetooth", - "scanner_linuxPairingShowPin": "Покажи PIN", - "scanner_linuxPairingHidePin": "Скриване на PIN кода", - "scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв)." "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm", "radioStats_stripWaiting": "Извличане на данни за радиото…", "radioStats_settingsTile": "Статистически данни за радиостанции", - "radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос" + "radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingHidePin": "Скрий ПИН", + "scanner_linuxPairingShowPin": "Покажи PIN", + "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth", + "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 48064e3..c156a44 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1968,9 +1968,6 @@ "appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen", "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert", "settings_multiAck": "Mehrfach-Bestätigungen: {value}", "map_showOverlaps": "Überlappungen der Repeater-Taste", @@ -1996,10 +1993,6 @@ } } }, - "scanner_linuxPairingHidePin": "PIN verbergen", - "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_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2041,5 +2034,16 @@ "radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm", "radioStats_stripWaiting": "Abrufen von Radiostatus…", "radioStats_settingsTile": "Senderinformationen", - "radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit" + "radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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)." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fb2abae..d8d73ab 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -127,17 +127,7 @@ } } }, - "scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN", - "scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { - "type": "String" - } - } - }, - "scanner_linuxPairingShowPin": "Show PIN", - "scanner_linuxPairingHidePin": "Hide PIN", + "scanner_stop": "Stop", "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", @@ -2051,5 +2041,16 @@ }, "radioStats_stripWaiting": "Fetching radio stats…", "radioStats_settingsTile": "Radio stats", - "radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime" -} + "radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime", + "scanner_linuxPairingShowPin": "Show PIN", + "scanner_linuxPairingHidePin": "Hide PIN", + "scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN", + "scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + } +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b77b9ac..245f732 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1968,9 +1968,6 @@ "appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes", "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Modo de telemetría actualizado", "settings_multiAck": "Multi-ACKs: {value}", "map_showOverlaps": "Superposiciones de tecla repetidora", @@ -1996,10 +1993,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).", - "scanner_linuxPairingHidePin": "Ocultar PIN", - "scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth", - "scanner_linuxPairingShowPin": "Mostrar código PIN" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2041,5 +2034,16 @@ "radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm", "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" + "radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Mostrar PIN", + "scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth", + "scanner_linuxPairingHidePin": "Ocultar PIN", + "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5edf2d3..21b231a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_multiAck": "Multi-ACKs : {value}", "settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour", "map_showOverlaps": "Chevauchement de la touche répétitive", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingShowPin": "Afficher le code PIN", - "scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth", - "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", - "scanner_linuxPairingHidePin": "Masquer le code PIN" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm", "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" + "radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Afficher le code PIN", + "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)." } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 558e1f0..dc96020 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2044,5 +2044,16 @@ "contact_teleEnv": "Adatkapcsolati környezet", "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." + "map_runTraceWithReturnPath": "Visszaforduljon az eredeti úton.", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingHidePin": "PIN elrejtése", + "scanner_linuxPairingShowPin": "PIN megjelenítése", + "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 241cfdd..13a9602 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio", "appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Modalità telemetria aggiornata", "settings_multiAck": "Multi-ACKs: {value}", "map_showOverlaps": "Sovrapposizioni della chiave ripetitore", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingShowPin": "Mostra PIN", - "scanner_linuxPairingHidePin": "Nascondi il PIN", - "scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).", - "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm", "radioStats_stripWaiting": "Recupero delle statistiche radio…", "radioStats_settingsTile": "Statistiche radio", - "radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione" + "radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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'è)." } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index d63834c..adb4eea 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2044,5 +2044,16 @@ "contact_teleEnv": "テレメトリ環境", "contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する", "map_showOverlaps": "リピーターキーの重複", - "map_runTraceWithReturnPath": "元の経路に戻る。" + "map_runTraceWithReturnPath": "元の経路に戻る。", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "PINを表示", + "scanner_linuxPairingHidePin": "PINを非表示", + "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", + "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index c2215b9..6bccc19 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2044,5 +2044,16 @@ "contact_teleEnv": "텔레메트리 환경", "contact_teleEnvSubtitle": "환경 센서 데이터를 공유하도록 허용", "map_showOverlaps": "반복 키 중복", - "map_runTraceWithReturnPath": "원래 경로로 돌아가세요." + "map_runTraceWithReturnPath": "원래 경로로 돌아가세요.", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "PIN 표시", + "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN", + "scanner_linuxPairingHidePin": "PIN 숨기기", + "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index fc2771e..db787b3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -598,30 +598,6 @@ abstract class AppLocalizations { /// **'Connection failed: {error}'** String scanner_connectionFailed(String error); - /// No description provided for @scanner_linuxPairingPinTitle. - /// - /// In en, this message translates to: - /// **'Bluetooth Pairing PIN'** - String get scanner_linuxPairingPinTitle; - - /// No description provided for @scanner_linuxPairingPinPrompt. - /// - /// In en, this message translates to: - /// **'Enter PIN for {deviceName} (leave blank if none).'** - String scanner_linuxPairingPinPrompt(String deviceName); - - /// No description provided for @scanner_linuxPairingShowPin. - /// - /// In en, this message translates to: - /// **'Show PIN'** - String get scanner_linuxPairingShowPin; - - /// No description provided for @scanner_linuxPairingHidePin. - /// - /// In en, this message translates to: - /// **'Hide PIN'** - String get scanner_linuxPairingHidePin; - /// No description provided for @scanner_stop. /// /// In en, this message translates to: @@ -6172,6 +6148,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Noise floor, RSSI, SNR, and airtime'** String get radioStats_settingsSubtitle; + + /// No description provided for @scanner_linuxPairingShowPin. + /// + /// In en, this message translates to: + /// **'Show PIN'** + String get scanner_linuxPairingShowPin; + + /// No description provided for @scanner_linuxPairingHidePin. + /// + /// In en, this message translates to: + /// **'Hide PIN'** + String get scanner_linuxPairingHidePin; + + /// No description provided for @scanner_linuxPairingPinTitle. + /// + /// In en, this message translates to: + /// **'Bluetooth Pairing PIN'** + String get scanner_linuxPairingPinTitle; + + /// No description provided for @scanner_linuxPairingPinPrompt. + /// + /// In en, this message translates to: + /// **'Enter PIN for {deviceName} (leave blank if none).'** + String scanner_linuxPairingPinPrompt(String deviceName); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index d66dde8..2909278 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -268,20 +268,6 @@ class AppLocalizationsBg extends AppLocalizations { return 'Връзката не успя: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN за свързване чрез Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Покажи PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Скриване на PIN кода'; - @override String get scanner_stop => 'Спрете'; @@ -3581,4 +3567,19 @@ class AppLocalizationsBg extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Ниво на шума, RSSI, SNR и време на пренос'; + + @override + String get scanner_linuxPairingShowPin => 'Покажи PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Скрий ПИН'; + + @override + String get scanner_linuxPairingPinTitle => + 'PIN код за сдвояване на Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Въведете ПИН за $deviceName (оставете празно, ако няма).'; + } } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1bc2b7a..4afefde 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -271,20 +271,6 @@ class AppLocalizationsDe extends AppLocalizations { return 'Verbindungsfehler: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN für die Bluetooth-Verbindung'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Geben Sie den PIN-Code für $deviceName ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).'; - } - - @override - String get scanner_linuxPairingShowPin => 'PIN anzeigen'; - - @override - String get scanner_linuxPairingHidePin => 'PIN verbergen'; - @override String get scanner_stop => 'Stopp'; @@ -3590,4 +3576,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit'; + + @override + String get scanner_linuxPairingShowPin => 'PIN anzeigen'; + + @override + String get scanner_linuxPairingHidePin => 'PIN ausblenden'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1bc45d7..a420a55 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -265,20 +265,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Connection failed: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Enter PIN for $deviceName (leave blank if none).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Show PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Hide PIN'; - @override String get scanner_stop => 'Stop'; @@ -3515,4 +3501,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Noise floor, RSSI, SNR, and airtime'; + + @override + String get scanner_linuxPairingShowPin => 'Show PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Hide PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Enter PIN for $deviceName (leave blank if none).'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 522adb8..93a8bc9 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -269,21 +269,6 @@ class AppLocalizationsEs extends AppLocalizations { return 'Error de conexión: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN para emparejar dispositivos Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Mostrar código PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Ocultar PIN'; - @override String get scanner_stop => 'Detener'; @@ -3584,4 +3569,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Nivel de ruido, RSSI, SNR y tiempo de transmisión'; + + @override + String get scanner_linuxPairingShowPin => 'Mostrar PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Ocultar PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'PIN de emparejamiento Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Introduzca el PIN para $deviceName (déjelo en blanco si no hay ninguno).'; + } } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 2fc4a5b..9912542 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -271,21 +271,6 @@ class AppLocalizationsFr extends AppLocalizations { return 'Échec de la connexion : $error'; } - @override - 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 nécessaire).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Afficher le code PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Masquer le code PIN'; - @override String get scanner_stop => 'Arrêter'; @@ -3610,4 +3595,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne'; + + @override + String get scanner_linuxPairingShowPin => 'Afficher le code PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Masquer le code PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Code PIN d’appairage Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Entrez le code PIN pour $deviceName (laissez vide si aucun).'; + } } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 7a0bf11..dc6374a 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3586,4 +3586,18 @@ class AppLocalizationsHu extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama'; + + @override + String get scanner_linuxPairingShowPin => 'PIN megjelenítése'; + + @override + String get scanner_linuxPairingHidePin => 'PIN elrejtése'; + + @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).'; + } } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 8e6e7ff..3fc5e56 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -271,21 +271,6 @@ class AppLocalizationsIt extends AppLocalizations { return 'Connessione fallita: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN per l\'accoppiamento Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Mostra PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Nascondi il PIN'; - @override String get scanner_stop => 'Interrompere'; @@ -3588,4 +3573,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione'; + + @override + String get scanner_linuxPairingShowPin => 'Mostra PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Nascondi PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'PIN di associazione Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Inserisci il PIN per $deviceName (lascia vuoto se non ce n\'è).'; + } } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 12f7bb7..03d70d4 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3401,4 +3401,18 @@ class AppLocalizationsJa extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'ノイズレベル、RSSI、SNR、および通信時間'; + + @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を入力してください(なしの場合は空欄のまま)。'; + } } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 57ee40a..5e5925f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3400,4 +3400,18 @@ class AppLocalizationsKo extends AppLocalizations { @override String get radioStats_settingsSubtitle => '잡음 수준, RSSI, 신호 대 잡음비, 통신 시간'; + + @override + String get scanner_linuxPairingShowPin => 'PIN 표시'; + + @override + String get scanner_linuxPairingHidePin => 'PIN 숨기기'; + + @override + String get scanner_linuxPairingPinTitle => '블루투스 페어링 PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return '$deviceName에 대한 PIN을 입력하세요 (없으면 비워두세요).'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 22c1cb1..bcb2d5d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -267,20 +267,6 @@ class AppLocalizationsNl extends AppLocalizations { return 'Verbinding mislukt: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN voor Bluetooth-koppeling'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Voer het pincode-in voor $deviceName in (laat dit leeg als er geen is).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Toon PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Verberg PIN'; - @override String get scanner_stop => 'Stoppen'; @@ -3565,4 +3551,18 @@ class AppLocalizationsNl extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd'; + + @override + String get scanner_linuxPairingShowPin => 'Toon PIN'; + + @override + String get scanner_linuxPairingHidePin => 'PIN verbergen'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth‑koppelings‑PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Voer PIN in voor $deviceName (laat leeg als er geen is).'; + } } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2a8aadb..5c66761 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -272,21 +272,6 @@ class AppLocalizationsPl extends AppLocalizations { return 'Połączenie nieudane: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN do sparowania przez Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Wprowadź kod PIN dla $deviceName (pust, jeśli nie jest wymagany).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Wyświetl kod PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Ukryj kod PIN'; - @override String get scanner_stop => 'Zatrzymaj'; @@ -3600,4 +3585,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Szum tła, RSSI, SNR oraz czas dostępny'; + + @override + String get scanner_linuxPairingShowPin => 'Pokaż PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Ukryj PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Kod PIN parowania Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).'; + } } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 1b4ba3d..98c72f5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -270,20 +270,6 @@ class AppLocalizationsPt extends AppLocalizations { return 'Falha na conexão: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN de pareamento Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Insira o código PIN para $deviceName (deixe em branco se não houver).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Mostrar PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Esconder o PIN'; - @override String get scanner_stop => 'Pare'; @@ -3580,4 +3566,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Nível de ruído, RSSI, SNR e tempo de transmissão'; + + @override + String get scanner_linuxPairingShowPin => 'Mostrar PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Ocultar PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Insira o PIN para $deviceName (deixe em branco se não houver).'; + } } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index eabc655..4184641 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -270,21 +270,6 @@ class AppLocalizationsRu extends AppLocalizations { return 'Подключение не удалось: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN для сопряжения устройств по Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Введите PIN-код для $deviceName (оставьте поле пустым, если PIN-код отсутствует).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Показать PIN-код'; - - @override - String get scanner_linuxPairingHidePin => 'Скрыть PIN-код'; - @override String get scanner_stop => 'Стоп'; @@ -3595,4 +3580,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Уровень шума, RSSI, SNR и время передачи'; + + @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 (оставьте пустым, если нет).'; + } } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 5477951..59f46bd 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -269,20 +269,6 @@ class AppLocalizationsSk extends AppLocalizations { return 'Pripojenie zlyhalo: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Zadajte PIN pre $deviceName (nechajte prázdne, ak neexistuje).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Zobraziť PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Skryť PIN'; - @override String get scanner_stop => 'Zastavte'; @@ -3558,4 +3544,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Úroveň hluku, RSSI, SNR a časové rozloženie'; + + @override + String get scanner_linuxPairingShowPin => 'Zobraziť PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Skryť PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth párovací PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Zadajte PIN pre $deviceName (ak nie je, nechajte prázdne).'; + } } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 3ab2972..171353c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -267,21 +267,6 @@ class AppLocalizationsSl extends AppLocalizations { return 'Pošlo je z povezavo: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN za združevanje preko Bluetootha'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Vnesite PIN kodo za $deviceName (ostavite prazno, če nimate kode).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Prikaži PIN'; - - @override - String get scanner_linuxPairingHidePin => 'Skrijte PIN'; - @override String get scanner_stop => 'Prekliči'; @@ -3564,4 +3549,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema'; + + @override + String get scanner_linuxPairingShowPin => 'Prikaži PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Skrij PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).'; + } } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c85fbd5..6a776d7 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -266,20 +266,6 @@ class AppLocalizationsSv extends AppLocalizations { return 'Anslutning misslyckades: $error'; } - @override - String get scanner_linuxPairingPinTitle => 'PIN för Bluetooth-parning'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Ange PIN-kod för $deviceName (lämna tomt om ingen finns).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Visa PIN-kod'; - - @override - String get scanner_linuxPairingHidePin => 'Dölj PIN-kod'; - @override String get scanner_stop => 'Stoppa'; @@ -3540,4 +3526,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Bakgrundsnivå, RSSI, SNR och tillgänglig tid'; + + @override + String get scanner_linuxPairingShowPin => 'Visa PIN'; + + @override + String get scanner_linuxPairingHidePin => 'Dölj PIN'; + + @override + String get scanner_linuxPairingPinTitle => 'Bluetooth‑parnings‑PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return 'Ange PIN för $deviceName (lämna tomt om ingen).'; + } } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7a31f8f..9ebead2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -269,21 +269,6 @@ class AppLocalizationsUk extends AppLocalizations { return 'Помилка підключення: $error'; } - @override - String get scanner_linuxPairingPinTitle => - 'PIN для з\'єднання через Bluetooth'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Введіть PIN-код для $deviceName (залиште поле порожнім, якщо немає).'; - } - - @override - String get scanner_linuxPairingShowPin => 'Показати PIN-код'; - - @override - String get scanner_linuxPairingHidePin => 'Приховати PIN-код'; - @override String get scanner_stop => 'Стоп'; @@ -3598,4 +3583,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get radioStats_settingsSubtitle => 'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.'; + + @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 (залиште порожнім, якщо його немає).'; + } } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 5936d91..6d3a856 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -256,20 +256,6 @@ class AppLocalizationsZh extends AppLocalizations { return '连接失败:$error'; } - @override - String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN'; - - @override - String scanner_linuxPairingPinPrompt(String deviceName) { - return '输入 $deviceName 的 PIN 码(如果为空,则留空)。'; - } - - @override - String get scanner_linuxPairingShowPin => '显示PIN码'; - - @override - String get scanner_linuxPairingHidePin => '隐藏PIN码'; - @override String get scanner_stop => '停止'; @@ -3312,4 +3298,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间'; + + @override + String get scanner_linuxPairingShowPin => '显示 PIN码'; + + @override + String get scanner_linuxPairingHidePin => '隐藏 PIN'; + + @override + String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN'; + + @override + String scanner_linuxPairingPinPrompt(String deviceName) { + return '输入 $deviceName 的 PIN(如果没有,请留空)。'; + } } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 0b8b4f2..9f164fd 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen", "appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt", "settings_multiAck": "Multi-ACKs: {value}", "map_showOverlaps": "Herhalingssleutel overlapt", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN voor Bluetooth-koppeling", - "scanner_linuxPairingShowPin": "Toon PIN", - "scanner_linuxPairingHidePin": "Verberg PIN", - "scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is)." "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm", "radioStats_stripWaiting": "Radio-statistieken ophalen…", "radioStats_settingsTile": "Statistieken over radio", - "radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd" + "radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index fcade6c..87b4754 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1978,9 +1978,6 @@ "appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości", "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany", "settings_multiAck": "Wiele potwierdzeń: {value}", "map_showOverlaps": "Nakładające się klucze powtarzalne", @@ -2006,10 +2003,6 @@ } } }, - "scanner_linuxPairingHidePin": "Ukryj kod PIN", - "scanner_linuxPairingPinTitle": "PIN do sparowania przez Bluetooth", - "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pust, jeśli nie jest wymagany).", - "scanner_linuxPairingShowPin": "Wyświetl kod PIN" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2051,5 +2044,16 @@ "radioStats_stripNoise": "Poziom szumów: {noiseDbm} dBm", "radioStats_stripWaiting": "Pobieranie danych dotyczących radia…", "radioStats_settingsTile": "Statystyki radiowe", - "radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny" + "radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index aeb42c9..eb87a15 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens", "appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Modo de telemetria atualizado", "settings_multiAck": "Multi-ACKs: {value}", "map_showOverlaps": "Sobreposições da Chave Repeater", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN de pareamento Bluetooth", - "scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).", - "scanner_linuxPairingShowPin": "Mostrar PIN", - "scanner_linuxPairingHidePin": "Esconder o PIN" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm", "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" + "radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 1fc3a69..c9493a0 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1180,9 +1180,6 @@ "appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения", "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Режим телеметрии обновлен", "settings_multiAck": "Мульти-ACK: {value}", "map_showOverlaps": "Перекрытия ключа повтора", @@ -1208,10 +1205,6 @@ } } }, - "scanner_linuxPairingShowPin": "Показать PIN-код", - "scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).", - "scanner_linuxPairingHidePin": "Скрыть PIN-код", - "scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -1253,5 +1246,16 @@ "radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм", "radioStats_stripWaiting": "Получение данных о радио…", "radioStats_settingsTile": "Статистика радиовещания", - "radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи" + "radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "Показать PIN", + "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", + "scanner_linuxPairingHidePin": "Скрыть PIN", + "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d6878ee..5a7aa6d 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ", "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný", "settings_multiAck": "Viaceré ACK: {value}", "map_showOverlaps": "Prekrývanie opakovača kľúča", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (nechajte prázdne, ak neexistuje).", - "scanner_linuxPairingShowPin": "Zobraziť PIN", - "scanner_linuxPairingHidePin": "Skryť PIN", - "scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm", "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" + "radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 04be3dc..9adb387 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil", "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_multiAck": "Večkratni potrditvi: {value}", "settings_telemetryModeUpdated": "Način telemetrije posodobljen", "map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingShowPin": "Prikaži PIN", - "scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).", - "scanner_linuxPairingHidePin": "Skrijte PIN", - "scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Število šuma: {noiseDbm} dBm", "radioStats_stripWaiting": "Prejemanje statistike o radiju…", "radioStats_settingsTile": "Radijske statistike", - "radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema" + "radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index fc21ad8..e4ace3e 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Maximalt antal försök", "appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Telemetri-läge uppdaterat", "settings_multiAck": "Multi-ACKs: {value}", "map_showOverlaps": "Repeater-nyckelöverlappningar", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Ange PIN-kod för {deviceName} (lämna tomt om ingen finns).", - "scanner_linuxPairingHidePin": "Dölj PIN-kod", - "scanner_linuxPairingShowPin": "Visa PIN-kod", - "scanner_linuxPairingPinTitle": "PIN för Bluetooth-parning" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm", "radioStats_stripWaiting": "Hämtar radiostatistik…", "radioStats_settingsTile": "Radiostation", - "radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid" + "radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "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" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 6bc0550..8e27da1 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1940,9 +1940,6 @@ "appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення", "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_telemetryModeUpdated": "Режим телеметрії оновлено", "settings_multiAck": "Багатократне підтвердження: {value}", "map_showOverlaps": "Перекриття ключа повторювача", @@ -1968,10 +1965,6 @@ } } }, - "scanner_linuxPairingHidePin": "Приховати PIN-код", - "scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth", - "scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).", - "scanner_linuxPairingShowPin": "Показати PIN-код" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2013,5 +2006,16 @@ "radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм", "radioStats_stripWaiting": "Отримано статистику радіо…", "radioStats_settingsTile": "Дані про радіостанції", - "radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал." + "radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", + "scanner_linuxPairingShowPin": "Показати PIN", + "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", + "scanner_linuxPairingHidePin": "Приховати PIN" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 203c870..cd7b44d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1945,9 +1945,6 @@ "appSettings_maxMessageRetries": "最大消息重试次数", "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", "path_routeWeight": "{weight}/{max}", - "@scanner_linuxPairingPinPrompt": { - "placeholders": { - "deviceName": { "settings_multiAck": "多重ACK:{value}", "settings_telemetryModeUpdated": "遥测模式已更新", "map_showOverlaps": "重复键重叠", @@ -1973,10 +1970,6 @@ } } }, - "scanner_linuxPairingPinTitle": "蓝牙配对 PIN", - "scanner_linuxPairingHidePin": "隐藏PIN码", - "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。", - "scanner_linuxPairingShowPin": "显示PIN码" "@radioStats_txAir": { "placeholders": { "seconds": { @@ -2018,5 +2011,16 @@ "radioStats_stripNoise": "噪声水平:{noiseDbm} dBm", "radioStats_stripWaiting": "正在获取收音机数据…", "radioStats_settingsTile": "广播统计数据", - "radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间" + "radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间", + "@scanner_linuxPairingPinPrompt": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_linuxPairingShowPin": "显示 PIN码", + "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。", + "scanner_linuxPairingPinTitle": "蓝牙配对 PIN", + "scanner_linuxPairingHidePin": "隐藏 PIN" }